org.apache.commons
commons-exec
diff --git a/src/main/java/com/knowledgepixels/query/GrlcSpec.java b/src/main/java/com/knowledgepixels/query/GrlcSpec.java
index ede038a..077469d 100644
--- a/src/main/java/com/knowledgepixels/query/GrlcSpec.java
+++ b/src/main/java/com/knowledgepixels/query/GrlcSpec.java
@@ -1,31 +1,19 @@
package com.knowledgepixels.query;
-import com.knowledgepixels.query.vocabulary.KPXL_GRLC;
import io.vertx.core.MultiMap;
import net.trustyuri.TrustyUriUtils;
import org.eclipse.rdf4j.model.IRI;
-import org.eclipse.rdf4j.model.Statement;
-import org.eclipse.rdf4j.model.ValueFactory;
-import org.eclipse.rdf4j.model.impl.SimpleValueFactory;
import org.eclipse.rdf4j.model.vocabulary.DCTERMS;
-import org.eclipse.rdf4j.model.vocabulary.RDFS;
-import org.eclipse.rdf4j.query.MalformedQueryException;
import org.eclipse.rdf4j.query.QueryLanguage;
import org.eclipse.rdf4j.query.TupleQueryResult;
-import org.eclipse.rdf4j.query.algebra.Var;
-import org.eclipse.rdf4j.query.algebra.helpers.AbstractSimpleQueryModelVisitor;
-import org.eclipse.rdf4j.query.parser.ParsedGraphQuery;
-import org.eclipse.rdf4j.query.parser.ParsedQuery;
-import org.eclipse.rdf4j.query.parser.sparql.SPARQLParser;
+import org.eclipse.rdf4j.repository.RepositoryConnection;
import org.eclipse.rdf4j.rio.RDFFormat;
import org.nanopub.MalformedNanopubException;
-import org.eclipse.rdf4j.repository.RepositoryConnection;
import org.nanopub.Nanopub;
import org.nanopub.NanopubImpl;
import org.nanopub.SimpleCreatorPattern;
import org.nanopub.extra.server.GetNanopub;
-
-import java.util.concurrent.ConcurrentHashMap;
+import org.nanopub.extra.services.QueryTemplate;
import org.nanopub.vocabulary.NPA;
import org.nanopub.vocabulary.NPX;
import org.slf4j.Logger;
@@ -33,18 +21,33 @@
import java.io.ByteArrayInputStream;
import java.io.IOException;
-import java.util.*;
-
-//TODO merge this class with GrlcQuery of Nanodash and move to a library like nanopub-java
+import java.util.ArrayList;
+import java.util.Base64;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
/**
- * This class produces a page with the grlc specification. This is needed internally to tell grlc
- * how to execute a particular query template.
+ * Nanopub Query-specific wrapper around {@link QueryTemplate} that adds:
+ *
+ * - request-URL parsing ({@code /…/RA…/{name}.rq})
+ * - nanopub fetch cache
+ * - the {@code _nanopub_trig} inline-nanopub parameter
+ * - {@code api-version=latest} resolution against the local meta repo
+ * - rewriting the canonical {@code https://w3id.org/np/l/nanopub-query-1.1/repo/}
+ * endpoint prefix to the in-cluster {@code NANOPUB_QUERY_URL/repo/}, plus
+ * validation that the endpoint matches the canonical prefix
+ * - {@link #getSpec()} YAML rendering for the legacy {@code /grlc-spec/} route
+ * - {@link #getRepoName()} derived from the rewritten endpoint
+ *
+ * Parsing, placeholder extraction and SPARQL expansion are delegated to
+ * {@link QueryTemplate}. Static placeholder helpers are forwarded so existing
+ * callers ({@link OpenApiSpecPage}) keep compiling.
*/
public class GrlcSpec {
- private static final ValueFactory vf = SimpleValueFactory.getInstance();
-
private static final Logger logger = LoggerFactory.getLogger(GrlcSpec.class);
private static final ConcurrentHashMap nanopubCache = new ConcurrentHashMap<>();
@@ -71,19 +74,13 @@ private InvalidGrlcSpecException(String msg, Throwable throwable) {
private static final String NANOPUB_QUERY_REPO_URL = "https://w3id.org/np/l/nanopub-query-1.1/repo/";
- private MultiMap parameters;
- private Nanopub np;
- private String requestUrlBase;
- private String artifactCode;
- private String queryPart;
- private String queryName;
- private String label;
- private String desc;
- private String license;
- private String queryContent;
- private String endpoint;
- private List placeholdersList;
- private boolean isConstructQuery;
+ private final MultiMap parameters;
+ private final QueryTemplate template;
+ private final String requestUrlBase;
+ private final String artifactCode;
+ private final String queryPart;
+ private final String queryContent;
+ private final String endpoint;
/**
* Creates a new page instance.
@@ -97,12 +94,14 @@ public GrlcSpec(String requestUrl, MultiMap parameters) throws InvalidGrlcSpecEx
if (!requestUrl.matches(".*/RA[A-Za-z0-9\\-_]{43}/(.*)?")) {
throw new InvalidGrlcSpecException("Invalid grlc API request: " + requestUrl);
}
- artifactCode = requestUrl.replaceFirst("^(.*/)(RA[A-Za-z0-9\\-_]{43})/(.*)?$", "$2");
+ String parsedArtifactCode = requestUrl.replaceFirst("^(.*/)(RA[A-Za-z0-9\\-_]{43})/(.*)?$", "$2");
requestUrlBase = requestUrl.replaceFirst("^/(.*/)(RA[A-Za-z0-9\\-_]{43})/(.*)?$", "$1");
- queryPart = requestUrl.replaceFirst("^(.*/)(RA[A-Za-z0-9\\-_]{43}/)(.*)?$", "$3");
- queryPart = queryPart.replaceFirst(".rq$", "");
+ String parsedQueryPart = requestUrl.replaceFirst("^(.*/)(RA[A-Za-z0-9\\-_]{43}/)(.*)?$", "$3");
+ parsedQueryPart = parsedQueryPart.replaceFirst(".rq$", "");
+ queryPart = parsedQueryPart;
+ Nanopub np;
String nanopubParam = parameters.get("_nanopub_trig");
if (nanopubParam != null && !nanopubParam.isEmpty()) {
try {
@@ -112,7 +111,7 @@ public GrlcSpec(String requestUrl, MultiMap parameters) throws InvalidGrlcSpecEx
throw new InvalidGrlcSpecException("Failed to parse nanopub from 'nanopub' parameter", ex);
}
} else {
- np = nanopubCache.computeIfAbsent(artifactCode, GetNanopub::get);
+ np = nanopubCache.computeIfAbsent(parsedArtifactCode, GetNanopub::get);
}
// TODO rename "api-version" to "_api_version" for consistency
if (parameters.get("api-version") != null && parameters.get("api-version").equals("latest")) {
@@ -120,62 +119,37 @@ public GrlcSpec(String requestUrl, MultiMap parameters) throws InvalidGrlcSpecEx
if (!latestUri.equals(np.getUri().stringValue())) {
np = nanopubCache.computeIfAbsent(TrustyUriUtils.getArtifactCode(latestUri), GetNanopub::get);
}
- artifactCode = TrustyUriUtils.getArtifactCode(np.getUri().stringValue());
+ parsedArtifactCode = TrustyUriUtils.getArtifactCode(np.getUri().stringValue());
}
- for (Statement st : np.getAssertion()) {
- if (!st.getSubject().stringValue().startsWith(np.getUri().stringValue())) {
- continue;
- }
- String qn = st.getSubject().stringValue().replaceFirst("^.*[#/](.*)$", "$1");
- if (queryName != null && !qn.equals(queryName)) {
- throw new InvalidGrlcSpecException("Subject suffixes don't match: " + queryName);
- }
- queryName = qn;
- if (st.getPredicate().equals(RDFS.LABEL)) {
- label = st.getObject().stringValue();
- } else if (st.getPredicate().equals(DCTERMS.DESCRIPTION)) {
- desc = st.getObject().stringValue();
- } else if (st.getPredicate().equals(DCTERMS.LICENSE) && st.getObject() instanceof IRI) {
- license = st.getObject().stringValue();
- } else if (st.getPredicate().equals(KPXL_GRLC.SPARQL)) {
- // TODO Improve this:
- queryContent = st.getObject().stringValue().replace(NANOPUB_QUERY_REPO_URL, nanopubQueryUrl + "repo/");
- } else if (st.getPredicate().equals(KPXL_GRLC.ENDPOINT) && st.getObject() instanceof IRI) {
- endpoint = st.getObject().stringValue();
- if (endpoint.startsWith(NANOPUB_QUERY_REPO_URL)) {
- endpoint = endpoint.replace(NANOPUB_QUERY_REPO_URL, nanopubQueryUrl + "repo/");
- } else {
- throw new InvalidGrlcSpecException("Invalid/non-recognized endpoint: " + endpoint);
- }
+ artifactCode = parsedArtifactCode;
+
+ try {
+ if (queryPart.isEmpty()) {
+ template = new QueryTemplate(np);
+ } else {
+ template = new QueryTemplate(np, artifactCode + "/" + queryPart);
}
+ } catch (IllegalArgumentException ex) {
+ throw new InvalidGrlcSpecException(ex.getMessage(), ex);
}
- if (!queryPart.isEmpty() && !queryPart.equals(queryName)) {
- throw new InvalidGrlcSpecException("Query part doesn't match query name: " + queryPart + " / " + queryName);
+ if (!queryPart.isEmpty() && !queryPart.equals(template.getQuerySuffix())) {
+ throw new InvalidGrlcSpecException(
+ "Query part doesn't match query name: " + queryPart + " / " + template.getQuerySuffix());
}
- final Set placeholders = new HashSet<>();
- try {
- ParsedQuery query = new SPARQLParser().parseQuery(queryContent, null);
- isConstructQuery = query instanceof ParsedGraphQuery;
- query.getTupleExpr().visitChildren(new AbstractSimpleQueryModelVisitor<>() {
-
- @Override
- public void meet(Var node) throws RuntimeException {
- super.meet(node);
- if (!node.isConstant() && !node.isAnonymous() && node.getName().startsWith("_")) {
- placeholders.add(node.getName());
- }
- }
+ queryContent = template.getSparql().replace(NANOPUB_QUERY_REPO_URL, nanopubQueryUrl + "repo/");
- });
- } catch (MalformedQueryException ex) {
- throw new InvalidGrlcSpecException("Invalid SPARQL string", ex);
+ IRI rawEndpoint = template.getEndpoint();
+ if (rawEndpoint != null) {
+ String ep = rawEndpoint.stringValue();
+ if (!ep.startsWith(NANOPUB_QUERY_REPO_URL)) {
+ throw new InvalidGrlcSpecException("Invalid/non-recognized endpoint: " + ep);
+ }
+ endpoint = ep.replace(NANOPUB_QUERY_REPO_URL, nanopubQueryUrl + "repo/");
+ } else {
+ endpoint = null;
}
- List placeholdersListPre = new ArrayList<>(placeholders);
- Collections.sort(placeholdersListPre);
- placeholdersListPre.sort(Comparator.comparing(String::length));
- placeholdersList = Collections.unmodifiableList(placeholdersListPre);
}
/**
@@ -185,15 +159,19 @@ public void meet(Var node) throws RuntimeException {
*/
public String getSpec() {
String s = "";
+ String label = template.getLabel();
+ String desc = template.getDescription();
+ IRI license = template.getLicense();
+ String queryName = template.getQuerySuffix();
if (queryPart.isEmpty()) {
if (label == null) {
s += "title: \"untitled query\"\n";
} else {
- s += "title: \"" + escapeLiteral(label) + "\"\n";
+ s += "title: \"" + QueryTemplate.escapeLiteral(label) + "\"\n";
}
- s += "description: \"" + escapeLiteral(desc) + "\"\n";
+ s += "description: \"" + QueryTemplate.escapeLiteral(desc) + "\"\n";
StringBuilder userName = new StringBuilder();
- Set creators = SimpleCreatorPattern.getCreators(np);
+ Set creators = SimpleCreatorPattern.getCreators(template.getNanopub());
for (IRI userIri : creators) {
userName.append(", ").append(userIri);
}
@@ -205,22 +183,22 @@ public String getSpec() {
url = creators.iterator().next().stringValue();
}
s += "contact:\n";
- s += " name: \"" + escapeLiteral(userName.toString()) + "\"\n";
+ s += " name: \"" + QueryTemplate.escapeLiteral(userName.toString()) + "\"\n";
s += " url: " + url + "\n";
if (license != null) {
- s += "licence: " + license + "\n";
+ s += "licence: " + license.stringValue() + "\n";
}
s += "queries:\n";
s += " - " + nanopubQueryUrl + requestUrlBase + artifactCode + "/" + queryName + ".rq";
} else if (queryPart.equals(queryName)) {
if (label != null) {
- s += "#+ summary: \"" + escapeLiteral(label) + "\"\n";
+ s += "#+ summary: \"" + QueryTemplate.escapeLiteral(label) + "\"\n";
}
if (desc != null) {
- s += "#+ description: \"" + escapeLiteral(desc) + "\"\n";
+ s += "#+ description: \"" + QueryTemplate.escapeLiteral(desc) + "\"\n";
}
if (license != null) {
- s += "#+ licence: " + license + "\n";
+ s += "#+ licence: " + license.stringValue() + "\n";
}
if (endpoint != null) {
s += "#+ endpoint: " + endpoint + "\n";
@@ -248,7 +226,7 @@ public MultiMap getParameters() {
* @return the nanopub
*/
public Nanopub getNanopub() {
- return np;
+ return template.getNanopub();
}
/**
@@ -266,7 +244,7 @@ public String getArtifactCode() {
* @return the label
*/
public String getLabel() {
- return label;
+ return template.getLabel();
}
/**
@@ -275,7 +253,7 @@ public String getLabel() {
* @return the description
*/
public String getDescription() {
- return desc;
+ return template.getDescription();
}
/**
@@ -284,7 +262,7 @@ public String getDescription() {
* @return the query name
*/
public String getQueryName() {
- return queryName;
+ return template.getQuerySuffix();
}
/**
@@ -293,7 +271,7 @@ public String getQueryName() {
* @return the list of placeholders
*/
public List getPlaceholdersList() {
- return placeholdersList;
+ return template.getPlaceholdersList();
}
/**
@@ -306,7 +284,8 @@ public String getRepoName() {
}
/**
- * Returns the query content.
+ * Returns the query content (with the canonical repo URL rewritten to the
+ * in-cluster {@link #nanopubQueryUrl}{@code /repo/}).
*
* @return the query content
*/
@@ -315,56 +294,28 @@ public String getQueryContent() {
}
public boolean isConstructQuery() {
- return isConstructQuery;
+ return template.isConstructQuery();
}
/**
- * Expands the query by replacing the placeholders with the provided parameter values.
+ * Expands the query by replacing the placeholders with the provided parameter
+ * values, and rewrites the canonical repo URL to the in-cluster one.
*
* @return the expanded query
* @throws InvalidGrlcSpecException if a non-optional placeholder is missing a value
*/
public String expandQuery() throws InvalidGrlcSpecException {
- String expandedQueryContent = queryContent;
+ Map> params = new LinkedHashMap<>();
+ for (String name : parameters.names()) {
+ params.put(name, new ArrayList<>(parameters.getAll(name)));
+ }
logger.info("Expanding grlc query with parameters: {}", parameters);
- for (String ph : placeholdersList) {
- logger.info("Processing placeholder <{}> associated to parameter with name <{}>", ph, getParamName(ph));
- if (isMultiPlaceholder(ph)) {
- // TODO multi placeholders need proper documentation
- List val = parameters.getAll(getParamName(ph));
- if (!isOptionalPlaceholder(ph) && val.isEmpty()) {
- throw new InvalidGrlcSpecException("Missing value for non-optional placeholder: " + ph);
- }
- if (val.isEmpty()) {
- expandedQueryContent = expandedQueryContent.replaceAll("values\\s*\\?" + ph + "\\s*\\{\\s*\\}(\\s*\\.)?", "");
- continue;
- }
- String valueList = "";
- for (String v : val) {
- if (isIriPlaceholder(ph)) {
- valueList += serializeIri(v) + " ";
- } else {
- valueList += serializeLiteral(v) + " ";
- }
- }
- expandedQueryContent = expandedQueryContent.replaceAll("values\\s*\\?" + ph + "\\s*\\{\\s*\\}", "values ?" + ph + " { " + escapeSlashes(valueList) + "}");
- } else {
- String val = parameters.get(getParamName(ph));
- logger.info("Value for placeholder <{}>: {}", ph, val);
- if (!isOptionalPlaceholder(ph) && val == null) {
- throw new InvalidGrlcSpecException("Missing value for non-optional placeholder: " + ph);
- }
- if (val == null) {
- continue;
- }
- if (isIriPlaceholder(ph)) {
- expandedQueryContent = expandedQueryContent.replaceAll("\\?" + ph, escapeSlashes(serializeIri(val)));
- } else {
- expandedQueryContent = expandedQueryContent.replaceAll("\\?" + ph, escapeSlashes(serializeLiteral(val)));
- }
- }
+ try {
+ String expanded = template.expandQuery(params);
+ return expanded.replace(NANOPUB_QUERY_REPO_URL, nanopubQueryUrl + "repo/");
+ } catch (IllegalArgumentException ex) {
+ throw new InvalidGrlcSpecException(ex.getMessage(), ex);
}
- return expandedQueryContent;
}
/**
@@ -374,7 +325,7 @@ public String expandQuery() throws InvalidGrlcSpecException {
* @return The escaped string
*/
public static String escapeLiteral(String s) {
- return s.replace("\\", "\\\\").replace("\n", "\\n").replace("\"", "\\\"");
+ return QueryTemplate.escapeLiteral(s);
}
/**
@@ -384,7 +335,7 @@ public static String escapeLiteral(String s) {
* @return true if it is an optional placeholder, false otherwise
*/
public static boolean isOptionalPlaceholder(String placeholder) {
- return placeholder.startsWith("__");
+ return QueryTemplate.isOptionalPlaceholder(placeholder);
}
/**
@@ -394,7 +345,7 @@ public static boolean isOptionalPlaceholder(String placeholder) {
* @return true if it is a multi-value placeholder, false otherwise
*/
public static boolean isMultiPlaceholder(String placeholder) {
- return placeholder.endsWith("_multi") || placeholder.endsWith("_multi_iri");
+ return QueryTemplate.isMultiPlaceholder(placeholder);
}
/**
@@ -404,7 +355,7 @@ public static boolean isMultiPlaceholder(String placeholder) {
* @return true if it is an IRI placeholder, false otherwise
*/
public static boolean isIriPlaceholder(String placeholder) {
- return placeholder.endsWith("_iri");
+ return QueryTemplate.isIriPlaceholder(placeholder);
}
/**
@@ -414,7 +365,7 @@ public static boolean isIriPlaceholder(String placeholder) {
* @return The parameter name
*/
public static String getParamName(String placeholder) {
- return placeholder.replaceFirst("^_+", "").replaceFirst("_iri$", "").replaceFirst("_multi$", "");
+ return QueryTemplate.getParamName(placeholder);
}
/**
@@ -424,17 +375,7 @@ public static String getParamName(String placeholder) {
* @return The serialized IRI
*/
public static String serializeIri(String iriString) {
- return "<" + iriString + ">";
- }
-
- /**
- * Escapes slashes in a string.
- *
- * @param string The string
- * @return The escaped string
- */
- private static String escapeSlashes(String string) {
- return string.replace("\\", "\\\\");
+ return QueryTemplate.serializeIri(iriString);
}
/**
@@ -444,7 +385,7 @@ private static String escapeSlashes(String string) {
* @return The serialized literal
*/
public static String serializeLiteral(String literalString) {
- return "\"" + escapeLiteral(literalString) + "\"";
+ return QueryTemplate.serializeLiteral(literalString);
}
/**
diff --git a/src/main/java/com/knowledgepixels/query/vocabulary/KPXL_GRLC.java b/src/main/java/com/knowledgepixels/query/vocabulary/KPXL_GRLC.java
deleted file mode 100644
index 2f3334e..0000000
--- a/src/main/java/com/knowledgepixels/query/vocabulary/KPXL_GRLC.java
+++ /dev/null
@@ -1,23 +0,0 @@
-package com.knowledgepixels.query.vocabulary;
-
-import org.eclipse.rdf4j.model.IRI;
-import org.eclipse.rdf4j.model.Namespace;
-import org.nanopub.vocabulary.VocabUtils;
-
-public class KPXL_GRLC {
-
- public static final String NAMESPACE = "https://w3id.org/kpxl/grlc/";
- public static final String PREFIX = "kpxl_grlc";
- public static final Namespace NS = VocabUtils.createNamespace(PREFIX, NAMESPACE);
-
- /**
- * IRI for relation to link a grlc query instance to its SPARQL endpoint URL.
- */
- public static final IRI ENDPOINT = VocabUtils.createIRI(NAMESPACE, "endpoint");
-
- /**
- * IRI for relation to link a grlc query instance to its SPARQL template.
- */
- public static final IRI SPARQL = VocabUtils.createIRI(NAMESPACE, "sparql");
-
-}