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

knowledgepixels / nanopub-query / 24669686981

20 Apr 2026 01:37PM UTC coverage: 60.242% (+0.3%) from 59.905%
24669686981

push

github

web-flow
Merge pull request #76 from knowledgepixels/feat/optional-repo-disables

feat: change 9 — optional per-instance disable of full/text/last30d repo writes

293 of 544 branches covered (53.86%)

Branch coverage included in aggregate %.

851 of 1355 relevant lines covered (62.8%)

8.97 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 com.github.jsonldjava.shaded.com.google.common.base.Charsets;
4
import com.knowledgepixels.query.GrlcSpec.InvalidGrlcSpecException;
5
import io.micrometer.prometheus.PrometheusMeterRegistry;
6
import io.vertx.core.AbstractVerticle;
7
import io.vertx.core.Future;
8
import io.vertx.core.Promise;
9
import io.vertx.core.buffer.Buffer;
10
import io.vertx.core.http.*;
11
import io.vertx.ext.web.Router;
12
import io.vertx.ext.web.RoutingContext;
13
import io.vertx.ext.web.handler.CorsHandler;
14
import io.vertx.ext.web.handler.StaticHandler;
15
import io.vertx.ext.web.proxy.handler.ProxyHandler;
16
import io.vertx.httpproxy.*;
17
import io.vertx.micrometer.PrometheusScrapingHandler;
18
import io.vertx.micrometer.backends.BackendRegistries;
19
import org.eclipse.rdf4j.model.Value;
20
import org.slf4j.Logger;
21
import org.slf4j.LoggerFactory;
22

23
import java.io.InputStream;
24
import java.net.URLEncoder;
25
import java.util.ArrayList;
26
import java.util.Collections;
27
import java.util.List;
28
import java.util.Scanner;
29
import java.util.concurrent.Executors;
30
import java.util.concurrent.TimeUnit;
31

32
/**
33
 * Main verticle that coordinates the incoming HTTP requests.
34
 */
35
@GeneratedFlagForDependentElements
36
public class MainVerticle extends AbstractVerticle {
37

38
    private static String css = null;
39

40
    private static final Logger log = LoggerFactory.getLogger(MainVerticle.class);
41

42
    /**
43
     * Start the main verticle.
44
     *
45
     * @param startPromise the promise to complete when the verticle is started
46
     * @throws Exception if an error occurs during startup
47
     */
48
    @Override
49
    public void start(Promise<Void> startPromise) throws Exception {
50
        if (!FeatureFlags.trustStateEnabled()) {
51
            log.warn("Trust state feature disabled via NANOPUB_QUERY_ENABLE_TRUST_STATE=false — "
52
                    + "no trust snapshots will be fetched or materialised, and the 'trust' repo will not be auto-created.");
53
        }
54
        if (!FeatureFlags.spacesEnabled()) {
55
            log.warn("Spaces feature disabled via NANOPUB_QUERY_ENABLE_SPACES=false — "
56
                    + "no space-defining nanopubs will be registered, no extracts will be written, "
57
                    + "and the 'spaces' repo will not be auto-created.");
58
        }
59
        if (!FeatureFlags.fullRepoEnabled()) {
60
            log.warn("Writes to the 'full' repo disabled via NANOPUB_QUERY_ENABLE_FULL_REPO=false — "
61
                    + "generic SPARQL queries against /repo/full will return an empty store.");
62
        }
63
        if (!FeatureFlags.textRepoEnabled()) {
64
            log.warn("Writes to the 'text' repo disabled via NANOPUB_QUERY_ENABLE_TEXT_REPO=false — "
65
                    + "full-text search via /repo/text will return nothing.");
66
        }
67
        if (!FeatureFlags.last30dRepoEnabled()) {
68
            log.warn("Writes to the 'last30d' repo disabled via NANOPUB_QUERY_ENABLE_LAST30D_REPO=false — "
69
                    + "the /repo/last30d endpoint will be empty; rewrite queries against /repo/full with a date filter.");
70
        }
71
        HttpClient httpClient = vertx.createHttpClient(
72
                new HttpClientOptions()
73
                        .setConnectTimeout(Utils.getEnvInt("NANOPUB_QUERY_VERTX_CONNECT_TIMEOUT", 1000))
74
                        .setIdleTimeoutUnit(TimeUnit.SECONDS)
75
                        .setIdleTimeout(Utils.getEnvInt("NANOPUB_QUERY_VERTX_IDLE_TIMEOUT", 60))
76
                        .setReadIdleTimeout(Utils.getEnvInt("NANOPUB_QUERY_VERTX_IDLE_TIMEOUT", 60))
77
                        .setWriteIdleTimeout(Utils.getEnvInt("NANOPUB_QUERY_VERTX_IDLE_TIMEOUT", 60)),
78
                new PoolOptions().setHttp1MaxSize(200).setHttp2MaxSize(200)
79
        );
80

81
        HttpServer proxyServer = vertx.createHttpServer(
82
                new HttpServerOptions().setMaxInitialLineLength(65536)
83
        );
84
        Router proxyRouter = Router.router(vertx);
85
        proxyRouter.route().handler(CorsHandler.create().addRelativeOrigin(".*"));
86

87
        // Metrics
88
        final var metricsHttpServer = vertx.createHttpServer();
89
        final var metricsRouter = Router.router(vertx);
90
        metricsHttpServer.requestHandler(metricsRouter).listen(9394);
91

92
        final var metricsRegistry = (PrometheusMeterRegistry) BackendRegistries.getDefaultNow();
93
        final var collector = new MetricsCollector(metricsRegistry);
94
        metricsRouter.route("/metrics").handler(PrometheusScrapingHandler.create(metricsRegistry));
95
        // ----------
96
        // This part is only used if the redirection is not done through Nginx.
97
        // See nginx.conf and this bug report: https://github.com/eclipse-rdf4j/rdf4j/discussions/5120
98
        HttpProxy rdf4jProxy = HttpProxy.reverseProxy(httpClient);
99
        String proxy = Utils.getEnvString("RDF4J_PROXY_HOST", "rdf4j");
100
        int proxyPort = Utils.getEnvInt("RDF4J_PROXY_PORT", 8080);
101
        rdf4jProxy.origin(proxyPort, proxy);
102

103
        rdf4jProxy.addInterceptor(new ProxyInterceptor() {
×
104

105
            @Override
106
            @GeneratedFlagForDependentElements
107
            public Future<ProxyResponse> handleProxyRequest(ProxyContext context) {
108
                ProxyRequest request = context.request();
109
                request.setURI(request.getURI().replaceAll("/", "_").replaceFirst("^_repo_", "/rdf4j-server/repositories/"));
110
                // For later to try to get HTML tables out:
111
//                                if (request.headers().get("Accept") == null) {
112
//                                        request.putHeader("Accept", "text/html");
113
//                                }
114
//                                request.putHeader("Accept", "application/json");
115
                return ProxyInterceptor.super.handleProxyRequest(context);
116
            }
117

118
            @Override
119
            @GeneratedFlagForDependentElements
120
            public Future<Void> handleProxyResponse(ProxyContext context) {
121
                ProxyResponse resp = context.response();
122
                resp.putHeader("Access-Control-Allow-Origin", "*");
123
                resp.putHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS");
124
                // For later to try to get HTML tables out:
125
//                                String acceptHeader = context.request().headers().get("Accept");
126
//                                if (acceptHeader != null && acceptHeader.contains("text/html")) {
127
//                                        resp.putHeader("Content-Type", "text/html");
128
//                                        resp.setBody(Body.body(Buffer.buffer("<html><body><strong>test</strong></body></html>")));
129
//                                }
130
                return ProxyInterceptor.super.handleProxyResponse(context);
131
            }
132

133
        });
134
        // ----------
135

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

333
        // TODO This is no longer needed and can be removed at some point:
334
        proxyRouter.route(HttpMethod.GET, "/grlc-spec/*").handler(req -> {
335
            vertx.<String>executeBlocking(() -> {
336
                GrlcSpec gsp = new GrlcSpec(req.normalizedPath(), req.queryParams());
337
                return gsp.getSpec();
338
            }, false).onSuccess(spec -> {
339
                req.response().putHeader("content-type", "text/yaml").end(spec);
340
            }).onFailure(ex -> {
341
                if (ex instanceof InvalidGrlcSpecException) {
342
                    req.response().setStatusCode(400).end(ex.getMessage());
343
                } else {
344
                    req.response().setStatusCode(500).end("Unexpected error: " + ex.getMessage());
345
                }
346
            });
347
        });
348

349
        proxyRouter.route(HttpMethod.GET, "/openapi/spec/*").handler(req -> {
350
            vertx.<String>executeBlocking(() -> {
351
                OpenApiSpecPage osp = new OpenApiSpecPage(req.normalizedPath(), req.queryParams());
352
                return osp.getSpec();
353
            }, false).onSuccess(spec -> {
354
                req.response().putHeader("content-type", "text/yaml").end(spec);
355
            }).onFailure(ex -> {
356
                if (ex instanceof InvalidGrlcSpecException) {
357
                    req.response().setStatusCode(400).end("Invalid grlc API definition: " + ex.getMessage());
358
                } else {
359
                    req.response().setStatusCode(500).end("Unexpected error: " + ex.getMessage());
360
                }
361
            });
362
        });
363

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

366
        HttpProxy grlcxProxy = HttpProxy.reverseProxy(httpClient);
367
        grlcxProxy.origin(proxyPort, proxy);
368

369
        grlcxProxy.addInterceptor(new ProxyInterceptor() {
×
370

371
            @Override
372
            @GeneratedFlagForDependentElements
373
            public Future<ProxyResponse> handleProxyRequest(ProxyContext context) {
374
                final ProxyRequest req = context.request();
375
                final String apiPattern = "^/api/(RA[a-zA-Z0-9-_]{43})/([a-zA-Z0-9-_]+)([.]csv|[.]json|[.]srx)?([?].*)?$";
376
                if (req.getURI().matches(apiPattern)) {
377
                    try {
378
                        req.setMethod(HttpMethod.POST);
379
                        if (req.getURI().matches(".*[.]csv([?].*)?$")) {
380
                            req.putHeader("Accept", "text/csv");
381
                            req.setURI(req.getURI().replaceFirst("[.]csv([?].*)?$", "$1"));
382
                        } else if (req.getURI().matches(".*[.]json([?].*)?$")) {
383
                            req.putHeader("Accept", "application/json");
384
                            req.setURI(req.getURI().replaceFirst("[.]json([?].*)?$", "$1"));
385
                        } else if (req.getURI().matches(".*[.]srx([?].*)?$")) {
386
                            req.putHeader("Accept", "application/xml");
387
                            req.setURI(req.getURI().replaceFirst("[.]srx([?].*)?$", "$1"));
388
                        }
389
                        GrlcSpec grlcSpec = new GrlcSpec(req.getURI(), req.proxiedRequest().params());
390

391
                        // Variant 1:
392
                        req.putHeader("Content-Type", "application/sparql-query");
393
                        req.setBody(Body.body(Buffer.buffer(grlcSpec.expandQuery())));
394
                        // Variant 2:
395
                        //req.putHeader("Content-Type", "application/x-www-form-urlencoded");
396
                        //req.setBody(Body.body(Buffer.buffer("query=" + URLEncoder.encode(grlcSpec.getExpandedQueryContent(), Charsets.UTF_8))));
397

398
                        req.setURI("/rdf4j-server/repositories/" + grlcSpec.getRepoName());
399
                        log.info("Forwarding apix request to /rdf4j-server/repositories/", grlcSpec.getRepoName());
400
                    } catch (InvalidGrlcSpecException ex) {
401
                        return Future.succeededFuture(context.request()
402
                                .response()
403
                                .setStatusCode(400)
404
                                .putHeader("Content-Type", "text/plain")
405
                                .setBody(Body.body(Buffer.buffer("Bad request: " + ex.getMessage()))));
406
                    } catch (Exception ex) {
407
                        return Future.succeededFuture(context.request()
408
                                .response()
409
                                .setStatusCode(500)
410
                                .putHeader("Content-Type", "text/plain")
411
                                .setBody(Body.body(Buffer.buffer("Unexpected error: " + ex.getMessage()))));
412
                    }
413
                }
414
                return ProxyInterceptor.super.handleProxyRequest(context);
415
            }
416

417
            @Override
418
            @GeneratedFlagForDependentElements
419
            public Future<Void> handleProxyResponse(ProxyContext context) {
420
                log.info("Receiving api response");
421
                ProxyResponse resp = context.response();
422
                resp.putHeader("Access-Control-Allow-Origin", "*");
423
                resp.putHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS");
424
                resp.putHeader("Content-Disposition", "inline");
425
                return ProxyInterceptor.super.handleProxyResponse(context);
426
            }
427

428
        });
429
        proxyRouter.route(HttpMethod.GET, "/api/*").handler(ProxyHandler.create(grlcxProxy));
430

431
        // Handle HEAD requests for all paths not already covered (e.g. /repo/* has its own HEAD handler).
432
        // Global headers are applied before routing, so we just end the response with no body.
433
        proxyRouter.route(HttpMethod.HEAD, "/*").handler(req -> {
434
            req.response().setStatusCode(200).end();
435
        });
436

437
        proxyServer.requestHandler(req -> {
438
            applyGlobalHeaders(req.response());
439
            proxyRouter.handle(req);
440
        });
441
        proxyServer.listen(9393);
442

443
        // Periodic metrics update. Runs on a dedicated single-thread scheduled executor
444
        // (not on the Vert.x event loop) because `updateMetrics` can fall through to a
445
        // synchronous HTTP call in `TripleStore.getRepositoryNames()` when the cache has
446
        // been invalidated. `scheduleWithFixedDelay` naturally serialises ticks and cannot
447
        // pile up if the work occasionally runs long. Same pattern as `JellyNanopubLoader.loadUpdates`
448
        // below.
449
        Executors.newSingleThreadScheduledExecutor()
450
                .scheduleWithFixedDelay(collector::updateMetrics, 1, 1, TimeUnit.SECONDS);
451

452

453
        new Thread(() -> {
454
            try {
455
                var status = StatusController.get().initialize();
456
                log.info("Current state: {}, last committed counter: {}", status.state, status.loadCounter);
457
                // Restore or fetch the registry setup ID
458
                Long storedSetupId = StatusController.get().getRegistrySetupId();
459
                if (storedSetupId != null) {
460
                    JellyNanopubLoader.setLastKnownSetupId(storedSetupId);
461
                    log.info("Restored registry setupId: {}", storedSetupId);
462
                } else if (status.state == StatusController.State.LAUNCHING
463
                        || status.state == StatusController.State.LOADING_INITIAL) {
464
                    // Fresh start or crashed during initial load – safe to adopt the current setupId
465
                    try {
466
                        var metadata = JellyNanopubLoader.fetchRegistryMetadata();
467
                        JellyNanopubLoader.setLastKnownSetupId(metadata.setupId());
468
                        if (metadata.setupId() != null) {
469
                            StatusController.get().setRegistrySetupId(metadata.setupId());
470
                            log.info("Fetched initial registry setupId: {}", metadata.setupId());
471
                        }
472
                    } catch (Exception e) {
473
                        log.warn("Could not fetch initial registry setupId", e);
474
                    }
475
                } else {
476
                    // Upgrade from a version without setupId tracking. The DB has data but
477
                    // we can't verify it matches the current registry state. Leave lastKnownSetupId
478
                    // as null so that loadUpdates() will trigger a resync.
479
                    log.warn("No stored registry setupId but DB has data (state: {}, counter: {}). "
480
                            + "A resync will be triggered on the first update poll.",
481
                            status.state, status.loadCounter);
482
                }
483
                boolean forceResync = "true".equalsIgnoreCase(
484
                        Utils.getEnvString("FORCE_RESYNC", "false"));
485
                if (forceResync && status.state != StatusController.State.LAUNCHING) {
486
                    log.warn("FORCE_RESYNC is set. Forcing full re-load from registry.");
487
                    var metadata = JellyNanopubLoader.fetchRegistryMetadata();
488
                    JellyNanopubLoader.setLastKnownSetupId(metadata.setupId());
489
                    if (metadata.setupId() != null) {
490
                        StatusController.get().setRegistrySetupId(metadata.setupId());
491
                    }
492
                    StatusController.get().setResetting();
493
                    StatusController.get().setLoadingInitial(-1);
494
                    JellyNanopubLoader.loadInitial(-1);
495
                    StatusController.get().setReady();
496
                } else if (status.state == StatusController.State.LAUNCHING || status.state == StatusController.State.LOADING_INITIAL) {
497
                    // Do the initial nanopublication loading
498
                    StatusController.get().setLoadingInitial(status.loadCounter);
499
                    // Fall back to local nanopub loading if the local files are present
500
                    if (!LocalNanopubLoader.init()) {
501
                        JellyNanopubLoader.loadInitial(status.loadCounter);
502
                    } else {
503
                        log.info("Local nanopublication loading finished");
504
                    }
505
                    StatusController.get().setReady();
506
                } else {
507
                    log.info("Initial load is already done");
508
                    StatusController.get().setReady();
509
                }
510
            } catch (Exception ex) {
511
                log.info("Initial load failed, terminating...", ex);
512
                Runtime.getRuntime().exit(1);
513
            }
514

515
            // Seed the TrustStateRegistry from any persisted pointer before the
516
            // periodic poll begins, so the first tick doesn't re-materialize state
517
            // we already have.
518
            TrustStateLoader.bootstrap();
519

520
            // Seed the SpaceRegistry from any persisted (spaceRef, spaceIri) pairs
521
            // so previously-known spaces survive a restart, then catch up on any
522
            // gen:Space-typed nanopubs that were already loaded before persistence
523
            // was wired up (so existing deployments don't need a fresh DB).
524
            SpacesAdminStore.bootstrap(SpaceRegistry.get());
525
            SpacesAdminStore.scanExistingSpaces(SpaceRegistry.get());
526

527
            // Start periodic nanopub loading
528
            log.info("Starting periodic nanopub loading...");
529
            var executor = Executors.newSingleThreadScheduledExecutor();
530
            executor.scheduleWithFixedDelay(
531
                    JellyNanopubLoader::loadUpdates,
532
                    JellyNanopubLoader.UPDATES_POLL_INTERVAL,
533
                    JellyNanopubLoader.UPDATES_POLL_INTERVAL,
534
                    TimeUnit.MILLISECONDS
535
            );
536
        }).start();
537

538
        Runtime.getRuntime().addShutdownHook(new Thread(() -> {
539
            try {
540
                log.info("Gracefully shutting down...");
541
                TripleStore.get().shutdownRepositories();
542
                vertx.close().toCompletionStage().toCompletableFuture().get(5, TimeUnit.SECONDS);
543
                log.info("Graceful shutdown completed");
544
            } catch (Exception ex) {
545
                log.info("Graceful shutdown failed", ex);
546
            }
547
        }));
548
    }
549

550
    private String getResourceAsString(String file) {
551
        InputStream is = getClass().getClassLoader().getResourceAsStream("com/knowledgepixels/query/" + file);
552
        try (Scanner s = new Scanner(is).useDelimiter("\\A")) {
553
            String fileContent = s.hasNext() ? s.next() : "";
554
            return fileContent;
555
        }
556
    }
557

558
    private static void handleRedirect(RoutingContext req, String path) {
559
        String queryString = "";
560
        if (!req.queryParam("query").isEmpty())
561
            queryString = "?query=" + URLEncoder.encode(req.queryParam("query").getFirst(), Charsets.UTF_8);
562
        if (req.queryParam("for-type").size() == 1) {
563
            String type = req.queryParam("for-type").getFirst();
564
            req.response().putHeader("location", path + "/type/" + Utils.createHash(type) + queryString);
565
            req.response().setStatusCode(301).end();
566
        } else if (req.queryParam("for-pubkey").size() == 1) {
567
            String type = req.queryParam("for-pubkey").getFirst();
568
            req.response().putHeader("location", path + "/pubkey/" + Utils.createHash(type) + queryString);
569
            req.response().setStatusCode(301).end();
570
        } else if (req.queryParam("for-user").size() == 1) {
571
            String type = req.queryParam("for-user").getFirst();
572
            req.response().putHeader("location", path + "/user/" + Utils.createHash(type) + queryString);
573
            req.response().setStatusCode(301).end();
574
        }
575
    }
576

577
    /**
578
     * Apply headers to the response that should be present for all requests.
579
     *
580
     * @param response The response to which the headers should be applied.
581
     */
582
    static void applyGlobalHeaders(HttpServerResponse response) {
583
        var state = StatusController.get().getState();
584
        response.putHeader("Nanopub-Query-Status", state.state.toString());
585
        response.putHeader("Nanopub-Query-Registry-Url", JellyNanopubLoader.registryUrl);
586
        Long setupId = StatusController.get().getRegistrySetupId();
587
        response.putHeader("Nanopub-Query-Registry-Setup-Id", setupId == null ? "" : setupId.toString());
588
        response.putHeader("Nanopub-Query-Load-Counter", String.valueOf(state.loadCounter));
589
        // Forward registry metadata headers
590
        String coverageTypes = JellyNanopubLoader.lastCoverageTypes;
591
        response.putHeader("Nanopub-Query-Registry-Coverage-Types", coverageTypes != null ? coverageTypes : "all");
592
        String coverageAgents = JellyNanopubLoader.lastCoverageAgents;
593
        response.putHeader("Nanopub-Query-Registry-Coverage-Agents", coverageAgents != null ? coverageAgents : "viaSetting");
594
        String testInstance = JellyNanopubLoader.lastTestInstance;
595
        if (testInstance != null) {
596
            response.putHeader("Nanopub-Query-Registry-Test-Instance", testInstance);
597
        }
598
        String nanopubCount = JellyNanopubLoader.lastNanopubCount;
599
        if (nanopubCount != null) {
600
            response.putHeader("Nanopub-Query-Registry-Nanopub-Count", nanopubCount);
601
        }
602
    }
603
}
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