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

knowledgepixels / nanopub-query / 22309839160

23 Feb 2026 02:14PM UTC coverage: 70.629% (-0.3%) from 70.899%
22309839160

push

github

tkuhn
Adjust Accept header Media types for Construct queries

215 of 328 branches covered (65.55%)

Branch coverage included in aggregate %.

593 of 816 relevant lines covered (72.67%)

10.94 hits per line

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

66.09
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.nanopub.extra.services.QueryAccess;
21
import org.slf4j.Logger;
22
import org.slf4j.LoggerFactory;
23

24
import java.util.*;
25

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

28
/**
29
 * This class produces a page with the grlc specification. This is needed internally to tell grlc
30
 * how to execute a particular query template.
31
 */
32
public class GrlcSpec {
33

34
    private static final ValueFactory vf = SimpleValueFactory.getInstance();
6✔
35

36
    private static final Logger log = LoggerFactory.getLogger(GrlcSpec.class);
9✔
37

38
    /**
39
     * Exception for invalid grlc specifications.
40
     */
41
    public static class InvalidGrlcSpecException extends Exception {
42

43
        private InvalidGrlcSpecException(String msg) {
44
            super(msg);
9✔
45
        }
3✔
46

47
        private InvalidGrlcSpecException(String msg, Throwable throwable) {
48
            super(msg, throwable);
×
49
        }
×
50

51
    }
52

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

295
    public boolean isConstructQuery() {
296
        return isConstructQuery;
9✔
297
    }
298

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

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

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

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

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

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

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

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

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

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