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

knowledgepixels / nanopub-query / 19065716017

04 Nov 2025 10:31AM UTC coverage: 71.131% (+0.8%) from 70.317%
19065716017

push

github

ashleycaselli
test(GrlcSpec): add unit tests for placeholder and serialization methods

210 of 320 branches covered (65.63%)

Branch coverage included in aggregate %.

576 of 785 relevant lines covered (73.38%)

3.69 hits per line

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

64.04
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 MultiMap parameters;
68
    private Nanopub np;
69
    private String requestUrlBase;
70
    private String artifactCode;
71
    private String queryPart;
72
    private String queryName;
73
    private String label;
74
    private String desc;
75
    private String license;
76
    private String queryContent;
77
    private String endpoint;
78
    private List<String> placeholdersList;
79

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

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

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

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

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

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

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

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

209
    /**
210
     * Returns the request parameters.
211
     *
212
     * @return the request parameters
213
     */
214
    public MultiMap getParameters() {
215
        return parameters;
×
216
    }
217

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

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

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

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

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

263
    public List<String> getPlaceholdersList() {
264
        return placeholdersList;
3✔
265
    }
266

267
    public String getRepoName() {
268
        return endpoint.replaceAll("/", "_").replaceFirst("^.*_repo_", "");
×
269
    }
270

271
    public String getQueryContent() {
272
        return queryContent;
×
273
    }
274

275
    public String expandQuery() throws InvalidGrlcSpecException {
276
        String expandedQueryContent = queryContent;
×
277
        for (String ph : placeholdersList) {
×
278
            log.info("ph: {}", ph);
×
279
            log.info("getParamName(ph): {}", getParamName(ph));
×
280
            if (isMultiPlaceholder(ph)) {
×
281
                // TODO multi placeholders need proper documentation
282
                List<String> val = parameters.getAll(getParamName(ph));
×
283
                if (!isOptionalPlaceholder(ph) && val.isEmpty()) {
×
284
                    throw new InvalidGrlcSpecException("Missing value for non-optional placeholder: " + ph);
×
285
                }
286
                if (val.isEmpty()) {
×
287
                    expandedQueryContent = expandedQueryContent.replaceAll("values\\s*\\?" + ph + "\\s*\\{\\s*\\}(\\s*\\.)?", "");
×
288
                    continue;
×
289
                }
290
                String valueList = "";
×
291
                for (String v : val) {
×
292
                    if (isIriPlaceholder(ph)) {
×
293
                        valueList += serializeIri(v) + " ";
×
294
                    } else {
295
                        valueList += serializeLiteral(v) + " ";
×
296
                    }
297
                }
×
298
                expandedQueryContent = expandedQueryContent.replaceAll("values\\s*\\?" + ph + "\\s*\\{\\s*\\}", "values ?" + ph + " { " + escapeSlashes(valueList) + "}");
×
299
            } else {
×
300
                String val = parameters.get(getParamName(ph));
×
301
                log.info("val: {}", val);
×
302
                if (!isOptionalPlaceholder(ph) && val == null) {
×
303
                    throw new InvalidGrlcSpecException("Missing value for non-optional placeholder: " + ph);
×
304
                }
305
                if (val == null) continue;
×
306
                if (isIriPlaceholder(ph)) {
×
307
                    expandedQueryContent = expandedQueryContent.replaceAll("\\?" + ph, escapeSlashes(serializeIri(val)));
×
308
                } else {
309
                    expandedQueryContent = expandedQueryContent.replaceAll("\\?" + ph, escapeSlashes(serializeLiteral(val)));
×
310
                }
311
            }
312
        }
×
313
        log.info("Expanded grlc query:\n {}", expandedQueryContent);
×
314
        return expandedQueryContent;
×
315
    }
316

317
    /**
318
     * Escapes a literal string for SPARQL.
319
     *
320
     * @param s The string
321
     * @return The escaped string
322
     */
323
    public static String escapeLiteral(String s) {
324
        return s.replace("\\", "\\\\").replace("\n", "\\n").replace("\"", "\\\"");
11✔
325
    }
326

327
    /**
328
     * Checks whether the given placeholder is an optional placeholder.
329
     *
330
     * @param placeholder The placeholder name
331
     * @return true if it is an optional placeholder, false otherwise
332
     */
333
    public static boolean isOptionalPlaceholder(String placeholder) {
334
        return placeholder.startsWith("__");
4✔
335
    }
336

337
    /**
338
     * Checks whether the given placeholder is a multi-value placeholder.
339
     *
340
     * @param placeholder The placeholder name
341
     * @return true if it is a multi-value placeholder, false otherwise
342
     */
343
    public static boolean isMultiPlaceholder(String placeholder) {
344
        return placeholder.endsWith("_multi") || placeholder.endsWith("_multi_iri");
12✔
345
    }
346

347
    /**
348
     * Checks whether the given placeholder is an IRI placeholder.
349
     *
350
     * @param placeholder The placeholder name
351
     * @return true if it is an IRI placeholder, false otherwise
352
     */
353
    public static boolean isIriPlaceholder(String placeholder) {
354
        return placeholder.endsWith("_iri");
4✔
355
    }
356

357
    /**
358
     * Returns the parameter name for the given placeholder.
359
     *
360
     * @param placeholder The placeholder name
361
     * @return The parameter name
362
     */
363
    public static String getParamName(String placeholder) {
364
        return placeholder.replaceFirst("^_+", "").replaceFirst("_iri$", "").replaceFirst("_multi$", "");
×
365
    }
366

367
    /**
368
     * Serializes an IRI string for SPARQL.
369
     *
370
     * @param iriString The IRI string
371
     * @return The serialized IRI
372
     */
373
    public static String serializeIri(String iriString) {
374
        return "<" + iriString + ">";
3✔
375
    }
376

377
    /**
378
     * Escapes slashes in a string.
379
     *
380
     * @param string The string
381
     * @return The escaped string
382
     */
383
    private static String escapeSlashes(String string) {
384
        return string.replace("\\", "\\\\");
×
385
    }
386

387
    /**
388
     * Serializes a literal string for SPARQL.
389
     *
390
     * @param literalString The literal string
391
     * @return The serialized literal
392
     */
393
    public static String serializeLiteral(String literalString) {
394
        return "\"" + escapeLiteral(literalString) + "\"";
4✔
395
    }
396

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