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

knowledgepixels / nanopub-query / 20414263980

21 Dec 2025 06:37PM UTC coverage: 71.251%. Remained the same
20414263980

push

github

ashleycaselli
chore: code clean up

214 of 326 branches covered (65.64%)

Branch coverage included in aggregate %.

589 of 801 relevant lines covered (73.53%)

11.09 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
        HttpClient httpClient = vertx.createHttpClient(
51
                new HttpClientOptions()
52
                        .setConnectTimeout(Utils.getEnvInt("NANOPUB_QUERY_VERTX_CONNECT_TIMEOUT", 1000))
53
                        .setIdleTimeoutUnit(TimeUnit.SECONDS)
54
                        .setIdleTimeout(Utils.getEnvInt("NANOPUB_QUERY_VERTX_IDLE_TIMEOUT", 60))
55
                        .setReadIdleTimeout(Utils.getEnvInt("NANOPUB_QUERY_VERTX_IDLE_TIMEOUT", 60))
56
                        .setWriteIdleTimeout(Utils.getEnvInt("NANOPUB_QUERY_VERTX_IDLE_TIMEOUT", 60)),
57
                new PoolOptions().setHttp1MaxSize(200).setHttp2MaxSize(200)
58
        );
59

60
        HttpServer proxyServer = vertx.createHttpServer();
61
        Router proxyRouter = Router.router(vertx);
62
        proxyRouter.route().handler(CorsHandler.create().addRelativeOrigin(".*"));
63

64
        // Metrics
65
        final var metricsHttpServer = vertx.createHttpServer();
66
        final var metricsRouter = Router.router(vertx);
67
        metricsHttpServer.requestHandler(metricsRouter).listen(9394);
68

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

80
        rdf4jProxy.addInterceptor(new ProxyInterceptor() {
×
81

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

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

110
        });
111
        // ----------
112

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

298
        // TODO This is no longer needed and can be removed at some point:
299
        proxyRouter.route(HttpMethod.GET, "/grlc-spec/*").handler(req -> {
300
            try {
301
                GrlcSpec gsp = new GrlcSpec(req.normalizedPath(), req.queryParams());
302
                req.response().putHeader("content-type", "text/yaml").end(gsp.getSpec());
303
            } catch (InvalidGrlcSpecException ex) {
304
                req.response().setStatusCode(400).end(ex.getMessage());
305
            } catch (Exception ex) {
306
                req.response().setStatusCode(500).end("Unexpected error: " + ex.getMessage());
307
            }
308
        });
309

310
        proxyRouter.route(HttpMethod.GET, "/openapi/spec/*").handler(req -> {
311
            try {
312
                OpenApiSpecPage osp = new OpenApiSpecPage(req.normalizedPath(), req.queryParams());
313
                req.response().putHeader("content-type", "text/yaml").end(osp.getSpec());
314
            } catch (InvalidGrlcSpecException ex) {
315
                req.response().setStatusCode(400).end("Invlid grlc API definition: " + ex.getMessage());
316
            } catch (Exception ex) {
317
                req.response().setStatusCode(500).end("Unexpected error: " + ex.getMessage());
318
            }
319
        });
320

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

323
        HttpProxy grlcxProxy = HttpProxy.reverseProxy(httpClient);
324
        grlcxProxy.origin(proxyPort, proxy);
325

326
        grlcxProxy.addInterceptor(new ProxyInterceptor() {
×
327

328
            @Override
329
            @GeneratedFlagForDependentElements
330
            public Future<ProxyResponse> handleProxyRequest(ProxyContext context) {
331
                final ProxyRequest req = context.request();
332
                final String apiPattern = "^/api/(RA[a-zA-Z0-9-_]{43})/([a-zA-Z0-9-_]+)([?].*)?$";
333
                if (req.getURI().matches(apiPattern)) {
334
                    try {
335
                        GrlcSpec grlcSpec = new GrlcSpec(req.getURI(), req.proxiedRequest().params());
336
                        req.setMethod(HttpMethod.POST);
337

338
                        // Variant 1:
339
                        req.putHeader("Content-Type", "application/sparql-query");
340
                        req.setBody(Body.body(Buffer.buffer(grlcSpec.expandQuery())));
341
                        // Variant 2:
342
                        //req.putHeader("Content-Type", "application/x-www-form-urlencoded");
343
                        //req.setBody(Body.body(Buffer.buffer("query=" + URLEncoder.encode(grlcSpec.getExpandedQueryContent(), Charsets.UTF_8))));
344

345
                        req.setURI("/rdf4j-server/repositories/" + grlcSpec.getRepoName());
346
                        log.info("Forwarding apix request to /rdf4j-server/repositories/", grlcSpec.getRepoName());
347
                    } catch (InvalidGrlcSpecException ex) {
348
                        return Future.succeededFuture(context.request()
349
                                .response()
350
                                .setStatusCode(400)
351
                                .putHeader("Content-Type", "text/plain")
352
                                .setBody(Body.body(Buffer.buffer("Bad request: " + ex.getMessage()))));
353
                    } catch (Exception ex) {
354
                        return Future.succeededFuture(context.request()
355
                                .response()
356
                                .setStatusCode(500)
357
                                .putHeader("Content-Type", "text/plain")
358
                                .setBody(Body.body(Buffer.buffer("Unexpected error: " + ex.getMessage()))));
359
                    }
360
                }
361
                return ProxyInterceptor.super.handleProxyRequest(context);
362
            }
363

364
            @Override
365
            @GeneratedFlagForDependentElements
366
            public Future<Void> handleProxyResponse(ProxyContext context) {
367
                log.info("Receiving api response");
368
                ProxyResponse resp = context.response();
369
                resp.putHeader("Access-Control-Allow-Origin", "*");
370
                resp.putHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS");
371
                resp.putHeader("Content-Disposition", "inline");
372
                return ProxyInterceptor.super.handleProxyResponse(context);
373
            }
374

375
        });
376
        proxyRouter.route(HttpMethod.GET, "/api/*").handler(ProxyHandler.create(grlcxProxy));
377

378
        proxyServer.requestHandler(req -> {
379
            applyGlobalHeaders(req.response());
380
            proxyRouter.handle(req);
381
        });
382
        proxyServer.listen(9393);
383

384
        // Periodic metrics update
385
        vertx.setPeriodic(1000, id -> collector.updateMetrics());
386

387

388
        new Thread(() -> {
389
            try {
390
                var status = StatusController.get().initialize();
391
                log.info("Current state: {}, last committed counter: {}", status.state, status.loadCounter);
392
                if (status.state == StatusController.State.LAUNCHING || status.state == StatusController.State.LOADING_INITIAL) {
393
                    // Do the initial nanopublication loading
394
                    StatusController.get().setLoadingInitial(status.loadCounter);
395
                    // Fall back to local nanopub loading if the local files are present
396
                    if (!LocalNanopubLoader.init()) {
397
                        JellyNanopubLoader.loadInitial(status.loadCounter);
398
                    } else {
399
                        log.info("Local nanopublication loading finished");
400
                    }
401
                    StatusController.get().setReady();
402
                } else {
403
                    log.info("Initial load is already done");
404
                    StatusController.get().setReady();
405
                }
406
            } catch (Exception ex) {
407
                log.info("Initial load failed, terminating...", ex);
408
                Runtime.getRuntime().exit(1);
409
            }
410

411
            // Start periodic nanopub loading
412
            log.info("Starting periodic nanopub loading...");
413
            var executor = Executors.newSingleThreadScheduledExecutor();
414
            executor.scheduleWithFixedDelay(
415
                    JellyNanopubLoader::loadUpdates,
416
                    JellyNanopubLoader.UPDATES_POLL_INTERVAL,
417
                    JellyNanopubLoader.UPDATES_POLL_INTERVAL,
418
                    TimeUnit.MILLISECONDS
419
            );
420
        }).start();
421

422
        Runtime.getRuntime().addShutdownHook(new Thread(() -> {
423
            try {
424
                log.info("Gracefully shutting down...");
425
                TripleStore.get().shutdownRepositories();
426
                vertx.close().toCompletionStage().toCompletableFuture().get(5, TimeUnit.SECONDS);
427
                log.info("Graceful shutdown completed");
428
            } catch (Exception ex) {
429
                log.info("Graceful shutdown failed", ex);
430
            }
431
        }));
432
    }
433

434
    private String getResourceAsString(String file) {
435
        InputStream is = getClass().getClassLoader().getResourceAsStream("com/knowledgepixels/query/" + file);
436
        try (Scanner s = new Scanner(is).useDelimiter("\\A")) {
437
            String fileContent = s.hasNext() ? s.next() : "";
438
            return fileContent;
439
        }
440
    }
441

442
    private static void handleRedirect(RoutingContext req, String path) {
443
        String queryString = "";
444
        if (!req.queryParam("query").isEmpty())
445
            queryString = "?query=" + URLEncoder.encode(req.queryParam("query").getFirst(), Charsets.UTF_8);
446
        if (req.queryParam("for-type").size() == 1) {
447
            String type = req.queryParam("for-type").getFirst();
448
            req.response().putHeader("location", path + "/type/" + Utils.createHash(type) + queryString);
449
            req.response().setStatusCode(301).end();
450
        } else if (req.queryParam("for-pubkey").size() == 1) {
451
            String type = req.queryParam("for-pubkey").getFirst();
452
            req.response().putHeader("location", path + "/pubkey/" + Utils.createHash(type) + queryString);
453
            req.response().setStatusCode(301).end();
454
        } else if (req.queryParam("for-user").size() == 1) {
455
            String type = req.queryParam("for-user").getFirst();
456
            req.response().putHeader("location", path + "/user/" + Utils.createHash(type) + queryString);
457
            req.response().setStatusCode(301).end();
458
        }
459
    }
460

461
    /**
462
     * Apply headers to the response that should be present for all requests.
463
     *
464
     * @param response The response to which the headers should be applied.
465
     */
466
    private static void applyGlobalHeaders(HttpServerResponse response) {
467
        response.putHeader("Nanopub-Query-Status", StatusController.get().getState().state.toString());
468
    }
469
}
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