diff --git a/pom.xml b/pom.xml index 3a02f75..d4018eb 100644 --- a/pom.xml +++ b/pom.xml @@ -106,14 +106,8 @@ org.nanopub nanopub - 1.87.1 + 1.89.0 - - 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: + * + *

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"); - -}