• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

knowledgepixels / nanopub-query / 22871832647

09 Mar 2026 07:50PM UTC coverage: 70.256% (-0.4%) from 70.691%
22871832647

Pull #59

github

web-flow
Merge ecc13bbfe into c5531c09a
Pull Request #59: Fix api-version=latest hanging for OpenAPI links

216 of 336 branches covered (64.29%)

Branch coverage included in aggregate %.

606 of 834 relevant lines covered (72.66%)

10.73 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

61.72
src/main/java/com/knowledgepixels/query/GrlcSpec.java
1
package com.knowledgepixels.query;
2

3
import io.vertx.core.MultiMap;
4
import net.trustyuri.TrustyUriUtils;
5
import org.eclipse.rdf4j.model.IRI;
6
import org.eclipse.rdf4j.model.Statement;
7
import org.eclipse.rdf4j.model.ValueFactory;
8
import org.eclipse.rdf4j.model.impl.SimpleValueFactory;
9
import org.eclipse.rdf4j.model.vocabulary.DCTERMS;
10
import org.eclipse.rdf4j.model.vocabulary.RDFS;
11
import org.eclipse.rdf4j.query.MalformedQueryException;
12
import org.eclipse.rdf4j.query.algebra.Var;
13
import org.eclipse.rdf4j.query.algebra.helpers.AbstractSimpleQueryModelVisitor;
14
import org.eclipse.rdf4j.query.parser.ParsedGraphQuery;
15
import org.eclipse.rdf4j.query.parser.ParsedQuery;
16
import org.eclipse.rdf4j.query.parser.sparql.SPARQLParser;
17
import org.nanopub.Nanopub;
18
import org.nanopub.SimpleCreatorPattern;
19
import org.nanopub.extra.server.GetNanopub;
20
import org.eclipse.rdf4j.query.QueryLanguage;
21
import org.eclipse.rdf4j.query.TupleQueryResult;
22
import org.eclipse.rdf4j.repository.RepositoryConnection;
23
import org.nanopub.vocabulary.NPA;
24
import org.nanopub.vocabulary.NPX;
25
import org.slf4j.Logger;
26
import org.slf4j.LoggerFactory;
27

28
import java.util.*;
29

30
//TODO merge this class with GrlcQuery of Nanodash and move to a library like nanopub-java
31

32
/**
33
 * This class produces a page with the grlc specification. This is needed internally to tell grlc
34
 * how to execute a particular query template.
35
 */
36
public class GrlcSpec {
37

38
    private static final ValueFactory vf = SimpleValueFactory.getInstance();
6✔
39

40
    private static final Logger logger = LoggerFactory.getLogger(GrlcSpec.class);
9✔
41

42
    /**
43
     * Exception for invalid grlc specifications.
44
     */
45
    public static class InvalidGrlcSpecException extends Exception {
46

47
        private InvalidGrlcSpecException(String msg) {
48
            super(msg);
9✔
49
        }
3✔
50

51
        private InvalidGrlcSpecException(String msg, Throwable throwable) {
52
            super(msg, throwable);
×
53
        }
×
54

55
    }
56

57
    /**
58
     * IRI for relation to link a grlc query instance to its SPARQL template.
59
     */
60
    public static final IRI HAS_SPARQL = vf.createIRI("https://w3id.org/kpxl/grlc/sparql");
12✔
61

62
    /**
63
     * IRI for relation to link a grlc query instance to its SPARQL endpoint URL.
64
     */
65
    public static final IRI HAS_ENDPOINT = vf.createIRI("https://w3id.org/kpxl/grlc/endpoint");
12✔
66

67
    /**
68
     * URL for the given Nanopub Query instance, needed for internal coordination.
69
     */
70
    public static final String nanopubQueryUrl = Utils.getEnvString("NANOPUB_QUERY_URL", "http://query:9393/");
15✔
71

72
    private static final String NANOPUB_QUERY_REPO_URL = "https://w3id.org/np/l/nanopub-query-1.1/repo/";
73

74
    private MultiMap parameters;
75
    private Nanopub np;
76
    private String requestUrlBase;
77
    private String artifactCode;
78
    private String queryPart;
79
    private String queryName;
80
    private String label;
81
    private String desc;
82
    private String license;
83
    private String queryContent;
84
    private String endpoint;
85
    private List<String> placeholdersList;
86
    private boolean isConstructQuery;
87

88
    /**
89
     * Creates a new page instance.
90
     *
91
     * @param requestUrl The request URL
92
     * @param parameters The URL request parameters
93
     */
94
    public GrlcSpec(String requestUrl, MultiMap parameters) throws InvalidGrlcSpecException {
6✔
95
        this.parameters = parameters;
9✔
96
        requestUrl = requestUrl.replaceFirst("\\?.*$", "");
15✔
97
        if (!requestUrl.matches(".*/RA[A-Za-z0-9\\-_]{43}/(.*)?")) {
12✔
98
            throw new InvalidGrlcSpecException("Invalid grlc API request: " + requestUrl);
18✔
99
        }
100
        artifactCode = requestUrl.replaceFirst("^(.*/)(RA[A-Za-z0-9\\-_]{43})/(.*)?$", "$2");
18✔
101
        requestUrlBase = requestUrl.replaceFirst("^/(.*/)(RA[A-Za-z0-9\\-_]{43})/(.*)?$", "$1");
18✔
102

103
        queryPart = requestUrl.replaceFirst("^(.*/)(RA[A-Za-z0-9\\-_]{43}/)(.*)?$", "$3");
18✔
104
        queryPart = queryPart.replaceFirst(".rq$", "");
21✔
105

106
        // TODO Get the nanopub from the local store:
107
        np = GetNanopub.get(artifactCode);
15✔
108
        if (parameters.get("api-version") != null && parameters.get("api-version").equals("latest")) {
30✔
109
            String latestUri = getLatestVersionIdLocally(np.getUri().stringValue());
18✔
110
            if (!latestUri.equals(np.getUri().stringValue())) {
21!
111
                np = GetNanopub.get(TrustyUriUtils.getArtifactCode(latestUri));
×
112
            }
113
            artifactCode = TrustyUriUtils.getArtifactCode(np.getUri().stringValue());
21✔
114
        }
115
        for (Statement st : np.getAssertion()) {
36✔
116
            if (!st.getSubject().stringValue().startsWith(np.getUri().stringValue())) continue;
27!
117
            String qn = st.getSubject().stringValue().replaceFirst("^.*[#/](.*)$", "$1");
21✔
118
            if (queryName != null && !qn.equals(queryName)) {
24!
119
                throw new InvalidGrlcSpecException("Subject suffixes don't match: " + queryName);
×
120
            }
121
            queryName = qn;
9✔
122
            if (st.getPredicate().equals(RDFS.LABEL)) {
15✔
123
                label = st.getObject().stringValue();
18✔
124
            } else if (st.getPredicate().equals(DCTERMS.DESCRIPTION)) {
15✔
125
                desc = st.getObject().stringValue();
18✔
126
            } else if (st.getPredicate().equals(DCTERMS.LICENSE) && st.getObject() instanceof IRI) {
27!
127
                license = st.getObject().stringValue();
18✔
128
            } else if (st.getPredicate().equals(HAS_SPARQL)) {
15✔
129
                // TODO Improve this:
130
                queryContent = st.getObject().stringValue().replace(NANOPUB_QUERY_REPO_URL, nanopubQueryUrl + "repo/");
30✔
131
            } else if (st.getPredicate().equals(HAS_ENDPOINT) && st.getObject() instanceof IRI) {
27!
132
                endpoint = st.getObject().stringValue();
15✔
133
                if (endpoint.startsWith(NANOPUB_QUERY_REPO_URL)) {
15!
134
                    endpoint = endpoint.replace(NANOPUB_QUERY_REPO_URL, nanopubQueryUrl + "repo/");
27✔
135
                } else {
136
                    throw new InvalidGrlcSpecException("Invalid/non-recognized endpoint: " + endpoint);
×
137
                }
138
            }
139
        }
3✔
140

141
        if (!queryPart.isEmpty() && !queryPart.equals(queryName)) {
30✔
142
            throw new InvalidGrlcSpecException("Query part doesn't match query name: " + queryPart + " / " + queryName);
27✔
143
        }
144

145
        final Set<String> placeholders = new HashSet<>();
12✔
146
        try {
147
            ParsedQuery query = new SPARQLParser().parseQuery(queryContent, null);
24✔
148
            isConstructQuery = query instanceof ParsedGraphQuery;
12✔
149
            query.getTupleExpr().visitChildren(new AbstractSimpleQueryModelVisitor<>() {
42✔
150

151
                @Override
152
                public void meet(Var node) throws RuntimeException {
153
                    super.meet(node);
9✔
154
                    if (!node.isConstant() && !node.isAnonymous() && node.getName().startsWith("_")) {
33!
155
                        placeholders.add(node.getName());
×
156
                    }
157
                }
3✔
158

159
            });
160
        } catch (MalformedQueryException ex) {
×
161
            throw new InvalidGrlcSpecException("Invalid SPARQL string", ex);
×
162
        }
3✔
163
        List<String> placeholdersListPre = new ArrayList<>(placeholders);
15✔
164
        Collections.sort(placeholdersListPre);
6✔
165
        placeholdersListPre.sort(Comparator.comparing(String::length));
12✔
166
        placeholdersList = Collections.unmodifiableList(placeholdersListPre);
12✔
167
    }
3✔
168

169
    /**
170
     * Returns the grlc spec as a string.
171
     *
172
     * @return grlc specification string
173
     */
174
    public String getSpec() {
175
        String s = "";
6✔
176
        if (queryPart.isEmpty()) {
12✔
177
            if (label == null) {
9!
178
                s += "title: \"untitled query\"\n";
×
179
            } else {
180
                s += "title: \"" + escapeLiteral(label) + "\"\n";
18✔
181
            }
182
            s += "description: \"" + escapeLiteral(desc) + "\"\n";
18✔
183
            StringBuilder userName = new StringBuilder();
12✔
184
            Set<IRI> creators = SimpleCreatorPattern.getCreators(np);
12✔
185
            for (IRI userIri : creators) {
30✔
186
                userName.append(", ").append(userIri);
18✔
187
            }
3✔
188
            if (!userName.isEmpty()) userName = new StringBuilder(userName.substring(2));
30!
189
            String url = "";
6✔
190
            if (!creators.isEmpty()) url = creators.iterator().next().stringValue();
27!
191
            s += "contact:\n";
9✔
192
            s += "  name: \"" + escapeLiteral(userName.toString()) + "\"\n";
18✔
193
            s += "  url: " + url + "\n";
12✔
194
            if (license != null) {
9!
195
                s += "licence: " + license + "\n";
15✔
196
            }
197
            s += "queries:\n";
9✔
198
            s += "  - " + nanopubQueryUrl + requestUrlBase + artifactCode + "/" + queryName + ".rq";
30✔
199
        } else if (queryPart.equals(queryName)) {
21!
200
            if (label != null) {
9!
201
                s += "#+ summary: \"" + escapeLiteral(label) + "\"\n";
18✔
202
            }
203
            if (desc != null) {
9!
204
                s += "#+ description: \"" + escapeLiteral(desc) + "\"\n";
18✔
205
            }
206
            if (license != null) {
9!
207
                s += "#+ licence: " + license + "\n";
15✔
208
            }
209
            if (endpoint != null) {
9!
210
                s += "#+ endpoint: " + endpoint + "\n";
15✔
211
            }
212
            s += "\n";
9✔
213
            s += queryContent;
18✔
214
        } else {
215
            throw new RuntimeException("Unexpected queryPart: " + queryPart);
×
216
        }
217
        return s;
6✔
218
    }
219

220
    /**
221
     * Returns the request parameters.
222
     *
223
     * @return the request parameters
224
     */
225
    public MultiMap getParameters() {
226
        return parameters;
9✔
227
    }
228

229
    /**
230
     * Returns the nanopub.
231
     *
232
     * @return the nanopub
233
     */
234
    public Nanopub getNanopub() {
235
        return np;
9✔
236
    }
237

238
    /**
239
     * Returns the artifact code.
240
     *
241
     * @return the artifact code
242
     */
243
    public String getArtifactCode() {
244
        return artifactCode;
9✔
245
    }
246

247
    /**
248
     * Returns the label.
249
     *
250
     * @return the label
251
     */
252
    public String getLabel() {
253
        return label;
9✔
254
    }
255

256
    /**
257
     * Returns the description.
258
     *
259
     * @return the description
260
     */
261
    public String getDescription() {
262
        return desc;
9✔
263
    }
264

265
    /**
266
     * Returns the query name.
267
     *
268
     * @return the query name
269
     */
270
    public String getQueryName() {
271
        return queryName;
9✔
272
    }
273

274
    /**
275
     * Returns the list of placeholders.
276
     *
277
     * @return the list of placeholders
278
     */
279
    public List<String> getPlaceholdersList() {
280
        return placeholdersList;
9✔
281
    }
282

283
    /**
284
     * Returns the repository name derived from the endpoint URL.
285
     *
286
     * @return the repository name
287
     */
288
    public String getRepoName() {
289
        return endpoint.replaceAll("/", "_").replaceFirst("^.*_repo_", "");
27✔
290
    }
291

292
    /**
293
     * Returns the query content.
294
     *
295
     * @return the query content
296
     */
297
    public String getQueryContent() {
298
        return queryContent;
9✔
299
    }
300

301
    public boolean isConstructQuery() {
302
        return isConstructQuery;
9✔
303
    }
304

305
    /**
306
     * Expands the query by replacing the placeholders with the provided parameter values.
307
     *
308
     * @return the expanded query
309
     * @throws InvalidGrlcSpecException if a non-optional placeholder is missing a value
310
     */
311
    public String expandQuery() throws InvalidGrlcSpecException {
312
        String expandedQueryContent = queryContent;
×
313
        logger.info("Expanding grlc query with parameters: {}", parameters);
×
314
        for (String ph : placeholdersList) {
×
315
            logger.info("Processing placeholder <{}> associated to parameter with name <{}>", ph, getParamName(ph));
×
316
            if (isMultiPlaceholder(ph)) {
×
317
                // TODO multi placeholders need proper documentation
318
                List<String> val = parameters.getAll(getParamName(ph));
×
319
                if (!isOptionalPlaceholder(ph) && val.isEmpty()) {
×
320
                    throw new InvalidGrlcSpecException("Missing value for non-optional placeholder: " + ph);
×
321
                }
322
                if (val.isEmpty()) {
×
323
                    expandedQueryContent = expandedQueryContent.replaceAll("values\\s*\\?" + ph + "\\s*\\{\\s*\\}(\\s*\\.)?", "");
×
324
                    continue;
×
325
                }
326
                String valueList = "";
×
327
                for (String v : val) {
×
328
                    if (isIriPlaceholder(ph)) {
×
329
                        valueList += serializeIri(v) + " ";
×
330
                    } else {
331
                        valueList += serializeLiteral(v) + " ";
×
332
                    }
333
                }
×
334
                expandedQueryContent = expandedQueryContent.replaceAll("values\\s*\\?" + ph + "\\s*\\{\\s*\\}", "values ?" + ph + " { " + escapeSlashes(valueList) + "}");
×
335
            } else {
×
336
                String val = parameters.get(getParamName(ph));
×
337
                logger.info("Value for placeholder <{}>: {}", ph, val);
×
338
                if (!isOptionalPlaceholder(ph) && val == null) {
×
339
                    throw new InvalidGrlcSpecException("Missing value for non-optional placeholder: " + ph);
×
340
                }
341
                if (val == null) continue;
×
342
                if (isIriPlaceholder(ph)) {
×
343
                    expandedQueryContent = expandedQueryContent.replaceAll("\\?" + ph, escapeSlashes(serializeIri(val)));
×
344
                } else {
345
                    expandedQueryContent = expandedQueryContent.replaceAll("\\?" + ph, escapeSlashes(serializeLiteral(val)));
×
346
                }
347
            }
348
        }
×
349
        return expandedQueryContent;
×
350
    }
351

352
    /**
353
     * Escapes a literal string for SPARQL.
354
     *
355
     * @param s The string
356
     * @return The escaped string
357
     */
358
    public static String escapeLiteral(String s) {
359
        return s.replace("\\", "\\\\").replace("\n", "\\n").replace("\"", "\\\"");
33✔
360
    }
361

362
    /**
363
     * Checks whether the given placeholder is an optional placeholder.
364
     *
365
     * @param placeholder The placeholder name
366
     * @return true if it is an optional placeholder, false otherwise
367
     */
368
    public static boolean isOptionalPlaceholder(String placeholder) {
369
        return placeholder.startsWith("__");
12✔
370
    }
371

372
    /**
373
     * Checks whether the given placeholder is a multi-value placeholder.
374
     *
375
     * @param placeholder The placeholder name
376
     * @return true if it is a multi-value placeholder, false otherwise
377
     */
378
    public static boolean isMultiPlaceholder(String placeholder) {
379
        return placeholder.endsWith("_multi") || placeholder.endsWith("_multi_iri");
36✔
380
    }
381

382
    /**
383
     * Checks whether the given placeholder is an IRI placeholder.
384
     *
385
     * @param placeholder The placeholder name
386
     * @return true if it is an IRI placeholder, false otherwise
387
     */
388
    public static boolean isIriPlaceholder(String placeholder) {
389
        return placeholder.endsWith("_iri");
12✔
390
    }
391

392
    /**
393
     * Returns the parameter name for the given placeholder.
394
     *
395
     * @param placeholder The placeholder name
396
     * @return The parameter name
397
     */
398
    public static String getParamName(String placeholder) {
399
        return placeholder.replaceFirst("^_+", "").replaceFirst("_iri$", "").replaceFirst("_multi$", "");
33✔
400
    }
401

402
    /**
403
     * Serializes an IRI string for SPARQL.
404
     *
405
     * @param iriString The IRI string
406
     * @return The serialized IRI
407
     */
408
    public static String serializeIri(String iriString) {
409
        return "<" + iriString + ">";
9✔
410
    }
411

412
    /**
413
     * Escapes slashes in a string.
414
     *
415
     * @param string The string
416
     * @return The escaped string
417
     */
418
    private static String escapeSlashes(String string) {
419
        return string.replace("\\", "\\\\");
×
420
    }
421

422
    /**
423
     * Serializes a literal string for SPARQL.
424
     *
425
     * @param literalString The literal string
426
     * @return The serialized literal
427
     */
428
    public static String serializeLiteral(String literalString) {
429
        return "\"" + escapeLiteral(literalString) + "\"";
12✔
430
    }
431

432
    /**
433
     * Resolves the latest version of a nanopub by following the supersedes chain in the local store.
434
     * Uses a single SPARQL query with a property path to find the latest non-invalidated version
435
     * signed by the same key. If no result is found locally, or if the local store is unavailable,
436
     * returns the original URI.
437
     *
438
     * @param nanopubUri the URI of the nanopub to resolve
439
     * @return the URI of the latest version
440
     */
441
    static String getLatestVersionIdLocally(String nanopubUri) {
442
        logger.info("Resolving latest version locally for: {}", nanopubUri);
12✔
443
        try {
444
            RepositoryConnection conn = TripleStore.get().getRepoConnection("meta");
×
445
            try (conn) {
×
446
                String query =
×
447
                    "SELECT ?latest ?date WHERE { " +
448
                    "GRAPH <" + NPA.GRAPH + "> { " +
449
                        "<" + nanopubUri + "> <" + NPA.HAS_VALID_SIGNATURE_FOR_PUBLIC_KEY + "> ?pubkey . " +
450
                        "?latest <" + NPA.HAS_VALID_SIGNATURE_FOR_PUBLIC_KEY + "> ?pubkey . " +
451
                        "FILTER NOT EXISTS { ?npx <" + NPX.INVALIDATES + "> ?latest ; " +
452
                            "<" + NPA.HAS_VALID_SIGNATURE_FOR_PUBLIC_KEY + "> ?pubkey . } " +
453
                        "?latest <" + DCTERMS.CREATED + "> ?date . " +
454
                    "} " +
455
                    "GRAPH <" + NPA.NETWORK_GRAPH + "> { " +
456
                        "?latest (<" + NPX.SUPERSEDES + ">)* <" + nanopubUri + "> . " +
457
                    "} " +
458
                    "} ORDER BY DESC(?date) LIMIT 1";
459
                TupleQueryResult r = conn.prepareTupleQuery(QueryLanguage.SPARQL, query).evaluate();
×
460
                try (r) {
×
461
                    if (r.hasNext()) {
×
462
                        String latestUri = r.next().getBinding("latest").getValue().stringValue();
×
463
                        logger.info("Resolved latest version: {}", latestUri);
×
464
                        return latestUri;
×
465
                    }
466
                }
×
467
                logger.info("No latest version found locally for: {}", nanopubUri);
×
468
                return nanopubUri;
×
469
            }
×
470
        } catch (Exception ex) {
3✔
471
            logger.warn("Could not resolve latest version locally, using original version: {}", ex.getMessage());
15✔
472
            return nanopubUri;
6✔
473
        }
474
    }
475

476
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc