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

knowledgepixels / nanopub-query / 17795838363

17 Sep 2025 11:17AM UTC coverage: 70.444% (-1.0%) from 71.466%
17795838363

push

github

tkuhn
feat: Cover multi placeholders in OpenAPI spec

206 of 318 branches covered (64.78%)

Branch coverage included in aggregate %.

571 of 785 relevant lines covered (72.74%)

3.65 hits per line

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

60.35
src/main/java/com/knowledgepixels/query/GrlcSpec.java
1
package com.knowledgepixels.query;
2

3
import java.util.ArrayList;
4
import java.util.Collections;
5
import java.util.Comparator;
6
import java.util.HashSet;
7
import java.util.List;
8
import java.util.Set;
9

10
import org.eclipse.rdf4j.model.IRI;
11
import org.eclipse.rdf4j.model.Statement;
12
import org.eclipse.rdf4j.model.ValueFactory;
13
import org.eclipse.rdf4j.model.impl.SimpleValueFactory;
14
import org.eclipse.rdf4j.model.vocabulary.DCTERMS;
15
import org.eclipse.rdf4j.model.vocabulary.RDFS;
16
import org.eclipse.rdf4j.query.MalformedQueryException;
17
import org.eclipse.rdf4j.query.algebra.Var;
18
import org.eclipse.rdf4j.query.algebra.helpers.AbstractSimpleQueryModelVisitor;
19
import org.eclipse.rdf4j.query.parser.ParsedQuery;
20
import org.eclipse.rdf4j.query.parser.sparql.SPARQLParser;
21
import org.nanopub.Nanopub;
22
import org.nanopub.SimpleCreatorPattern;
23
import org.nanopub.extra.server.GetNanopub;
24
import org.nanopub.extra.services.QueryAccess;
25

26
import io.vertx.core.MultiMap;
27
import net.trustyuri.TrustyUriUtils;
28
import org.slf4j.Logger;
29
import org.slf4j.LoggerFactory;
30

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

37
    private static final ValueFactory vf = SimpleValueFactory.getInstance();
2✔
38

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

41
    public static class InvalidGrlcSpecException extends Exception {
42

43
        private InvalidGrlcSpecException(String msg) {
44
            super(msg);
3✔
45
        }
1✔
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");
4✔
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");
4✔
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/");
5✔
67

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

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

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

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

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

136
        final Set<String> placeholders = new HashSet<>();
4✔
137
        try {
138
            ParsedQuery query = new SPARQLParser().parseQuery(queryContent, null);
8✔
139
            query.getTupleExpr().visitChildren(new AbstractSimpleQueryModelVisitor<>() {
14✔
140
        
141
                @Override
142
                public void meet(Var node) throws RuntimeException {
143
                    super.meet(node);
3✔
144
                    if (!node.isConstant() && !node.isAnonymous() && node.getName().startsWith("_")) {
11!
145
                        placeholders.add(node.getName());
×
146
                    }
147
                }
1✔
148
        
149
            });
150
        } catch (MalformedQueryException ex) {
×
151
            throw new InvalidGrlcSpecException("Invalid SPARQL string", ex);
×
152
        }
1✔
153
        List<String> placeholdersListPre = new ArrayList<>(placeholders);
5✔
154
        Collections.sort(placeholdersListPre);
2✔
155
        placeholdersListPre.sort(Comparator.comparing(String::length));
4✔
156
        placeholdersList = Collections.unmodifiableList(placeholdersListPre);
4✔
157
    }
1✔
158

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

210
    public MultiMap getParameters() {
211
        return parameters;
×
212
    }
213

214
    public Nanopub getNanopub() {
215
        return np;
3✔
216
    }
217

218
    public String getArtifactCode() {
219
        return artifactCode;
3✔
220
    }
221

222
    public String getLabel() {
223
        return label;
3✔
224
    }
225

226
    public String getDescription() {
227
        return desc;
3✔
228
    }
229

230
    public String getQueryName() {
231
        return queryName;
3✔
232
    }
233

234
    public List<String> getPlaceholdersList() {
235
        return placeholdersList;
3✔
236
    }
237

238
    public String getRepoName() {
239
        return endpoint.replaceAll("/", "_").replaceFirst("^.*_repo_", "");
×
240
    }
241

242
    public String getQueryContent() {
243
        return queryContent;
×
244
    }
245

246
    public String expandQuery() throws InvalidGrlcSpecException {
247
        String expandedQueryContent = queryContent;
×
248
        for (String ph : placeholdersList) {
×
249
            log.info("ph: ", ph);
×
250
            log.info("getParamName(ph): ", getParamName(ph));
×
251
            if (isMultiPlaceholder(ph)) {
×
252
                // TODO multi placeholders need proper documentation
253
                List<String> val = parameters.getAll(getParamName(ph));
×
254
                if (!isOptionalPlaceholder(ph) && val.isEmpty()) {
×
255
                    throw new InvalidGrlcSpecException("Missing value for non-optional placeholder: " + ph);
×
256
                }
257
                if (val.isEmpty()) {
×
258
                    expandedQueryContent = expandedQueryContent.replaceAll("[]values\\s*\\?" + ph + "\\s*\\{\\s*\\}(\\s*\\.)?", "");
×
259
                    continue;
×
260
                }
261
                String valueList = "";
×
262
                for (String v : val) {
×
263
                    if (isIriPlaceholder(ph)) {
×
264
                        valueList += serializeIri(v) + " ";
×
265
                    } else {
266
                        valueList += serializeLiteral(v) + " ";
×
267
                    }
268
                }
×
269
                expandedQueryContent = expandedQueryContent.replaceAll("values\\s*\\?" + ph + "\\s*\\{\\s*\\}", "values ?" + ph + " { " + valueList + "}");
×
270
            } else {
×
271
                String val = parameters.get(getParamName(ph));
×
272
                log.info("val: ", val);
×
273
                if (!isOptionalPlaceholder(ph) && val == null) {
×
274
                    throw new InvalidGrlcSpecException("Missing value for non-optional placeholder: " + ph);
×
275
                }
276
                if (val == null) continue;
×
277
                if (isIriPlaceholder(ph)) {
×
278
                    expandedQueryContent = expandedQueryContent.replaceAll("\\?" + ph, serializeIri(val));
×
279
                } else {
280
                    expandedQueryContent = expandedQueryContent.replaceAll("\\?" + ph, serializeLiteral(val));
×
281
                }
282
            }
283
        }
×
284
        log.info("Expanded grlc query:\n", expandedQueryContent);
×
285
        return expandedQueryContent;
×
286
    }
287

288
    public static String escapeLiteral(String s) {
289
        return s.replace("\\", "\\\\").replace("\n", "\\n").replace("\"", "\\\"");
11✔
290
    }
291

292
    public static boolean isOptionalPlaceholder(String placeholder) {
293
        return placeholder.startsWith("__");
×
294
    }
295

296
    public static boolean isMultiPlaceholder(String placeholder) {
297
        return placeholder.endsWith("_multi") || placeholder.endsWith("_multi_iri");
×
298
    }
299

300
    public static boolean isIriPlaceholder(String placeholder) {
301
        return placeholder.endsWith("_iri");
×
302
    }
303

304
    public static String getParamName(String placeholder) {
305
        return placeholder.replaceFirst("^_+", "").replaceFirst("_iri$", "").replaceFirst("_multi$", "");
×
306
    }
307

308
    public static String serializeIri(String iriString) {
309
        return "<" + iriString + ">";
×
310
    }
311

312
    public static String serializeLiteral(String literalString) {
313
        return "\"" + escapeLiteral(literalString) + "\"";
×
314
    }
315

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