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

knowledgepixels / nanodash / 23133101585

16 Mar 2026 07:45AM UTC coverage: 15.984% (+0.2%) from 15.811%
23133101585

Pull #402

github

web-flow
Merge bd8288c47 into 39c6ac11c
Pull Request #402: Fix unbounded memory growth and resource exhaustion

717 of 5509 branches covered (13.02%)

Branch coverage included in aggregate %.

1809 of 10294 relevant lines covered (17.57%)

2.39 hits per line

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

60.42
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 com.google.common.cache.Cache;
22
import com.google.common.cache.CacheBuilder;
23

24
import java.io.Serializable;
25
import java.util.*;
26
import java.util.concurrent.TimeUnit;
27

28
/**
29
 * Represents a GRLC query extracted from a nanopublication.
30
 * This class parses the query details, including SPARQL, endpoint, label, description, and placeholders.
31
 */
32
public class GrlcQuery implements Serializable {
33

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

36
    private static final Cache<String, GrlcQuery> instanceMap = CacheBuilder.newBuilder()
6✔
37
        .maximumSize(5_000)
9✔
38
        .expireAfterAccess(24, TimeUnit.HOURS)
3✔
39
        .build();
6✔
40

41
    /**
42
     * Returns a singleton instance of GrlcQuery for the given QueryRef.
43
     *
44
     * @param ref the QueryRef object containing the query name
45
     * @return a GrlcQuery instance
46
     */
47
    public static GrlcQuery get(QueryRef ref) {
48
        return get(ref.getQueryId());
12✔
49
    }
50

51
    /**
52
     * Returns a singleton instance of GrlcQuery for the given query ID.
53
     *
54
     * @param id the unique identifier or URI of the query
55
     * @return a GrlcQuery instance
56
     */
57
    public static GrlcQuery get(String id) {
58
        if (id == null) return null;
12✔
59
        GrlcQuery cached = instanceMap.getIfPresent(id);
15✔
60
        if (cached == null) {
6!
61
            try {
62
                GrlcQuery q = new GrlcQuery(id);
15✔
63
                id = q.getQueryId();
9✔
64
                cached = instanceMap.getIfPresent(id);
15✔
65
                if (cached != null) return cached;
12✔
66
                instanceMap.put(id, q);
12✔
67
                cached = q;
6✔
68
            } catch (Exception ex) {
3✔
69
                logger.error("Could not load query: {}", id, ex);
15✔
70
            }
3✔
71
        }
72
        return cached;
6✔
73
    }
74

75
    /**
76
     * The IRI for the GRLC query class and properties.
77
     */
78
    public final static IRI GRLC_QUERY_CLASS = Utils.vf.createIRI("https://w3id.org/kpxl/grlc/grlc-query");
12✔
79

80
    /**
81
     * The IRI for the SPARQL property and endpoint property in GRLC queries.
82
     */
83
    public final static IRI GRLC_HAS_SPARQL = Utils.vf.createIRI("https://w3id.org/kpxl/grlc/sparql");
12✔
84

85
    /**
86
     * The IRI for the endpoint property in GRLC queries.
87
     */
88
    public final static IRI GRLC_HAS_ENDPOINT = Utils.vf.createIRI("https://w3id.org/kpxl/grlc/endpoint");
15✔
89

90
    private final String queryId;
91
    private final String artifactCode;
92
    private final String querySuffix;
93
    private final Nanopub nanopub;
94
    private IRI queryUri;
95
    private String sparql;
96
    private IRI endpoint;
97
    private String label;
98
    private String description;
99
    private final List<String> placeholdersList;
100
    private boolean constructQuery;
101

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

153
        final Set<String> placeholders = new HashSet<>();
12✔
154
        ParsedQuery query = new SPARQLParser().parseQuery(sparql, null);
24✔
155
        constructQuery = query instanceof ParsedGraphQuery;
12✔
156
        try {
157
            query.getTupleExpr().visitChildren(new AbstractSimpleQueryModelVisitor<Exception>() {
42✔
158

159
                @Override
160
                public void meet(Var node) throws Exception {
161
                    super.meet(node);
9✔
162
                    if (!node.isConstant() && !node.isAnonymous() && node.getName().startsWith("_")) {
33!
163
                        placeholders.add(node.getName());
×
164
                    }
165
                }
3✔
166

167
            });
168
        } catch (Exception e) {
×
169
            throw new RuntimeException(e);
×
170
        }
3✔
171
        List<String> placeholdersListPre = new ArrayList<>(placeholders);
15✔
172
        Collections.sort(placeholdersListPre);
6✔
173
        placeholdersList = Collections.unmodifiableList(placeholdersListPre);
12✔
174
    }
3✔
175

176
    /**
177
     * Returns the unique query ID.
178
     *
179
     * @return The query ID.
180
     */
181
    public String getQueryId() {
182
        return queryId;
9✔
183
    }
184

185
    /**
186
     * Returns the artifact code extracted from the nanopublication.
187
     *
188
     * @return The artifact code.
189
     */
190
    public String getArtifactCode() {
191
        return artifactCode;
9✔
192
    }
193

194
    /**
195
     * Returns the suffix of the query.
196
     *
197
     * @return The query suffix.
198
     */
199
    public String getQuerySuffix() {
200
        return querySuffix;
9✔
201
    }
202

203
    /**
204
     * Returns the nanopublication containing the query.
205
     *
206
     * @return The nanopublication.
207
     */
208
    public Nanopub getNanopub() {
209
        return nanopub;
9✔
210
    }
211

212
    /**
213
     * Returns the URI of the query.
214
     *
215
     * @return The query URI.
216
     */
217
    public IRI getQueryUri() {
218
        return queryUri;
9✔
219
    }
220

221
    /**
222
     * Returns the SPARQL query string.
223
     *
224
     * @return The SPARQL query.
225
     */
226
    public String getSparql() {
227
        return sparql;
9✔
228
    }
229

230
    /**
231
     * Returns the endpoint URI for the query.
232
     *
233
     * @return The endpoint URI.
234
     */
235
    public IRI getEndpoint() {
236
        return endpoint;
9✔
237
    }
238

239
    /**
240
     * Returns the label of the query.
241
     *
242
     * @return The query label.
243
     */
244
    public String getLabel() {
245
        return label;
9✔
246
    }
247

248
    /**
249
     * Returns the description of the query.
250
     *
251
     * @return The query description.
252
     */
253
    public String getDescription() {
254
        return description;
9✔
255
    }
256

257
    /**
258
     * Returns a list of placeholders in the query.
259
     *
260
     * @return The list of placeholders.
261
     */
262
    public List<String> getPlaceholdersList() {
263
        return placeholdersList;
9✔
264
    }
265

266
    /**
267
     * Returns true if this is a CONSTRUCT query (returns RDF graph data instead of tabular data).
268
     *
269
     * @return true if CONSTRUCT query
270
     */
271
    public boolean isConstructQuery() {
272
        return constructQuery;
×
273
    }
274

275
    /**
276
     * Creates a list of query parameter fields for the placeholders in the query.
277
     *
278
     * @param markupId The markup ID for the fields.
279
     * @return A list of query parameter fields.
280
     */
281
    public List<QueryParamField> createParamFields(String markupId) {
282
        List<QueryParamField> l = new ArrayList<>();
12✔
283
        for (String s : placeholdersList) {
21!
284
            l.add(new QueryParamField(markupId, s));
×
285
        }
×
286
        return l;
6✔
287
    }
288

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

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

336
    /**
337
     * Returns true if all mandatory (non-optional) param fields have values set.
338
     *
339
     * @param paramFields the list of query parameter fields
340
     * @return true if all mandatory fields are set
341
     */
342
    public static boolean allMandatoryFieldsSet(List<QueryParamField> paramFields) {
343
        for (QueryParamField f : paramFields) {
×
344
            if (!f.isOptional() && !f.isSet()) return false;
×
345
        }
×
346
        return true;
×
347
    }
348

349
    private static boolean isIriPlaceholder(String placeholder) {
350
        return placeholder.endsWith("_iri");
×
351
    }
352

353
    private static String escapeLiteral(String s) {
354
        return s.replace("\\", "\\\\").replace("\n", "\\n").replace("\"", "\\\"");
×
355
    }
356

357
    private static String serializeIri(String iriString) {
358
        return "<" + iriString + ">";
×
359
    }
360

361
    private static String serializeLiteral(String literalString) {
362
        return "\"" + escapeLiteral(literalString) + "\"";
×
363
    }
364

365
    private static String escapeSlashes(String string) {
366
        return string.replace("\\", "\\\\");
×
367
    }
368

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