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

knowledgepixels / nanopub-query / 17149479353

22 Aug 2025 07:48AM UTC coverage: 76.74% (+8.0%) from 68.694%
17149479353

push

github

tkuhn
Some more @GeneratedFlagForDependentElements

230 of 318 branches covered (72.33%)

Branch coverage included in aggregate %.

608 of 774 relevant lines covered (78.55%)

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

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

117
        });
118
        // ----------
119

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

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

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

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

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

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

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

364
        });
365

366
        proxyServer.requestHandler(req -> {
367
            applyGlobalHeaders(req.response());
368
            proxyRouter.handle(req);
369
        });
370
        proxyServer.listen(9393);
371

372
        proxyRouter.route("/api/*").handler(ProxyHandler.create(grlcProxy));
373
        proxyRouter.route("/static/*").handler(ProxyHandler.create(grlcProxy));
374

375
        // Periodic metrics update
376
        vertx.setPeriodic(1000, id -> collector.updateMetrics());
377

378

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

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

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

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

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

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