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

knowledgepixels / nanodash / 22769842282

06 Mar 2026 03:24PM UTC coverage: 15.73% (-0.1%) from 15.877%
22769842282

push

github

web-flow
Merge pull request #381 from knowledgepixels/380-support-construct-queries

feat(QueryPage): support CONSTRUCT queries

705 of 5435 branches covered (12.97%)

Branch coverage included in aggregate %.

1743 of 10128 relevant lines covered (17.21%)

2.35 hits per line

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

59.56
src/main/java/com/knowledgepixels/nanodash/GrlcQuery.java
1
package com.knowledgepixels.nanodash;
2

3
import com.knowledgepixels.nanodash.component.QueryParamField;
4
import net.trustyuri.TrustyUriUtils;
5
import org.eclipse.rdf4j.model.IRI;
6
import org.eclipse.rdf4j.model.Literal;
7
import org.eclipse.rdf4j.model.Statement;
8
import org.eclipse.rdf4j.model.vocabulary.DCTERMS;
9
import org.eclipse.rdf4j.model.vocabulary.RDF;
10
import org.eclipse.rdf4j.model.vocabulary.RDFS;
11
import org.eclipse.rdf4j.query.algebra.Var;
12
import org.eclipse.rdf4j.query.algebra.helpers.AbstractSimpleQueryModelVisitor;
13
import org.eclipse.rdf4j.query.parser.ParsedGraphQuery;
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.extra.services.QueryRef;
18
import org.slf4j.Logger;
19
import org.slf4j.LoggerFactory;
20

21
import java.io.Serializable;
22
import java.util.*;
23

24
/**
25
 * Represents a GRLC query extracted from a nanopublication.
26
 * This class parses the query details, including SPARQL, endpoint, label, description, and placeholders.
27
 */
28
public class GrlcQuery implements Serializable {
29

30
    private static final Logger logger = LoggerFactory.getLogger(GrlcQuery.class);
9✔
31

32
    private static Map<String, GrlcQuery> instanceMap = new HashMap<>();
12✔
33

34
    /**
35
     * Returns a singleton instance of GrlcQuery for the given QueryRef.
36
     *
37
     * @param ref the QueryRef object containing the query name
38
     * @return a GrlcQuery instance
39
     */
40
    public static GrlcQuery get(QueryRef ref) {
41
        return get(ref.getQueryId());
12✔
42
    }
43

44
    /**
45
     * Returns a singleton instance of GrlcQuery for the given query ID.
46
     *
47
     * @param id the unique identifier or URI of the query
48
     * @return a GrlcQuery instance
49
     */
50
    public static GrlcQuery get(String id) {
51
        if (!instanceMap.containsKey(id)) {
12!
52
            try {
53
                GrlcQuery q = new GrlcQuery(id);
15✔
54
                id = q.getQueryId();
9✔
55
                if (instanceMap.containsKey(id)) return instanceMap.get(id);
27✔
56
                instanceMap.put(id, q);
15✔
57
            } catch (Exception ex) {
3✔
58
                logger.error("Could not load query: {}", id, ex);
15✔
59
            }
3✔
60
        }
61
        return instanceMap.get(id);
15✔
62
    }
63

64
    /**
65
     * The IRI for the GRLC query class and properties.
66
     */
67
    public final static IRI GRLC_QUERY_CLASS = Utils.vf.createIRI("https://w3id.org/kpxl/grlc/grlc-query");
12✔
68

69
    /**
70
     * The IRI for the SPARQL property and endpoint property in GRLC queries.
71
     */
72
    public final static IRI GRLC_HAS_SPARQL = Utils.vf.createIRI("https://w3id.org/kpxl/grlc/sparql");
12✔
73

74
    /**
75
     * The IRI for the endpoint property in GRLC queries.
76
     */
77
    public final static IRI GRLC_HAS_ENDPOINT = Utils.vf.createIRI("https://w3id.org/kpxl/grlc/endpoint");
15✔
78

79
    private final String queryId;
80
    private final String artifactCode;
81
    private final String querySuffix;
82
    private final Nanopub nanopub;
83
    private IRI queryUri;
84
    private String sparql;
85
    private IRI endpoint;
86
    private String label;
87
    private String description;
88
    private final List<String> placeholdersList;
89
    private boolean constructQuery;
90

91
    /**
92
     * Constructs a GrlcQuery object by parsing the provided query ID or URI.
93
     *
94
     * @param id The query ID or URI.
95
     * @throws IllegalArgumentException If the ID is null, invalid, or the nanopublication defines multiple queries.
96
     */
97
    private GrlcQuery(String id) {
6✔
98
        if (id == null) {
6✔
99
            throw new IllegalArgumentException("Null value for query ID");
15✔
100
        }
101
        if (TrustyUriUtils.isPotentialTrustyUri(id)) {
9✔
102
            artifactCode = TrustyUriUtils.getArtifactCode(id);
12✔
103
            nanopub = Utils.getNanopub(artifactCode);
15✔
104
            for (Statement st : nanopub.getAssertion()) {
36✔
105
                if (st.getPredicate().equals(RDF.TYPE) && st.getObject().equals(GRLC_QUERY_CLASS)) {
30!
106
                    if (queryUri != null) {
9✔
107
                        throw new IllegalArgumentException("Nanopublication defines more than one query: " + id);
18✔
108
                    }
109
                    queryUri = (IRI) st.getSubject();
15✔
110
                }
111
            }
3✔
112
            if (queryUri == null) {
9✔
113
                throw new IllegalArgumentException("No query found in nanopublication: " + id);
18✔
114
            }
115
            queryId = queryUri.stringValue().replaceFirst("^https?://.*[^A-Za-z0-9-_](RA[A-Za-z0-9-_]{43}[/#][^/#]+)$", "$1").replace("#", "/");
36✔
116
        } else {
117
            if (id.matches("https?://.*[^A-Za-z0-9-_]RA[A-Za-z0-9-_]{43}[/#][^/#]+")) {
12!
118
                queryId = id.replaceFirst("^https?://.*[^A-Za-z0-9-_](RA[A-Za-z0-9-_]{43}[/#][^/#]+)$", "$1").replace("#", "/");
×
119
            } else if (id.matches("RA[A-Za-z0-9-_]{43}[/#][^/#]+")) {
12!
120
                queryId = id;
12✔
121
            } else {
122
                throw new IllegalArgumentException("Not a valid query ID or URI: " + id);
×
123
            }
124
            artifactCode = queryId.replaceFirst("[/#].*$", "");
21✔
125
            nanopub = Utils.getNanopub(artifactCode);
15✔
126
        }
127
        querySuffix = queryId.replaceFirst("^.*[/#]", "");
21✔
128
        for (Statement st : nanopub.getAssertion()) {
36✔
129
            if (!st.getSubject().stringValue().replace("#", "/").endsWith(queryId)) continue;
30!
130
            queryUri = (IRI) st.getSubject();
15✔
131
            if (st.getPredicate().equals(GRLC_HAS_SPARQL) && st.getObject() instanceof Literal objLiteral) {
42!
132
                sparql = objLiteral.stringValue();
15✔
133
            } else if (st.getPredicate().equals(GRLC_HAS_ENDPOINT) && st.getObject() instanceof IRI objIri) {
42!
134
                endpoint = objIri;
12✔
135
            } else if (st.getPredicate().equals(RDFS.LABEL)) {
15✔
136
                label = st.getObject().stringValue();
18✔
137
            } else if (st.getPredicate().equals(DCTERMS.DESCRIPTION)) {
15✔
138
                description = st.getObject().stringValue();
15✔
139
            }
140
        }
3✔
141

142
        final Set<String> placeholders = new HashSet<>();
12✔
143
        ParsedQuery query = new SPARQLParser().parseQuery(sparql, null);
24✔
144
        constructQuery = query instanceof ParsedGraphQuery;
12✔
145
        try {
146
            query.getTupleExpr().visitChildren(new AbstractSimpleQueryModelVisitor<Exception>() {
42✔
147

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

156
            });
157
        } catch (Exception e) {
×
158
            throw new RuntimeException(e);
×
159
        }
3✔
160
        List<String> placeholdersListPre = new ArrayList<>(placeholders);
15✔
161
        Collections.sort(placeholdersListPre);
6✔
162
        placeholdersList = Collections.unmodifiableList(placeholdersListPre);
12✔
163
    }
3✔
164

165
    /**
166
     * Returns the unique query ID.
167
     *
168
     * @return The query ID.
169
     */
170
    public String getQueryId() {
171
        return queryId;
9✔
172
    }
173

174
    /**
175
     * Returns the artifact code extracted from the nanopublication.
176
     *
177
     * @return The artifact code.
178
     */
179
    public String getArtifactCode() {
180
        return artifactCode;
9✔
181
    }
182

183
    /**
184
     * Returns the suffix of the query.
185
     *
186
     * @return The query suffix.
187
     */
188
    public String getQuerySuffix() {
189
        return querySuffix;
9✔
190
    }
191

192
    /**
193
     * Returns the nanopublication containing the query.
194
     *
195
     * @return The nanopublication.
196
     */
197
    public Nanopub getNanopub() {
198
        return nanopub;
9✔
199
    }
200

201
    /**
202
     * Returns the URI of the query.
203
     *
204
     * @return The query URI.
205
     */
206
    public IRI getQueryUri() {
207
        return queryUri;
9✔
208
    }
209

210
    /**
211
     * Returns the SPARQL query string.
212
     *
213
     * @return The SPARQL query.
214
     */
215
    public String getSparql() {
216
        return sparql;
9✔
217
    }
218

219
    /**
220
     * Returns the endpoint URI for the query.
221
     *
222
     * @return The endpoint URI.
223
     */
224
    public IRI getEndpoint() {
225
        return endpoint;
9✔
226
    }
227

228
    /**
229
     * Returns the label of the query.
230
     *
231
     * @return The query label.
232
     */
233
    public String getLabel() {
234
        return label;
9✔
235
    }
236

237
    /**
238
     * Returns the description of the query.
239
     *
240
     * @return The query description.
241
     */
242
    public String getDescription() {
243
        return description;
9✔
244
    }
245

246
    /**
247
     * Returns a list of placeholders in the query.
248
     *
249
     * @return The list of placeholders.
250
     */
251
    public List<String> getPlaceholdersList() {
252
        return placeholdersList;
9✔
253
    }
254

255
    /**
256
     * Returns true if this is a CONSTRUCT query (returns RDF graph data instead of tabular data).
257
     *
258
     * @return true if CONSTRUCT query
259
     */
260
    public boolean isConstructQuery() {
261
        return constructQuery;
×
262
    }
263

264
    /**
265
     * Creates a list of query parameter fields for the placeholders in the query.
266
     *
267
     * @param markupId The markup ID for the fields.
268
     * @return A list of query parameter fields.
269
     */
270
    public List<QueryParamField> createParamFields(String markupId) {
271
        List<QueryParamField> l = new ArrayList<>();
12✔
272
        for (String s : placeholdersList) {
21!
273
            l.add(new QueryParamField(markupId, s));
×
274
        }
×
275
        return l;
6✔
276
    }
277

278
    // NOTE: The following methods are duplicated from nanopub-query's GrlcSpec.java.
279
    // They should eventually be moved to nanopub-java to avoid duplication.
280

281
    /**
282
     * Expands the SPARQL query by substituting placeholder values from the given param fields.
283
     * Unlike the server-side version in nanopub-query, missing mandatory params are simply skipped
284
     * (not thrown as errors) to support partial substitution for the Yasgui link.
285
     *
286
     * @param paramFields the list of query parameter fields with user-entered values
287
     * @return the expanded SPARQL query string
288
     */
289
    public String expandQuery(List<QueryParamField> paramFields) {
290
        Map<String, QueryParamField> fieldMap = new HashMap<>();
×
291
        for (QueryParamField f : paramFields) {
×
292
            fieldMap.put(f.getParamName(), f);
×
293
        }
×
294
        String expandedQueryContent = sparql;
×
295
        for (String ph : placeholdersList) {
×
296
            String paramName = QueryParamField.getParamName(ph);
×
297
            QueryParamField field = fieldMap.get(paramName);
×
298
            if (field == null || !field.isSet()) continue;
×
299
            if (QueryParamField.isMultiPlaceholder(ph)) {
×
300
                String[] values = field.getValues();
×
301
                StringBuilder valueList = new StringBuilder();
×
302
                for (String v : values) {
×
303
                    if (isIriPlaceholder(ph)) {
×
304
                        valueList.append(serializeIri(v)).append(" ");
×
305
                    } else {
306
                        valueList.append(serializeLiteral(v)).append(" ");
×
307
                    }
308
                }
309
                expandedQueryContent = expandedQueryContent.replaceAll(
×
310
                    "values\\s*\\?" + ph + "\\s*\\{\\s*\\}",
311
                    "values ?" + ph + " { " + escapeSlashes(valueList.toString()) + "}"
×
312
                );
313
            } else {
×
314
                String val = field.getValues()[0];
×
315
                if (isIriPlaceholder(ph)) {
×
316
                    expandedQueryContent = expandedQueryContent.replaceAll("\\?" + ph + "(?![A-Za-z0-9_])", escapeSlashes(serializeIri(val)));
×
317
                } else {
318
                    expandedQueryContent = expandedQueryContent.replaceAll("\\?" + ph + "(?![A-Za-z0-9_])", escapeSlashes(serializeLiteral(val)));
×
319
                }
320
            }
321
        }
×
322
        return expandedQueryContent;
×
323
    }
324

325
    /**
326
     * Returns true if all mandatory (non-optional) param fields have values set.
327
     *
328
     * @param paramFields the list of query parameter fields
329
     * @return true if all mandatory fields are set
330
     */
331
    public static boolean allMandatoryFieldsSet(List<QueryParamField> paramFields) {
332
        for (QueryParamField f : paramFields) {
×
333
            if (!f.isOptional() && !f.isSet()) return false;
×
334
        }
×
335
        return true;
×
336
    }
337

338
    private static boolean isIriPlaceholder(String placeholder) {
339
        return placeholder.endsWith("_iri");
×
340
    }
341

342
    private static String escapeLiteral(String s) {
343
        return s.replace("\\", "\\\\").replace("\n", "\\n").replace("\"", "\\\"");
×
344
    }
345

346
    private static String serializeIri(String iriString) {
347
        return "<" + iriString + ">";
×
348
    }
349

350
    private static String serializeLiteral(String literalString) {
351
        return "\"" + escapeLiteral(literalString) + "\"";
×
352
    }
353

354
    private static String escapeSlashes(String string) {
355
        return string.replace("\\", "\\\\");
×
356
    }
357

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