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

knowledgepixels / nanopub-query / 17121899603

21 Aug 2025 08:33AM UTC coverage: 68.694% (+18.7%) from 49.971%
17121899603

push

github

tkuhn
Add GeneratedFlagForDependentElements annotations

234 of 360 branches covered (65.0%)

Branch coverage included in aggregate %.

613 of 873 relevant lines covered (70.22%)

3.5 hits per line

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

0.0
src/main/java/com/knowledgepixels/query/MainVerticle.java
1
package com.knowledgepixels.query;
2

3
import java.io.InputStream;
4
import java.net.URLEncoder;
5
import java.util.ArrayList;
6
import java.util.Collections;
7
import java.util.List;
8
import java.util.Map.Entry;
9
import java.util.Scanner;
10
import java.util.concurrent.Executors;
11
import java.util.concurrent.TimeUnit;
12

13
import org.eclipse.rdf4j.model.Value;
14

15
import com.github.jsonldjava.shaded.com.google.common.base.Charsets;
16

17
import io.micrometer.prometheus.PrometheusMeterRegistry;
18
import io.vertx.core.AbstractVerticle;
19
import io.vertx.core.Future;
20
import io.vertx.core.MultiMap;
21
import io.vertx.core.Promise;
22
import io.vertx.core.http.HttpClient;
23
import io.vertx.core.http.HttpClientOptions;
24
import io.vertx.core.http.HttpMethod;
25
import io.vertx.core.http.HttpServer;
26
import io.vertx.core.http.HttpServerResponse;
27
import io.vertx.core.http.PoolOptions;
28
import io.vertx.ext.web.Router;
29
import io.vertx.ext.web.RoutingContext;
30
import io.vertx.ext.web.handler.CorsHandler;
31
import io.vertx.ext.web.handler.StaticHandler;
32
import io.vertx.ext.web.proxy.handler.ProxyHandler;
33
import io.vertx.httpproxy.HttpProxy;
34
import io.vertx.httpproxy.ProxyContext;
35
import io.vertx.httpproxy.ProxyInterceptor;
36
import io.vertx.httpproxy.ProxyRequest;
37
import io.vertx.httpproxy.ProxyResponse;
38
import io.vertx.micrometer.PrometheusScrapingHandler;
39
import io.vertx.micrometer.backends.BackendRegistries;
40

41
/**
42
 * Main verticle that coordinates the incoming HTTP requests.
43
 */
44
@GeneratedFlagForDependentElements
45
public class MainVerticle extends AbstractVerticle {
46

47
    private static String css = null;
48

49
    /**
50
     * Start the main verticle.
51
     *
52
     * @param startPromise the promise to complete when the verticle is started
53
     * @throws Exception if an error occurs during startup
54
     */
55
    @Override
56
    public void start(Promise<Void> startPromise) throws Exception {
57
        HttpClient httpClient = vertx.createHttpClient(
58
                new HttpClientOptions()
59
                        .setConnectTimeout(Utils.getEnvInt("NANOPUB_QUERY_VERTX_CONNECT_TIMEOUT", 1000))
60
                        .setIdleTimeoutUnit(TimeUnit.SECONDS)
61
                        .setIdleTimeout(Utils.getEnvInt("NANOPUB_QUERY_VERTX_IDLE_TIMEOUT", 60))
62
                        .setReadIdleTimeout(Utils.getEnvInt("NANOPUB_QUERY_VERTX_IDLE_TIMEOUT", 60))
63
                        .setWriteIdleTimeout(Utils.getEnvInt("NANOPUB_QUERY_VERTX_IDLE_TIMEOUT", 60)),
64
                new PoolOptions().setHttp1MaxSize(200).setHttp2MaxSize(200)
65
        );
66

67
        HttpServer proxyServer = vertx.createHttpServer();
68
        Router proxyRouter = Router.router(vertx);
69
        proxyRouter.route().handler(CorsHandler.create().addRelativeOrigin(".*"));
70

71
        // Metrics
72
        final var metricsHttpServer = vertx.createHttpServer();
73
        final var metricsRouter = Router.router(vertx);
74
        metricsHttpServer.requestHandler(metricsRouter).listen(9394);
75

76
        final var metricsRegistry = (PrometheusMeterRegistry) BackendRegistries.getDefaultNow();
77
        final var collector = new MetricsCollector(metricsRegistry);
78
        metricsRouter.route("/metrics").handler(PrometheusScrapingHandler.create(metricsRegistry));
79
        // ----------
80
        // This part is only used if the redirection is not done through Nginx.
81
        // See nginx.conf and this bug report: https://github.com/eclipse-rdf4j/rdf4j/discussions/5120
82
        HttpProxy rdf4jProxy = HttpProxy.reverseProxy(httpClient);
83
        String proxy = Utils.getEnvString("RDF4J_PROXY_HOST", "rdf4j");
84
        int proxyPort = Utils.getEnvInt("RDF4J_PROXY_PORT", 8080);
85
        rdf4jProxy.origin(proxyPort, proxy);
86

87
        rdf4jProxy.addInterceptor(new ProxyInterceptor() {
×
88

89
            @Override
90
            public Future<ProxyResponse> handleProxyRequest(ProxyContext context) {
91
                ProxyRequest request = context.request();
×
92
                request.setURI(request.getURI().replaceAll("/", "_").replaceFirst("^_repo_", "/rdf4j-server/repositories/"));
×
93
                // For later to try to get HTML tables out:
94
//                                if (request.headers().get("Accept") == null) {
95
//                                        request.putHeader("Accept", "text/html");
96
//                                }
97
//                                request.putHeader("Accept", "application/json");
98
                return ProxyInterceptor.super.handleProxyRequest(context);
×
99
            }
100

101
            @Override
102
            public Future<Void> handleProxyResponse(ProxyContext context) {
103
                ProxyResponse resp = context.response();
×
104
                resp.putHeader("Access-Control-Allow-Origin", "*");
×
105
                resp.putHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS");
×
106
                // For later to try to get HTML tables out:
107
//                                String acceptHeader = context.request().headers().get("Accept");
108
//                                if (acceptHeader != null && acceptHeader.contains("text/html")) {
109
//                                        resp.putHeader("Content-Type", "text/html");
110
//                                        resp.setBody(Body.body(Buffer.buffer("<html><body><strong>test</strong></body></html>")));
111
//                                }
112
                return ProxyInterceptor.super.handleProxyResponse(context);
×
113
            }
114

115
        });
116
        // ----------
117

118
        proxyRouter.route(HttpMethod.GET, "/repo").handler(req -> handleRedirect(req, "/repo"));
119
        proxyRouter.route(HttpMethod.GET, "/repo/*").handler(ProxyHandler.create(rdf4jProxy));
120
        proxyRouter.route(HttpMethod.POST, "/repo/*").handler(ProxyHandler.create(rdf4jProxy));
121
        proxyRouter.route(HttpMethod.HEAD, "/repo/*").handler(ProxyHandler.create(rdf4jProxy));
122
        proxyRouter.route(HttpMethod.OPTIONS, "/repo/*").handler(ProxyHandler.create(rdf4jProxy));
123
        proxyRouter.route(HttpMethod.GET, "/tools/*").handler(req -> {
124
            final String yasguiPattern = "^/tools/([a-zA-Z0-9-_]+)(/([a-zA-Z0-9-_]+))?/yasgui\\.html$";
125
            if (req.normalizedPath().matches(yasguiPattern)) {
126
                String repo = req.normalizedPath().replaceFirst(yasguiPattern, "$1$2");
127
                req.response()
128
                        .putHeader("content-type", "text/html")
129
                        .end("<!DOCTYPE html>\n"
130
                                + "<html lang=\"en\">\n"
131
                                + "<head>\n"
132
                                + "<meta charset=\"utf-8\">\n"
133
                                + "<meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">\n"
134
                                + "<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n"
135
                                + "<title>Nanopub Query SPARQL Editor for repository: " + repo + "</title>\n"
136
                                + "<link rel=\"stylesheet\" href=\"/style.css\">\n"
137
                                + "<link href='https://cdn.jsdelivr.net/yasgui/2.6.1/yasgui.min.css' rel='stylesheet' type='text/css'/>\n"
138
                                + "<style>.yasgui .endpointText {display:none !important;}</style>\n"
139
                                + "<script type=\"text/javascript\">localStorage.clear();</script>\n"
140
                                + "</head>\n"
141
                                + "<body>\n"
142
                                + "<h3>Nanopub Query SPARQL Editor for repository: " + repo + "</h3>\n"
143
                                + "<div id='yasgui'></div>\n"
144
                                + "<script src='https://cdn.jsdelivr.net/yasgui/2.6.1/yasgui.min.js'></script>\n"
145
                                + "<script type=\"text/javascript\">\n"
146
                                + "var yasgui = YASGUI(document.getElementById(\"yasgui\"), {\n"
147
                                + "  yasqe:{sparql:{endpoint:'/repo/" + repo + "'},value:'" + Utils.defaultQuery.replaceAll("\n", "\\\\n") + "'}\n"
148
                                + "});\n"
149
                                + "</script>\n"
150
                                + "</body>\n"
151
                                + "</html>");
152
            } else {
153
                req.response()
154
                        .putHeader("content-type", "text/plain")
155
                        .setStatusCode(404)
156
                        .end("not found");
157
            }
158
        });
159
        proxyRouter.route(HttpMethod.GET, "/page").handler(req -> handleRedirect(req, "/page"));
160
        proxyRouter.route(HttpMethod.GET, "/page/*").handler(req -> {
161
            final String pagePattern = "^/page/([a-zA-Z0-9-_]+)(/([a-zA-Z0-9-_]+))?$";
162
            if (req.normalizedPath().matches(pagePattern)) {
163
                String repo = req.normalizedPath().replaceFirst(pagePattern, "$1$2");
164
                req.response()
165
                        .putHeader("content-type", "text/html")
166
                        .end("<!DOCTYPE html>\n"
167
                                + "<html lang=\"en\">\n"
168
                                + "<head>\n"
169
                                + "<meta charset=\"utf-8\">\n"
170
                                + "<meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">\n"
171
                                + "<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n"
172
                                + "<title>Nanopub Query repo: " + repo + "</title>\n"
173
                                + "<link rel=\"stylesheet\" href=\"/style.css\">\n"
174
                                + "</head>\n"
175
                                + "<body>\n"
176
                                + "<h3>Nanopub Query repo: " + repo + "</h3>\n"
177
                                + "<p>Endpoint: <a href=\"/repo/" + repo + "\">/repo/" + repo + "</a></p>"
178
                                + "<p>YASGUI: <a href=\"/tools/" + repo + "/yasgui.html\">/tools/" + repo + "/yasgui.hml</a></p>"
179
                                + "</body>\n"
180
                                + "</html>");
181
            } else {
182
                req.response()
183
                        .putHeader("content-type", "text/plain")
184
                        .setStatusCode(404)
185
                        .end("not found");
186
            }
187
        });
188
        proxyRouter.route(HttpMethod.GET, "/").handler(req -> {
189
            String repos = "";
190
            List<String> repoList = new ArrayList<>(TripleStore.get().getRepositoryNames());
191
            Collections.sort(repoList);
192
            for (String s : repoList) {
193
                if (s.startsWith("pubkey_") || s.startsWith("type_")) continue;
194
                repos += "<li><code><a href=\"/page/" + s + "\">" + s + "</a></code></li>";
195
            }
196
            String pinnedApisValue = Utils.getEnvString("NANOPUB_QUERY_PINNED_APIS", "");
197
            String[] pinnedApis = pinnedApisValue.split(" ");
198
            String pinnedApiLinks = "";
199
            if (!pinnedApisValue.isEmpty()) {
200
                for (String s : pinnedApis) {
201
                    pinnedApiLinks = pinnedApiLinks + "<li><a href=\"openapi/?url=spec/" + s + "%3Fapi-version=latest\">" + s.replaceFirst("^.*/", "") + "</a></li>";
202
                }
203
                pinnedApiLinks = "<p>Pinned APIs:</p>\n" +
204
                        "<ul>\n" +
205
                        pinnedApiLinks +
206
                        "</ul>\n";
207
            }
208
            req.response()
209
                    .putHeader("content-type", "text/html")
210
                    .end("<!DOCTYPE html>\n"
211
                            + "<html lang='en'>\n"
212
                            + "<head>\n"
213
                            + "<title>Nanopub Query</title>\n"
214
                            + "<meta charset='utf-8'>\n"
215
                            + "<link rel=\"stylesheet\" href=\"/style.css\">\n"
216
                            + "</head>\n"
217
                            + "<body>\n"
218
                            + "<h1>Nanopub Query</h1>"
219
                            + "<p>General repos:</p>"
220
                            + "<ul>" + repos + "</ul>"
221
                            + "<p>Specific repos:</p>"
222
                            + "<ul>"
223
                            + "<li><a href=\"/pubkeys\">Pubkey Repos</a></li>"
224
                            + "<li><a href=\"/types\">Type Repos</a></li>"
225
                            + "</ul>"
226
                            + pinnedApiLinks
227
                            + "</body>\n"
228
                            + "</html>");
229
        });
230
        proxyRouter.route(HttpMethod.GET, "/pubkeys").handler(req -> {
231
            String repos = "";
232
            List<String> repoList = new ArrayList<>(TripleStore.get().getRepositoryNames());
233
            Collections.sort(repoList);
234
            for (String s : repoList) {
235
                if (!s.startsWith("pubkey_")) continue;
236
                String hash = s.replaceFirst("^([a-zA-Z0-9-]+)_([a-zA-Z0-9-_]+)$", "$2");
237
                Value hashObj = Utils.getObjectForHash(hash);
238
                String label;
239
                if (hashObj == null) {
240
                    label = "";
241
                } else {
242
                    label = " (" + Utils.getShortPubkeyName(hashObj.stringValue()) + ")";
243
                }
244
                s = s.replaceFirst("^([a-zA-Z0-9-]+)_([a-zA-Z0-9-_]+)$", "$1/$2");
245
                repos += "<li><code><a href=\"/page/" + s + "\">" + s + "</a>" + label + "</code></li>";
246
            }
247
            req.response()
248
                    .putHeader("content-type", "text/html")
249
                    .end("<!DOCTYPE html>\n"
250
                            + "<html lang='en'>\n"
251
                            + "<head>\n"
252
                            + "<title>Nanopub Query: Pubkey Repos</title>\n"
253
                            + "<meta charset='utf-8'>\n"
254
                            + "<link rel=\"stylesheet\" href=\"/style.css\">\n"
255
                            + "</head>\n"
256
                            + "<body>\n"
257
                            + "<h3>Pubkey Repos</h3>"
258
                            + "<p>Repos:</p>"
259
                            + "<ul>" + repos + "</ul>"
260
                            + "</body>\n"
261
                            + "</html>");
262
        });
263
        proxyRouter.route(HttpMethod.GET, "/types").handler(req -> {
264
            String repos = "";
265
            List<String> repoList = new ArrayList<>(TripleStore.get().getRepositoryNames());
266
            Collections.sort(repoList);
267
            for (String s : repoList) {
268
                if (!s.startsWith("type_")) continue;
269
                String hash = s.replaceFirst("^([a-zA-Z0-9-]+)_([a-zA-Z0-9-_]+)$", "$2");
270
                Value hashObj = Utils.getObjectForHash(hash);
271
                String label;
272
                if (hashObj == null) {
273
                    label = "";
274
                } else {
275
                    label = " (" + hashObj.stringValue() + ")";
276
                }
277
                s = s.replaceFirst("^([a-zA-Z0-9-]+)_([a-zA-Z0-9-_]+)$", "$1/$2");
278
                repos += "<li><code><a href=\"/page/" + s + "\">" + s + "</a>" + label + "</code></li>";
279
            }
280
            req.response()
281
                    .putHeader("content-type", "text/html")
282
                    .end("<!DOCTYPE html>\n"
283
                            + "<html lang='en'>\n"
284
                            + "<head>\n"
285
                            + "<title>Nanopub Query: Type Repos</title>\n"
286
                            + "<meta charset='utf-8'>\n"
287
                            + "<link rel=\"stylesheet\" href=\"/style.css\">\n"
288
                            + "</head>\n"
289
                            + "<body>\n"
290
                            + "<h3>Type Repos</h3>"
291
                            + "<p>Repos:</p>"
292
                            + "<ul>" + repos + "</ul>"
293
                            + "</body>\n"
294
                            + "</html>");
295
        });
296
        proxyRouter.route(HttpMethod.GET, "/style.css").handler(req -> {
297
            if (css == null) {
298
                css = getResourceAsString("style.css");
299
            }
300
            req.response().end(css);
301
        });
302

303
        proxyRouter.route(HttpMethod.GET, "/grlc-spec/*").handler(req -> {
304
            GrlcSpecPage gsp = new GrlcSpecPage(req.normalizedPath(), req.queryParams());
305
            String spec = gsp.getSpec();
306
            if (spec == null) {
307
                req.response().setStatusCode(404).end("query definition not found / not valid");
308
            } else {
309
                req.response().putHeader("content-type", "text/yaml").end(spec);
310
            }
311
        });
312

313
        proxyRouter.route(HttpMethod.GET, "/openapi/spec/*").handler(req -> {
314
            OpenApiSpecPage osp = new OpenApiSpecPage(req.normalizedPath(), req.queryParams());
315
            String spec = osp.getSpec();
316
            if (spec == null) {
317
                req.response().setStatusCode(404).end("query definition not found / not valid");
318
            } else {
319
                req.response().putHeader("content-type", "text/yaml").end(spec);
320
            }
321
        });
322

323
        proxyRouter.route("/openapi/*").handler(StaticHandler.create("com/knowledgepixels/query/swagger"));
324

325
        HttpProxy grlcProxy = HttpProxy.reverseProxy(httpClient);
326
        grlcProxy.origin(80, "grlc");
327
        grlcProxy.addInterceptor(new ProxyInterceptor() {
×
328

329
            @Override
330
            public Future<ProxyResponse> handleProxyRequest(ProxyContext context) {
331
                final String apiPattern = "^/api/(RA[a-zA-Z0-9-_]{43})/([a-zA-Z0-9-_]+)([?].*)?$";
×
332
                if (context.request().getURI().matches(apiPattern)) {
×
333
                    String artifactCode = context.request().getURI().replaceFirst(apiPattern, "$1");
×
334
                    String queryName = context.request().getURI().replaceFirst(apiPattern, "$2");
×
335
                    String grlcUrlParams = "";
×
336
                    String grlcSpecUrlParams = "";
×
337
                    MultiMap pm = context.request().proxiedRequest().params();
×
338
                    for (Entry<String, String> e : pm) {
×
339
                        if (e.getKey().equals("api-version")) {
×
340
                            grlcSpecUrlParams += "&" + e.getKey() + "=" + URLEncoder.encode(e.getValue(), Charsets.UTF_8);
×
341
                        } else {
342
                            grlcUrlParams += "&" + e.getKey() + "=" + URLEncoder.encode(e.getValue(), Charsets.UTF_8);
×
343
                        }
344
                    }
×
345
                    String url = "/api-url/" + queryName +
×
346
                            "?specUrl=" + URLEncoder.encode(GrlcSpecPage.nanopubQueryUrl + "grlc-spec/" + artifactCode + "/?" +
×
347
                            grlcSpecUrlParams, Charsets.UTF_8) + grlcUrlParams;
348
                    context.request().setURI(url);
×
349
                }
350
                return context.sendRequest();
×
351
            }
352

353
            @Override
354
            public Future<Void> handleProxyResponse(ProxyContext context) {
355
                // To avoid double entries:
356
                context.response().headers().remove("Access-Control-Allow-Origin");
×
357
                return context.sendResponse();
×
358
            }
359

360
        });
361

362
        proxyServer.requestHandler(req -> {
363
            applyGlobalHeaders(req.response());
364
            proxyRouter.handle(req);
365
        });
366
        proxyServer.listen(9393);
367

368
        proxyRouter.route("/api/*").handler(ProxyHandler.create(grlcProxy));
369
        proxyRouter.route("/static/*").handler(ProxyHandler.create(grlcProxy));
370

371
        // Periodic metrics update
372
        vertx.setPeriodic(1000, id -> collector.updateMetrics());
373

374

375
        new Thread(() -> {
376
            try {
377
                var status = StatusController.get().initialize();
378
                System.err.println("Current state: " + status.state + ", last committed counter: " + status.loadCounter);
379
                if (status.state == StatusController.State.LAUNCHING || status.state == StatusController.State.LOADING_INITIAL) {
380
                    // Do the initial nanopublication loading
381
                    StatusController.get().setLoadingInitial(status.loadCounter);
382
                    // Fall back to local nanopub loading if the local files are present
383
                    if (!LocalNanopubLoader.init()) {
384
                        JellyNanopubLoader.loadInitial(status.loadCounter);
385
                    } else {
386
                        System.err.println("Local nanopublication loading finished");
387
                    }
388
                    StatusController.get().setReady();
389
                } else {
390
                    System.err.println("Initial load is already done");
391
                    StatusController.get().setReady();
392
                }
393
            } catch (Exception ex) {
394
                ex.printStackTrace();
395
                System.err.println("Initial load failed, terminating...");
396
                Runtime.getRuntime().exit(1);
397
            }
398

399
            // Start periodic nanopub loading
400
            System.err.println("Starting periodic nanopub loading...");
401
            var executor = Executors.newSingleThreadScheduledExecutor();
402
            executor.scheduleWithFixedDelay(
403
                    JellyNanopubLoader::loadUpdates,
404
                    JellyNanopubLoader.UPDATES_POLL_INTERVAL,
405
                    JellyNanopubLoader.UPDATES_POLL_INTERVAL,
406
                    TimeUnit.MILLISECONDS
407
            );
408
        }).start();
409

410
        Runtime.getRuntime().addShutdownHook(new Thread(() -> {
411
            try {
412
                System.err.println("Gracefully shutting down...");
413
                TripleStore.get().shutdownRepositories();
414
                vertx.close().toCompletionStage().toCompletableFuture().get(5, TimeUnit.SECONDS);
415
                System.err.println("Graceful shutdown completed");
416
            } catch (Exception ex) {
417
                System.err.println("Graceful shutdown failed");
418
                ex.printStackTrace();
419
            }
420
        }));
421
    }
422

423
    private String getResourceAsString(String file) {
424
        InputStream is = getClass().getClassLoader().getResourceAsStream("com/knowledgepixels/query/" + file);
425
        try (Scanner s = new Scanner(is).useDelimiter("\\A")) {
426
            String fileContent = s.hasNext() ? s.next() : "";
427
            return fileContent;
428
        }
429
    }
430

431
    private static void handleRedirect(RoutingContext req, String path) {
432
        String queryString = "";
433
        if (!req.queryParam("query").isEmpty())
434
            queryString = "?query=" + URLEncoder.encode(req.queryParam("query").get(0), Charsets.UTF_8);
435
        if (req.queryParam("for-type").size() == 1) {
436
            String type = req.queryParam("for-type").get(0);
437
            req.response().putHeader("location", path + "/type/" + Utils.createHash(type) + queryString);
438
            req.response().setStatusCode(301).end();
439
        } else if (req.queryParam("for-pubkey").size() == 1) {
440
            String type = req.queryParam("for-pubkey").get(0);
441
            req.response().putHeader("location", path + "/pubkey/" + Utils.createHash(type) + queryString);
442
            req.response().setStatusCode(301).end();
443
        } else if (req.queryParam("for-user").size() == 1) {
444
            String type = req.queryParam("for-user").get(0);
445
            req.response().putHeader("location", path + "/user/" + Utils.createHash(type) + queryString);
446
            req.response().setStatusCode(301).end();
447
        }
448
    }
449

450
    /**
451
     * Apply headers to the response that should be present for all requests.
452
     *
453
     * @param response The response to which the headers should be applied.
454
     */
455
    private static void applyGlobalHeaders(HttpServerResponse response) {
456
        response.putHeader("Nanopub-Query-Status", StatusController.get().getState().state.toString());
457
    }
458
}
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