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

knowledgepixels / nanopub-query / 24658059155

20 Apr 2026 09:06AM UTC coverage: 60.573% (-0.2%) from 60.755%
24658059155

push

github

web-flow
Merge pull request #72 from knowledgepixels/fix/space-registry-sync-and-feature-flags

fix/feat: synchronise SpaceRegistry and add trust/spaces feature flags

293 of 538 branches covered (54.46%)

Branch coverage included in aggregate %.

827 of 1311 relevant lines covered (63.08%)

9.21 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
        HttpClient httpClient = vertx.createHttpClient(
60
                new HttpClientOptions()
61
                        .setConnectTimeout(Utils.getEnvInt("NANOPUB_QUERY_VERTX_CONNECT_TIMEOUT", 1000))
62
                        .setIdleTimeoutUnit(TimeUnit.SECONDS)
63
                        .setIdleTimeout(Utils.getEnvInt("NANOPUB_QUERY_VERTX_IDLE_TIMEOUT", 60))
64
                        .setReadIdleTimeout(Utils.getEnvInt("NANOPUB_QUERY_VERTX_IDLE_TIMEOUT", 60))
65
                        .setWriteIdleTimeout(Utils.getEnvInt("NANOPUB_QUERY_VERTX_IDLE_TIMEOUT", 60)),
66
                new PoolOptions().setHttp1MaxSize(200).setHttp2MaxSize(200)
67
        );
68

69
        HttpServer proxyServer = vertx.createHttpServer(
70
                new HttpServerOptions().setMaxInitialLineLength(65536)
71
        );
72
        Router proxyRouter = Router.router(vertx);
73
        proxyRouter.route().handler(CorsHandler.create().addRelativeOrigin(".*"));
74

75
        // Metrics
76
        final var metricsHttpServer = vertx.createHttpServer();
77
        final var metricsRouter = Router.router(vertx);
78
        metricsHttpServer.requestHandler(metricsRouter).listen(9394);
79

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

91
        rdf4jProxy.addInterceptor(new ProxyInterceptor() {
×
92

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

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

121
        });
122
        // ----------
123

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

321
        // TODO This is no longer needed and can be removed at some point:
322
        proxyRouter.route(HttpMethod.GET, "/grlc-spec/*").handler(req -> {
323
            vertx.<String>executeBlocking(() -> {
324
                GrlcSpec gsp = new GrlcSpec(req.normalizedPath(), req.queryParams());
325
                return gsp.getSpec();
326
            }, false).onSuccess(spec -> {
327
                req.response().putHeader("content-type", "text/yaml").end(spec);
328
            }).onFailure(ex -> {
329
                if (ex instanceof InvalidGrlcSpecException) {
330
                    req.response().setStatusCode(400).end(ex.getMessage());
331
                } else {
332
                    req.response().setStatusCode(500).end("Unexpected error: " + ex.getMessage());
333
                }
334
            });
335
        });
336

337
        proxyRouter.route(HttpMethod.GET, "/openapi/spec/*").handler(req -> {
338
            vertx.<String>executeBlocking(() -> {
339
                OpenApiSpecPage osp = new OpenApiSpecPage(req.normalizedPath(), req.queryParams());
340
                return osp.getSpec();
341
            }, false).onSuccess(spec -> {
342
                req.response().putHeader("content-type", "text/yaml").end(spec);
343
            }).onFailure(ex -> {
344
                if (ex instanceof InvalidGrlcSpecException) {
345
                    req.response().setStatusCode(400).end("Invalid grlc API definition: " + ex.getMessage());
346
                } else {
347
                    req.response().setStatusCode(500).end("Unexpected error: " + ex.getMessage());
348
                }
349
            });
350
        });
351

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

354
        HttpProxy grlcxProxy = HttpProxy.reverseProxy(httpClient);
355
        grlcxProxy.origin(proxyPort, proxy);
356

357
        grlcxProxy.addInterceptor(new ProxyInterceptor() {
×
358

359
            @Override
360
            @GeneratedFlagForDependentElements
361
            public Future<ProxyResponse> handleProxyRequest(ProxyContext context) {
362
                final ProxyRequest req = context.request();
363
                final String apiPattern = "^/api/(RA[a-zA-Z0-9-_]{43})/([a-zA-Z0-9-_]+)([.]csv|[.]json|[.]srx)?([?].*)?$";
364
                if (req.getURI().matches(apiPattern)) {
365
                    try {
366
                        req.setMethod(HttpMethod.POST);
367
                        if (req.getURI().matches(".*[.]csv([?].*)?$")) {
368
                            req.putHeader("Accept", "text/csv");
369
                            req.setURI(req.getURI().replaceFirst("[.]csv([?].*)?$", "$1"));
370
                        } else if (req.getURI().matches(".*[.]json([?].*)?$")) {
371
                            req.putHeader("Accept", "application/json");
372
                            req.setURI(req.getURI().replaceFirst("[.]json([?].*)?$", "$1"));
373
                        } else if (req.getURI().matches(".*[.]srx([?].*)?$")) {
374
                            req.putHeader("Accept", "application/xml");
375
                            req.setURI(req.getURI().replaceFirst("[.]srx([?].*)?$", "$1"));
376
                        }
377
                        GrlcSpec grlcSpec = new GrlcSpec(req.getURI(), req.proxiedRequest().params());
378

379
                        // Variant 1:
380
                        req.putHeader("Content-Type", "application/sparql-query");
381
                        req.setBody(Body.body(Buffer.buffer(grlcSpec.expandQuery())));
382
                        // Variant 2:
383
                        //req.putHeader("Content-Type", "application/x-www-form-urlencoded");
384
                        //req.setBody(Body.body(Buffer.buffer("query=" + URLEncoder.encode(grlcSpec.getExpandedQueryContent(), Charsets.UTF_8))));
385

386
                        req.setURI("/rdf4j-server/repositories/" + grlcSpec.getRepoName());
387
                        log.info("Forwarding apix request to /rdf4j-server/repositories/", grlcSpec.getRepoName());
388
                    } catch (InvalidGrlcSpecException ex) {
389
                        return Future.succeededFuture(context.request()
390
                                .response()
391
                                .setStatusCode(400)
392
                                .putHeader("Content-Type", "text/plain")
393
                                .setBody(Body.body(Buffer.buffer("Bad request: " + ex.getMessage()))));
394
                    } catch (Exception ex) {
395
                        return Future.succeededFuture(context.request()
396
                                .response()
397
                                .setStatusCode(500)
398
                                .putHeader("Content-Type", "text/plain")
399
                                .setBody(Body.body(Buffer.buffer("Unexpected error: " + ex.getMessage()))));
400
                    }
401
                }
402
                return ProxyInterceptor.super.handleProxyRequest(context);
403
            }
404

405
            @Override
406
            @GeneratedFlagForDependentElements
407
            public Future<Void> handleProxyResponse(ProxyContext context) {
408
                log.info("Receiving api response");
409
                ProxyResponse resp = context.response();
410
                resp.putHeader("Access-Control-Allow-Origin", "*");
411
                resp.putHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS");
412
                resp.putHeader("Content-Disposition", "inline");
413
                return ProxyInterceptor.super.handleProxyResponse(context);
414
            }
415

416
        });
417
        proxyRouter.route(HttpMethod.GET, "/api/*").handler(ProxyHandler.create(grlcxProxy));
418

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

425
        proxyServer.requestHandler(req -> {
426
            applyGlobalHeaders(req.response());
427
            proxyRouter.handle(req);
428
        });
429
        proxyServer.listen(9393);
430

431
        // Periodic metrics update
432
        vertx.setPeriodic(1000, id -> collector.updateMetrics());
433

434

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

497
            // Seed the TrustStateRegistry from any persisted pointer before the
498
            // periodic poll begins, so the first tick doesn't re-materialize state
499
            // we already have.
500
            TrustStateLoader.bootstrap();
501

502
            // Seed the SpaceRegistry from any persisted (spaceRef, spaceIri) pairs
503
            // so previously-known spaces survive a restart, then catch up on any
504
            // gen:Space-typed nanopubs that were already loaded before persistence
505
            // was wired up (so existing deployments don't need a fresh DB).
506
            SpacesAdminStore.bootstrap(SpaceRegistry.get());
507
            SpacesAdminStore.scanExistingSpaces(SpaceRegistry.get());
508

509
            // Start periodic nanopub loading
510
            log.info("Starting periodic nanopub loading...");
511
            var executor = Executors.newSingleThreadScheduledExecutor();
512
            executor.scheduleWithFixedDelay(
513
                    JellyNanopubLoader::loadUpdates,
514
                    JellyNanopubLoader.UPDATES_POLL_INTERVAL,
515
                    JellyNanopubLoader.UPDATES_POLL_INTERVAL,
516
                    TimeUnit.MILLISECONDS
517
            );
518
        }).start();
519

520
        Runtime.getRuntime().addShutdownHook(new Thread(() -> {
521
            try {
522
                log.info("Gracefully shutting down...");
523
                TripleStore.get().shutdownRepositories();
524
                vertx.close().toCompletionStage().toCompletableFuture().get(5, TimeUnit.SECONDS);
525
                log.info("Graceful shutdown completed");
526
            } catch (Exception ex) {
527
                log.info("Graceful shutdown failed", ex);
528
            }
529
        }));
530
    }
531

532
    private String getResourceAsString(String file) {
533
        InputStream is = getClass().getClassLoader().getResourceAsStream("com/knowledgepixels/query/" + file);
534
        try (Scanner s = new Scanner(is).useDelimiter("\\A")) {
535
            String fileContent = s.hasNext() ? s.next() : "";
536
            return fileContent;
537
        }
538
    }
539

540
    private static void handleRedirect(RoutingContext req, String path) {
541
        String queryString = "";
542
        if (!req.queryParam("query").isEmpty())
543
            queryString = "?query=" + URLEncoder.encode(req.queryParam("query").getFirst(), Charsets.UTF_8);
544
        if (req.queryParam("for-type").size() == 1) {
545
            String type = req.queryParam("for-type").getFirst();
546
            req.response().putHeader("location", path + "/type/" + Utils.createHash(type) + queryString);
547
            req.response().setStatusCode(301).end();
548
        } else if (req.queryParam("for-pubkey").size() == 1) {
549
            String type = req.queryParam("for-pubkey").getFirst();
550
            req.response().putHeader("location", path + "/pubkey/" + Utils.createHash(type) + queryString);
551
            req.response().setStatusCode(301).end();
552
        } else if (req.queryParam("for-user").size() == 1) {
553
            String type = req.queryParam("for-user").getFirst();
554
            req.response().putHeader("location", path + "/user/" + Utils.createHash(type) + queryString);
555
            req.response().setStatusCode(301).end();
556
        }
557
    }
558

559
    /**
560
     * Apply headers to the response that should be present for all requests.
561
     *
562
     * @param response The response to which the headers should be applied.
563
     */
564
    static void applyGlobalHeaders(HttpServerResponse response) {
565
        var state = StatusController.get().getState();
566
        response.putHeader("Nanopub-Query-Status", state.state.toString());
567
        response.putHeader("Nanopub-Query-Registry-Url", JellyNanopubLoader.registryUrl);
568
        Long setupId = StatusController.get().getRegistrySetupId();
569
        response.putHeader("Nanopub-Query-Registry-Setup-Id", setupId == null ? "" : setupId.toString());
570
        response.putHeader("Nanopub-Query-Load-Counter", String.valueOf(state.loadCounter));
571
        // Forward registry metadata headers
572
        String coverageTypes = JellyNanopubLoader.lastCoverageTypes;
573
        response.putHeader("Nanopub-Query-Registry-Coverage-Types", coverageTypes != null ? coverageTypes : "all");
574
        String coverageAgents = JellyNanopubLoader.lastCoverageAgents;
575
        response.putHeader("Nanopub-Query-Registry-Coverage-Agents", coverageAgents != null ? coverageAgents : "viaSetting");
576
        String testInstance = JellyNanopubLoader.lastTestInstance;
577
        if (testInstance != null) {
578
            response.putHeader("Nanopub-Query-Registry-Test-Instance", testInstance);
579
        }
580
        String nanopubCount = JellyNanopubLoader.lastNanopubCount;
581
        if (nanopubCount != null) {
582
            response.putHeader("Nanopub-Query-Registry-Nanopub-Count", nanopubCount);
583
        }
584
    }
585
}
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