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

knowledgepixels / nanopub-query / 19074486015

04 Nov 2025 03:48PM UTC coverage: 71.493% (+0.4%) from 71.131%
19074486015

push

github

ashleycaselli
test(GrlcSpec): add unit tests for class methods

210 of 320 branches covered (65.63%)

Branch coverage included in aggregate %.

580 of 785 relevant lines covered (73.89%)

3.72 hits per line

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

65.79
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.ParsedQuery;
15
import org.eclipse.rdf4j.query.parser.sparql.SPARQLParser;
16
import org.nanopub.Nanopub;
17
import org.nanopub.SimpleCreatorPattern;
18
import org.nanopub.extra.server.GetNanopub;
19
import org.nanopub.extra.services.QueryAccess;
20
import org.slf4j.Logger;
21
import org.slf4j.LoggerFactory;
22

23
import java.util.*;
24

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

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

33
    private static final ValueFactory vf = SimpleValueFactory.getInstance();
2✔
34

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

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

42
        private InvalidGrlcSpecException(String msg) {
43
            super(msg);
3✔
44
        }
1✔
45

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

50
    }
51

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

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

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

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

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

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

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

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

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

137
        final Set<String> placeholders = new HashSet<>();
4✔
138
        try {
139
            ParsedQuery query = new SPARQLParser().parseQuery(queryContent, null);
8✔
140
            query.getTupleExpr().visitChildren(new AbstractSimpleQueryModelVisitor<>() {
14✔
141

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

150
            });
151
        } catch (MalformedQueryException ex) {
×
152
            throw new InvalidGrlcSpecException("Invalid SPARQL string", ex);
×
153
        }
1✔
154
        List<String> placeholdersListPre = new ArrayList<>(placeholders);
5✔
155
        Collections.sort(placeholdersListPre);
2✔
156
        placeholdersListPre.sort(Comparator.comparing(String::length));
4✔
157
        placeholdersList = Collections.unmodifiableList(placeholdersListPre);
4✔
158
    }
1✔
159

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

211
    /**
212
     * Returns the request parameters.
213
     *
214
     * @return the request parameters
215
     */
216
    public MultiMap getParameters() {
217
        return parameters;
3✔
218
    }
219

220
    /**
221
     * Returns the nanopub.
222
     *
223
     * @return the nanopub
224
     */
225
    public Nanopub getNanopub() {
226
        return np;
3✔
227
    }
228

229
    /**
230
     * Returns the artifact code.
231
     *
232
     * @return the artifact code
233
     */
234
    public String getArtifactCode() {
235
        return artifactCode;
3✔
236
    }
237

238
    /**
239
     * Returns the label.
240
     *
241
     * @return the label
242
     */
243
    public String getLabel() {
244
        return label;
3✔
245
    }
246

247
    /**
248
     * Returns the description.
249
     *
250
     * @return the description
251
     */
252
    public String getDescription() {
253
        return desc;
3✔
254
    }
255

256
    /**
257
     * Returns the query name.
258
     *
259
     * @return the query name
260
     */
261
    public String getQueryName() {
262
        return queryName;
3✔
263
    }
264

265
    /**
266
     * Returns the list of placeholders.
267
     *
268
     * @return the list of placeholders
269
     */
270
    public List<String> getPlaceholdersList() {
271
        return placeholdersList;
3✔
272
    }
273

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

283
    /**
284
     * Returns the query content.
285
     *
286
     * @return the query content
287
     */
288
    public String getQueryContent() {
289
        return queryContent;
3✔
290
    }
291

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

340
    /**
341
     * Escapes a literal string for SPARQL.
342
     *
343
     * @param s The string
344
     * @return The escaped string
345
     */
346
    public static String escapeLiteral(String s) {
347
        return s.replace("\\", "\\\\").replace("\n", "\\n").replace("\"", "\\\"");
11✔
348
    }
349

350
    /**
351
     * Checks whether the given placeholder is an optional placeholder.
352
     *
353
     * @param placeholder The placeholder name
354
     * @return true if it is an optional placeholder, false otherwise
355
     */
356
    public static boolean isOptionalPlaceholder(String placeholder) {
357
        return placeholder.startsWith("__");
4✔
358
    }
359

360
    /**
361
     * Checks whether the given placeholder is a multi-value placeholder.
362
     *
363
     * @param placeholder The placeholder name
364
     * @return true if it is a multi-value placeholder, false otherwise
365
     */
366
    public static boolean isMultiPlaceholder(String placeholder) {
367
        return placeholder.endsWith("_multi") || placeholder.endsWith("_multi_iri");
12✔
368
    }
369

370
    /**
371
     * Checks whether the given placeholder is an IRI placeholder.
372
     *
373
     * @param placeholder The placeholder name
374
     * @return true if it is an IRI placeholder, false otherwise
375
     */
376
    public static boolean isIriPlaceholder(String placeholder) {
377
        return placeholder.endsWith("_iri");
4✔
378
    }
379

380
    /**
381
     * Returns the parameter name for the given placeholder.
382
     *
383
     * @param placeholder The placeholder name
384
     * @return The parameter name
385
     */
386
    public static String getParamName(String placeholder) {
387
        return placeholder.replaceFirst("^_+", "").replaceFirst("_iri$", "").replaceFirst("_multi$", "");
11✔
388
    }
389

390
    /**
391
     * Serializes an IRI string for SPARQL.
392
     *
393
     * @param iriString The IRI string
394
     * @return The serialized IRI
395
     */
396
    public static String serializeIri(String iriString) {
397
        return "<" + iriString + ">";
3✔
398
    }
399

400
    /**
401
     * Escapes slashes in a string.
402
     *
403
     * @param string The string
404
     * @return The escaped string
405
     */
406
    private static String escapeSlashes(String string) {
407
        return string.replace("\\", "\\\\");
×
408
    }
409

410
    /**
411
     * Serializes a literal string for SPARQL.
412
     *
413
     * @param literalString The literal string
414
     * @return The serialized literal
415
     */
416
    public static String serializeLiteral(String literalString) {
417
        return "\"" + escapeLiteral(literalString) + "\"";
4✔
418
    }
419

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

© 2025 Coveralls, Inc