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

knowledgepixels / nanopub-query / 24888210076

24 Apr 2026 11:55AM UTC coverage: 64.515% (+5.3%) from 59.265%
24888210076

push

github

web-flow
Merge pull request #79 from knowledgepixels/feature/62-spaces-extraction-v2

feat: v2 spaces extraction layer (#62)

381 of 670 branches covered (56.87%)

Branch coverage included in aggregate %.

1028 of 1514 relevant lines covered (67.9%)

10.25 hits per line

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

63.96
src/main/java/com/knowledgepixels/query/TripleStore.java
1
package com.knowledgepixels.query;
2

3
import org.apache.commons.exec.environment.EnvironmentUtils;
4
import org.apache.http.client.config.RequestConfig;
5
import org.apache.http.client.methods.CloseableHttpResponse;
6
import org.apache.http.client.methods.HttpUriRequest;
7
import org.apache.http.client.methods.RequestBuilder;
8
import org.apache.http.entity.StringEntity;
9
import org.apache.http.impl.client.BasicResponseHandler;
10
import org.apache.http.impl.client.CloseableHttpClient;
11
import org.apache.http.impl.client.HttpClients;
12
import org.eclipse.rdf4j.common.transaction.IsolationLevels;
13
import org.eclipse.rdf4j.model.ValueFactory;
14
import org.eclipse.rdf4j.model.impl.SimpleValueFactory;
15
import org.eclipse.rdf4j.repository.Repository;
16
import org.eclipse.rdf4j.repository.RepositoryConnection;
17
import org.eclipse.rdf4j.repository.base.RepositoryConnectionWrapper;
18
import org.eclipse.rdf4j.repository.http.HTTPRepository;
19
import org.nanopub.NanopubUtils;
20
import org.nanopub.vocabulary.NPA;
21
import org.slf4j.Logger;
22
import org.slf4j.LoggerFactory;
23

24
import java.io.BufferedReader;
25
import java.io.IOException;
26
import java.io.InputStreamReader;
27
import java.util.*;
28
import java.util.Map.Entry;
29
import java.util.concurrent.ConcurrentHashMap;
30
import java.util.concurrent.TimeUnit;
31
import java.util.concurrent.atomic.AtomicBoolean;
32
import java.util.concurrent.atomic.AtomicInteger;
33
import java.util.concurrent.locks.ReadWriteLock;
34
import java.util.concurrent.locks.ReentrantReadWriteLock;
35

36
/**
37
 * Class to access the database in the form of triple stores.
38
 */
39
public class TripleStore {
40

41
    /**
42
     * Name of the admin graph.
43
     */
44
    public static final String ADMIN_REPO = "admin";
45

46
    private static ValueFactory vf = SimpleValueFactory.getInstance();
6✔
47

48
    private static final Logger log = LoggerFactory.getLogger(TripleStore.class);
12✔
49

50
    private final Map<String, Repository> repositories = new LinkedHashMap<>();
15✔
51

52
    /**
53
     * Per-repo open-connection counter, read by the eviction loop to skip repos that
54
     * still have live connections. Incremented in {@link #getRepoConnection(String)}
55
     * just before the connection is handed out and decremented via a
56
     * {@link RepositoryConnectionWrapper} that intercepts {@code close()} exactly once.
57
     */
58
    private final ConcurrentHashMap<String, AtomicInteger> openConnections = new ConcurrentHashMap<>();
15✔
59

60
    private String endpointBase = null;
9✔
61
    private String endpointType = null;
9✔
62

63
    private static TripleStore tripleStoreInstance;
64

65
    /**
66
     * Returns singleton triple store instance.
67
     *
68
     * @return Triple store instance
69
     */
70
    public static TripleStore get() {
71
        if (tripleStoreInstance == null) {
6!
72
            try {
73
                tripleStoreInstance = new TripleStore();
×
74
            } catch (IOException ex) {
×
75
                log.info("Could not init TripleStore. ", ex);
×
76
            }
×
77
        }
78
        return tripleStoreInstance;
×
79
    }
80

81
    private TripleStore() throws IOException {
6✔
82
        Map<String, String> env = EnvironmentUtils.getProcEnvironment();
6✔
83
        endpointBase = env.get("ENDPOINT_BASE");
18✔
84
        log.info("Endpoint base: {}", endpointBase);
15✔
85
        endpointType = env.get("ENDPOINT_TYPE");
18✔
86

87
        getRepository("empty");  // Make sure empty repo exists
×
88
    }
×
89

90
    /**
91
     * Shared HTTP client for all RDF4J traffic. Apache HttpClient treats all requests
92
     * to a given host + port + protocol as one "route", so every `HTTPRepository` in
93
     * this process funnels through this single client's connection pool — plus the
94
     * two ad-hoc users in {@link #createRepo} and {@link #getRepositoryNames}, which
95
     * have been consolidated onto this client too.
96
     *
97
     * <p>The Apache defaults (maxPerRoute=2 / maxTotal=20) throttle four concurrent
98
     * loader-pool threads down to two-way parallelism at the HTTP layer — invisible
99
     * client-side serialisation the code is actively fighting. Raised to 10 / 40
100
     * here: comfortable headroom for the 4-thread loader pool plus admin-repo
101
     * transactions and the metrics tick, small enough to be a conservative first
102
     * step with room to grow later.
103
     *
104
     * <p>Timeouts set via {@code setDefaultRequestConfig}:
105
     * <ul>
106
     *   <li><b>socket-read = 60 s</b> — an individual HTTP response body must arrive
107
     *       within this window. Healthy per-nanopub commits complete in milliseconds,
108
     *       so 60 s is pure safety margin; its job is to turn the silent "threads
109
     *       parked forever inside a commit" wedge (observed in the April test) into
110
     *       a recoverable error that feeds the existing retry path.</li>
111
     *   <li><b>connection-request = 30 s</b> — a caller waiting for a pooled connection
112
     *       gives up after this long. Prevents the invisible self-deadlock in
113
     *       {@code loadInvalidateStatements} (one thread holding N connections while
114
     *       waiting for an (N+1)th) from hanging forever.</li>
115
     *   <li><b>connect = 10 s</b> — kills TCP handshakes that stall. Generous but
116
     *       bounded.</li>
117
     * </ul>
118
     * Without these defaults, HttpClient uses {@code -1} everywhere, which means
119
     * "wait forever".
120
     */
121
    private final CloseableHttpClient httpclient = HttpClients.custom()
9✔
122
            .setMaxConnPerRoute(10)
6✔
123
            .setMaxConnTotal(40)
3✔
124
            .setDefaultRequestConfig(RequestConfig.custom()
9✔
125
                    .setSocketTimeout(60_000)
6✔
126
                    .setConnectionRequestTimeout(30_000)
6✔
127
                    .setConnectTimeout(10_000)
3✔
128
                    .build())
3✔
129
            // Hygiene: kill pooled connections that RDF4J has quietly closed server-side
130
            // before we try to reuse them. Without this, a half-broken connection is
131
            // only noticed when the next request fails, spending the full socket-read
132
            // timeout discovering it.
133
            .evictExpiredConnections()
9✔
134
            .evictIdleConnections(30, TimeUnit.SECONDS)
3✔
135
            .build();
6✔
136

137
    @GeneratedFlagForDependentElements
138
    Repository getRepository(String name) {
139
        synchronized (this) {
140
            if (repositories.size() > 100) {
141
                evictIdleRepos();
142
            }
143
            if (repositories.containsKey(name)) {
144
                // Move to the end of the list:
145
                Repository repo = repositories.remove(name);
146
                repositories.put(name, repo);
147
            } else {
148
                Repository repository = null;
149
                if (endpointType == null || endpointType.equals("rdf4j")) {
150
                    HTTPRepository hr = new HTTPRepository(endpointBase + "repositories/" + name);
151
                    hr.setHttpClient(httpclient);
152
                    repository = hr;
153
//                        } else if (endpointType.equals("virtuoso")) {
154
//                                repository = new VirtuosoRepository(endpointBase + name, username, password);
155
                } else {
156
                    throw new RuntimeException("Unknown repository type: " + endpointType);
157
                }
158
                repositories.put(name, repository);
159
                createRepo(name);
160
                getRepoConnection(name).close();
161
            }
162
            return repositories.get(name);
163
        }
164
    }
165

166
    /**
167
     * Return the repository connection for the given repository name.
168
     *
169
     * @param name repository name
170
     * @return repository connection
171
     */
172
    @GeneratedFlagForDependentElements
173
    public RepositoryConnection getRepoConnection(String name) {
174
        // The increment has to happen under the same monitor that guards eviction,
175
        // otherwise another thread could evict this repo in the window between
176
        // getRepository() returning and the counter going above zero. getConnection()
177
        // on HTTPRepository is local (it doesn't do HTTP), so holding the lock here
178
        // is cheap.
179
        synchronized (this) {
180
            Repository repo = getRepository(name);
181
            if (repo == null) {
182
                return null;
183
            }
184
            AtomicInteger counter = openConnections.computeIfAbsent(name, k -> new AtomicInteger());
×
185
            counter.incrementAndGet();
186
            try {
187
                return new CountingRepositoryConnection(repo, repo.getConnection(), counter);
188
            } catch (Throwable t) {
189
                // If getConnection() or the wrapper constructor throws after we bumped
190
                // the counter, decrement so the repo isn't pinned against eviction by
191
                // a phantom "active" connection it doesn't actually have.
192
                counter.decrementAndGet();
193
                throw t;
194
            }
195
        }
196
    }
197

198
    /**
199
     * Evicts the eldest cache entries until either the size is back within the
200
     * 100-entry cap or every remaining entry has at least one open connection.
201
     * The cap is load-bearing — each cached entry keeps an LMDB environment alive
202
     * on the RDF4J server (in-memory cache, mmap pages, native memory), so
203
     * exceeding it by much risks server-side OOM. Actively-used repos are skipped
204
     * rather than shut down, because {@link org.eclipse.rdf4j.repository.http.HTTPRepository#shutDown()}
205
     * closes the session manager and kills any live transaction on that repo with
206
     * a connection-close error (the {@code MMapIndexInput – Already closed}
207
     * failure mode observed on query-3 in the April test). The cache self-converges
208
     * as active repos become idle.
209
     */
210
    @GeneratedFlagForDependentElements
211
    private void evictIdleRepos() {
212
        List<String> skipped = new ArrayList<>();
213
        Iterator<Entry<String, Repository>> iter = repositories.entrySet().iterator();
214
        while (iter.hasNext() && repositories.size() > 100) {
215
            Entry<String, Repository> e = iter.next();
216
            AtomicInteger active = openConnections.get(e.getKey());
217
            if (active != null && active.get() > 0) {
218
                skipped.add(e.getKey());
219
                continue;
220
            }
221
            iter.remove();
222
            log.info("Shutting down repo: {}", e.getKey());
223
            e.getValue().shutDown();
224
            log.info("Shutdown complete");
225
        }
226
        if (!skipped.isEmpty()) {
227
            log.warn("Skipped eviction for {} active repo(s); cache size is now {} (cap 100). Active names: {}",
228
                    skipped.size(), repositories.size(), skipped);
229
        }
230
    }
231

232
    /**
233
     * Minimal wrapper around a delegate {@link RepositoryConnection} whose only job
234
     * is to decrement the per-repo open-connection counter exactly once when the
235
     * caller closes it. Uses {@link AtomicBoolean} so that repeated/idempotent
236
     * {@code close()} calls (common with try-with-resources plus explicit close)
237
     * don't decrement more than once.
238
     */
239
    private static final class CountingRepositoryConnection extends RepositoryConnectionWrapper {
240

241
        private final AtomicInteger counter;
242
        private final AtomicBoolean closed = new AtomicBoolean();
×
243

244
        CountingRepositoryConnection(Repository repo, RepositoryConnection delegate, AtomicInteger counter) {
245
            super(repo, delegate);
×
246
            this.counter = counter;
×
247
        }
×
248

249
        @Override
250
        public void close() {
251
            try {
252
                super.close();
×
253
            } finally {
254
                if (closed.compareAndSet(false, true)) {
×
255
                    counter.decrementAndGet();
×
256
                }
257
            }
258
        }
×
259

260
    }
261

262
    @GeneratedFlagForDependentElements
263
    private void createRepo(String repoName) {
264
        if (!repoName.equals(ADMIN_REPO)) {
265
            getRepository(ADMIN_REPO);  // make sure admin repo is loaded first
266
        }
267
        // Uses the shared this.httpclient rather than a per-call client so it inherits
268
        // the configured pool sizes (and, once change 1 of the fix plan lands, the
269
        // socket/connection-request timeouts).
270
        try {
271
            //log.info("Trying to creating repo " + name);
272

273
            // TODO new syntax somehow doesn't work for the Lucene case:
274

275
//                        String createRegularRepoQueryString = "@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#>.\n"
276
//                                        + "@prefix config: <tag:rdf4j.org,2023:config/>.\n"
277
//                                        + "[] a config:Repository ;\n"
278
//                                        + "    config:rep.id \"" + name + "\" ;\n"
279
//                                        + "    rdfs:label \"" + name + " native store\" ;\n"
280
//                                        + "    config:rep.impl [\n"
281
//                                        + "        config:rep.type \"openrdf:SailRepository\" ;\n"
282
//                                        + "        config:sail.impl [\n"
283
//                                        + "            config:sail.type \"openrdf:NativeStore\" ;\n"
284
//                                        + "            config:sail.iterationCacheSyncThreshold \"10000\";\n"
285
//                                        + "            config:native.tripleIndexes \"spoc,posc,ospc,opsc,psoc,sopc,spoc,cpos,cosp,cops,cpso,csop\" ;\n"
286
//                                        + "            config:sail.defaultQueryEvaluationMode \"STANDARD\"\n"
287
//                                        + "        ]\n"
288
//                                        + "    ].";
289
//                        String createTextRepoQueryString = "@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#>.\n"
290
//                                        + "@prefix config: <tag:rdf4j.org,2023:config/>.\n"
291
//                                        + "[] a config:Repository ;\n"
292
//                                        + "    config:rep.id \"" + name + "\" ;\n"
293
//                                        + "    rdfs:label \"" + name + " native store\" ;\n"
294
//                                        + "    config:rep.impl [\n"
295
//                                        + "        config:rep.type \"openrdf:SailRepository\" ;\n"
296
//                                        + "        config:sail.impl [\n"
297
//                                        + "            config:sail.type \"openrdf:LuceneSail\" ;\n"
298
//                                        + "            config:sail.lucene.indexDir \"index/\" ;\n"
299
//                                        + "            config:delegate [\n"
300
//                                        + "                config:rep.type \"openrdf:SailRepository\" ;\n"
301
//                                        + "                config:sail.impl [\n"
302
//                                        + "                    config:sail.type \"openrdf:NativeStore\" ;\n"
303
//                                        + "                    config:sail.iterationCacheSyncThreshold \"10000\";\n"
304
//                                        + "                    config:native.tripleIndexes \"spoc,posc,ospc,opsc,psoc,sopc,spoc,cpos,cosp,cops,cpso,csop\" ;\n"
305
//                                        + "                    config:sail.defaultQueryEvaluationMode \"STANDARD\"\n"
306
//                                        + "                ]\n"
307
//                                        + "            ]\n"
308
//                                        + "        ]\n"
309
//                                        + "    ].";
310

311
            String indexTypes = "spoc,posc,ospc,cspo,cpos,cosp";
312
            if (repoName.startsWith("meta") || repoName.startsWith("text")) {
313
                indexTypes = "spoc,posc,ospc";
314
            }
315

316
            String createRegularRepoQueryString =
317
                    "@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#>.\n" +
318
                    "@prefix rep: <http://www.openrdf.org/config/repository#>.\n" +
319
                    "@prefix sr: <http://www.openrdf.org/config/repository/sail#>.\n" +
320
                    "@prefix sail: <http://www.openrdf.org/config/sail#>.\n" +
321
                    "@prefix sail-luc: <http://www.openrdf.org/config/sail/lucene#>.\n" +
322
                    "@prefix lmdb: <http://rdf4j.org/config/sail/lmdb#>.\n" +
323
                    "@prefix sb: <http://www.openrdf.org/config/sail/base#>.\n" +
324
                    "\n" +
325
                    "[] a rep:Repository ;\n" +
326
                    "    rep:repositoryID \"" + repoName + "\" ;\n" +
327
                    "    rdfs:label \"" + repoName + " LMDB store\" ;\n" +
328
                    "    rep:repositoryImpl [\n" +
329
                    "        rep:repositoryType \"openrdf:SailRepository\" ;\n" +
330
                    "        sr:sailImpl [\n" +
331
                    "            sail:sailType \"rdf4j:LmdbStore\" ;\n" +
332
                    "            sail:iterationCacheSyncThreshold \"10000\";\n" +
333
                    "            lmdb:tripleIndexes \"" + indexTypes + "\" ;\n" +
334
                    "            sb:defaultQueryEvaluationMode \"STANDARD\"\n" +
335
                    "        ]\n"
336
                    + "    ].\n";
337

338
            // TODO Index npa:hasFilterLiteral predicate too (see https://groups.google.com/g/rdf4j-users/c/epF4Af1jXGU):
339
            String createTextRepoQueryString =
340
                    "@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#>.\n" +
341
                    "@prefix rep: <http://www.openrdf.org/config/repository#>.\n" +
342
                    "@prefix sr: <http://www.openrdf.org/config/repository/sail#>.\n" +
343
                    "@prefix sail: <http://www.openrdf.org/config/sail#>.\n" +
344
                    "@prefix sail-luc: <http://www.openrdf.org/config/sail/lucene#>.\n" +
345
                    "@prefix lmdb: <http://rdf4j.org/config/sail/lmdb#>.\n" +
346
                    "@prefix sb: <http://www.openrdf.org/config/sail/base#>.\n" +
347
                    "\n"
348
                    + "[] a rep:Repository ;\n" +
349
                    "    rep:repositoryID \"" + repoName + "\" ;\n" +
350
                    "    rdfs:label \"" + repoName + " store\" ;\n" +
351
                    "    rep:repositoryImpl [\n" +
352
                    "        rep:repositoryType \"openrdf:SailRepository\" ;\n" +
353
                    "        sr:sailImpl [\n" +
354
                    "            sail:sailType \"openrdf:LuceneSail\" ;\n" +
355
                    "            sail-luc:indexDir \"index/\" ;\n" +
356
                    "            sail-luc:transactional false ;\n" +
357
                    "            sail:delegate [\n" +
358
                    "              sail:sailType \"rdf4j:LmdbStore\" ;\n" +
359
                    "              sail:iterationCacheSyncThreshold \"10000\";\n" +
360
                    "              lmdb:tripleIndexes \"" + indexTypes + "\" ;\n" +
361
                    "              sb:defaultQueryEvaluationMode \"STANDARD\"\n" +
362
                    "            ]\n" +
363
                    "        ]\n" +
364
                    "    ].";
365

366
            String createRepoQueryString = createRegularRepoQueryString;
367
            if (repoName.startsWith("text")) {
368
                createRepoQueryString = createTextRepoQueryString;
369
            }
370

371
            HttpUriRequest createRepoRequest = RequestBuilder.put().setUri(endpointBase + "repositories/" + repoName).addHeader("Content-Type", "text/turtle").setEntity(new StringEntity(createRepoQueryString)).build();
372

373
            // Response is try-with-resources'd so the connection is released back to
374
            // the shared pool rather than leaked.
375
            try (CloseableHttpResponse response = httpclient.execute(createRepoRequest)) {
376
                int statusCode = response.getStatusLine().getStatusCode();
377
                if (statusCode == 409) {
378
                    //log.info("Already exists.");
379
                    getRepository(repoName).init();
380
                } else if (statusCode >= 200 && statusCode < 300) {
381
                    //log.info("Successfully created.");
382
                    initNewRepo(repoName);
383
                } else {
384
                    log.info("Status code: {}", response.getStatusLine().getStatusCode());
385
                    log.info(response.getStatusLine().getReasonPhrase());
386
                    String handledResponse = new BasicResponseHandler().handleResponse(response);
387
                    log.info("Response: {}", handledResponse);
388
                }
389
            }
390
        } catch (IOException ex) {
391
            log.info("Could not create repo.", ex);
392
        }
393
    }
394

395
    /**
396
     * Sends shutdown signal to all repositories.
397
     */
398
    @GeneratedFlagForDependentElements
399
    public void shutdownRepositories() {
400
        for (Repository repo : repositories.values()) {
401
            if (repo != null && repo.isInitialized()) {
402
                repo.shutDown();
403
            }
404
        }
405
    }
406

407
    /**
408
     * Returns admin repo connection.
409
     *
410
     * @return repository connection to admin repository
411
     */
412
    @GeneratedFlagForDependentElements
413
    public RepositoryConnection getAdminRepoConnection() {
414
        return get().getRepoConnection(ADMIN_REPO);
415
    }
416

417
    private Set<String> cachedRepositoryNames = Set.of();
9✔
418
    private boolean repoNamesCacheValid = false;
9✔
419
    private final ReadWriteLock repoNamesCacheLock = new ReentrantReadWriteLock();
15✔
420

421
    /**
422
     * Returns set of all repository names.
423
     *
424
     * @return Repository name set
425
     */
426
    public Set<String> getRepositoryNames() {
427
        // See if the repository names are cached:
428
        final var readLock = repoNamesCacheLock.readLock();
12✔
429
        try {
430
            readLock.lock();
6✔
431
            if (repoNamesCacheValid) {
9✔
432
                return cachedRepositoryNames;
15✔
433
            }
434
        } finally {
435
            readLock.unlock();
6✔
436
        }
437

438
        // Not cached, get from server:
439
        final var writeLock = repoNamesCacheLock.writeLock();
12✔
440
        try {
441
            writeLock.lock();
6✔
442
            // Check again if another thread has already updated the cache:
443
            if (repoNamesCacheValid) {
9!
444
                return cachedRepositoryNames;
×
445
            }
446
            Map<String, Boolean> repositoryNames = null;
6✔
447
            // Uses the shared this.httpclient; response try-with-resources releases
448
            // the pooled connection when done.
449
            try (CloseableHttpResponse resp = httpclient.execute(RequestBuilder.get()
24✔
450
                    .setUri(endpointBase + "/repositories")
9✔
451
                    .addHeader("Content-Type", "text/csv")
3✔
452
                    .build())) {
3✔
453
                BufferedReader reader = new BufferedReader(new InputStreamReader(resp.getEntity().getContent()));
30✔
454
                int code = resp.getStatusLine().getStatusCode();
12✔
455
                if (code < 200 || code >= 300) return null;
30!
456
                repositoryNames = new HashMap<>();
12✔
457
                int lineCount = 0;
6✔
458
                while (true) {
459
                    String line = reader.readLine();
9✔
460
                    if (line == null) break;
9✔
461
                    if (lineCount > 0) {
6✔
462
                        String repoName = line.split(",")[1];
18✔
463
                        repositoryNames.put(repoName, true);
18✔
464
                    }
465
                    lineCount = lineCount + 1;
12✔
466
                }
3✔
467
            } catch (IOException ex) {
15!
468
                log.info("Could not get repository names.", ex);
12✔
469
                return null;
12✔
470
            }
3✔
471
            cachedRepositoryNames = repositoryNames.keySet();
12✔
472
            repoNamesCacheValid = true;
9✔
473
            return cachedRepositoryNames;
15✔
474
        } finally {
475
            writeLock.unlock();
6✔
476
        }
477
    }
478

479
    /**
480
     * Invalidates the repository names cache. Call this method when a repository is created or deleted.
481
     */
482
    private void invalidateRepositoryNamesCache() {
483
        final var writeLock = repoNamesCacheLock.writeLock();
×
484
        try {
485
            writeLock.lock();
×
486
            repoNamesCacheValid = false;
×
487
        } finally {
488
            writeLock.unlock();
×
489
        }
490
    }
×
491

492
    @GeneratedFlagForDependentElements
493
    private void initNewRepo(String repoName) {
494
        String repoInitId = new Random().nextLong() + "";
495
        getRepository(repoName).init();
496
        if (!repoName.equals("empty")) {
497
            RepositoryConnection conn = getRepoConnection(repoName);
498
            try (conn) {
499
                // Full isolation, just in case.
500
                conn.begin(IsolationLevels.SERIALIZABLE);
501
                conn.add(NPA.THIS_REPO, NPA.HAS_REPO_INIT_ID, vf.createLiteral(repoInitId), NPA.GRAPH);
502
                if (tracksNanopubCountAndChecksum(repoName)) {
503
                    conn.add(NPA.THIS_REPO, NPA.HAS_NANOPUB_COUNT, vf.createLiteral(0L), NPA.GRAPH);
504
                    conn.add(NPA.THIS_REPO, NPA.HAS_NANOPUB_CHECKSUM, vf.createLiteral(NanopubUtils.INIT_CHECKSUM), NPA.GRAPH);
505
                }
506
                if (repoName.startsWith("pubkey_") || repoName.startsWith("type_")) {
507
                    String h = repoName.replaceFirst("^[^_]+_", "");
508
                    conn.add(NPA.THIS_REPO, NPA.HAS_COVERAGE_ITEM, Utils.getObjectForHash(h), NPA.GRAPH);
509
                    conn.add(NPA.THIS_REPO, NPA.HAS_COVERAGE_HASH, vf.createLiteral(h), NPA.GRAPH);
510
                    conn.add(NPA.THIS_REPO, NPA.HAS_COVERAGE_FILTER, vf.createLiteral("_" + repoName), NPA.GRAPH);
511
                }
512
                conn.commit();
513
            }
514
            // Refresh repository names cache
515
            invalidateRepositoryNamesCache();
516
        }
517
    }
518

519
    /**
520
     * Whether the given repo participates in the cumulative nanopub-count / XOR-checksum
521
     * tracking. Repos that don't produce their own content-addressed population
522
     * skip the {@code npa:hasNanopubCount} and {@code npa:hasNanopubChecksum}
523
     * initial triples — leaving them at {@code 0} and the empty-XOR placeholder
524
     * forever would just be misleading. Currently excluded:
525
     * <ul>
526
     *   <li>{@code admin} — holds metadata only.</li>
527
     *   <li>{@code last30d} — content expires on a periodic cleanup.</li>
528
     *   <li>{@code trust} — holds derived trust state, mirrored from the registry.</li>
529
     *   <li>{@code spaces} — holds space-relevant raw nanopubs (a filtered projection
530
     *       of {@code full}) plus extraction triples; its XOR checksum over loaded
531
     *       trusty URIs would diverge from {@code full}'s without adding value.</li>
532
     * </ul>
533
     */
534
    private static boolean tracksNanopubCountAndChecksum(String repoName) {
535
        return !repoName.equals(ADMIN_REPO)
×
536
                && !repoName.equals("last30d")
×
537
                && !repoName.equals("trust")
×
538
                && !repoName.equals("spaces");
×
539
    }
540

541
}
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