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

knowledgepixels / nanopub-query / 23233444105

18 Mar 2026 07:16AM UTC coverage: 69.473% (-0.1%) from 69.573%
23233444105

push

github

ashleycaselli
chore(vocabulary): add KPXL_GRLC class for GRLC vocabulary IRIs and update references in GrlcSpec

216 of 336 branches covered (64.29%)

Branch coverage included in aggregate %.

601 of 840 relevant lines covered (71.55%)

10.71 hits per line

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

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

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

29
import java.util.*;
30

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

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

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

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

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

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

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

56
    }
57

58
    /**
59
     * URL for the given Nanopub Query instance, needed for internal coordination.
60
     */
61
    public static final String nanopubQueryUrl = Utils.getEnvString("NANOPUB_QUERY_URL", "http://query:9393/");
15✔
62

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

65
    private MultiMap parameters;
66
    private Nanopub np;
67
    private String requestUrlBase;
68
    private String artifactCode;
69
    private String queryPart;
70
    private String queryName;
71
    private String label;
72
    private String desc;
73
    private String license;
74
    private String queryContent;
75
    private String endpoint;
76
    private List<String> placeholdersList;
77
    private boolean isConstructQuery;
78

79
    /**
80
     * Creates a new page instance.
81
     *
82
     * @param requestUrl The request URL
83
     * @param parameters The URL request parameters
84
     */
85
    public GrlcSpec(String requestUrl, MultiMap parameters) throws InvalidGrlcSpecException {
6✔
86
        this.parameters = parameters;
9✔
87
        requestUrl = requestUrl.replaceFirst("\\?.*$", "");
15✔
88
        if (!requestUrl.matches(".*/RA[A-Za-z0-9\\-_]{43}/(.*)?")) {
12✔
89
            throw new InvalidGrlcSpecException("Invalid grlc API request: " + requestUrl);
18✔
90
        }
91
        artifactCode = requestUrl.replaceFirst("^(.*/)(RA[A-Za-z0-9\\-_]{43})/(.*)?$", "$2");
18✔
92
        requestUrlBase = requestUrl.replaceFirst("^/(.*/)(RA[A-Za-z0-9\\-_]{43})/(.*)?$", "$1");
18✔
93

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

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

134
        if (!queryPart.isEmpty() && !queryPart.equals(queryName)) {
30✔
135
            throw new InvalidGrlcSpecException("Query part doesn't match query name: " + queryPart + " / " + queryName);
27✔
136
        }
137

138
        final Set<String> placeholders = new HashSet<>();
12✔
139
        try {
140
            ParsedQuery query = new SPARQLParser().parseQuery(queryContent, null);
24✔
141
            isConstructQuery = query instanceof ParsedGraphQuery;
12✔
142
            query.getTupleExpr().visitChildren(new AbstractSimpleQueryModelVisitor<>() {
42✔
143

144
                @Override
145
                public void meet(Var node) throws RuntimeException {
146
                    super.meet(node);
9✔
147
                    if (!node.isConstant() && !node.isAnonymous() && node.getName().startsWith("_")) {
33!
148
                        placeholders.add(node.getName());
×
149
                    }
150
                }
3✔
151

152
            });
153
        } catch (MalformedQueryException ex) {
×
154
            throw new InvalidGrlcSpecException("Invalid SPARQL string", ex);
×
155
        }
3✔
156
        List<String> placeholdersListPre = new ArrayList<>(placeholders);
15✔
157
        Collections.sort(placeholdersListPre);
6✔
158
        placeholdersListPre.sort(Comparator.comparing(String::length));
12✔
159
        placeholdersList = Collections.unmodifiableList(placeholdersListPre);
12✔
160
    }
3✔
161

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

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

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

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

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

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

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

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

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

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

298
    public boolean isConstructQuery() {
299
        return isConstructQuery;
9✔
300
    }
301

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

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

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

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

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

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

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

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

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

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

475
}
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