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

knowledgepixels / nanopub-query / 24664456478

20 Apr 2026 11:39AM UTC coverage: 59.905% (-0.3%) from 60.236%
24664456478

push

github

web-flow
Merge pull request #75 from knowledgepixels/fix/timeouts-backoff-breaker

fix/perf/feat: changes 1+7+8 — HTTP timeouts, exponential backoff, circuit breaker

293 of 544 branches covered (53.86%)

Branch coverage included in aggregate %.

841 of 1349 relevant lines covered (62.34%)

6.06 hits per line

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

81.87
src/main/java/com/knowledgepixels/query/NanopubLoader.java
1
package com.knowledgepixels.query;
2

3
import net.trustyuri.TrustyUriUtils;
4
import org.apache.http.client.HttpClient;
5
import org.apache.http.impl.client.HttpClientBuilder;
6
import org.eclipse.rdf4j.common.exception.RDF4JException;
7
import org.eclipse.rdf4j.common.transaction.IsolationLevels;
8
import org.eclipse.rdf4j.model.*;
9
import org.eclipse.rdf4j.model.impl.SimpleValueFactory;
10
import org.eclipse.rdf4j.model.vocabulary.DCTERMS;
11
import org.eclipse.rdf4j.model.vocabulary.RDFS;
12
import org.eclipse.rdf4j.query.BindingSet;
13
import org.eclipse.rdf4j.query.QueryLanguage;
14
import org.eclipse.rdf4j.query.TupleQuery;
15
import org.eclipse.rdf4j.query.TupleQueryResult;
16
import org.eclipse.rdf4j.repository.RepositoryConnection;
17
import org.nanopub.Nanopub;
18
import org.nanopub.NanopubUtils;
19
import org.nanopub.SimpleCreatorPattern;
20
import org.nanopub.SimpleTimestampPattern;
21
import org.nanopub.extra.security.KeyDeclaration;
22
import org.nanopub.extra.security.MalformedCryptoElementException;
23
import org.nanopub.extra.security.NanopubSignatureElement;
24
import org.nanopub.extra.security.SignatureUtils;
25
import org.nanopub.extra.server.GetNanopub;
26
import org.nanopub.extra.setting.IntroNanopub;
27
import org.nanopub.vocabulary.NP;
28
import org.nanopub.vocabulary.NPA;
29
import org.nanopub.vocabulary.NPX;
30
import org.nanopub.vocabulary.PAV;
31
import org.slf4j.Logger;
32
import org.slf4j.LoggerFactory;
33

34
import com.knowledgepixels.query.vocabulary.GEN;
35

36
import java.security.GeneralSecurityException;
37
import java.util.*;
38
import java.util.concurrent.ExecutionException;
39
import java.util.concurrent.Executors;
40
import java.util.concurrent.Future;
41
import java.util.concurrent.ThreadLocalRandom;
42
import java.util.concurrent.ThreadPoolExecutor;
43
import java.util.function.Consumer;
44

45
/**
46
 * Utility class for loading nanopublications into the database.
47
 */
48
public class NanopubLoader {
49

50
    private static HttpClient httpClient;
51
    private static final ThreadPoolExecutor loadingPool = (ThreadPoolExecutor) Executors.newFixedThreadPool(4);
8✔
52

53
    /**
54
     * Retry budget for the five (with #71 merged: six) structurally identical
55
     * retry loops in this file. Previously the shape was flat {@code 10 s × 30} —
56
     * five minutes of constant hammering at RDF4J that did not help a slow server.
57
     * The new shape is bounded exponential backoff with ±50 % jitter:
58
     * {@code base = 1, 2, 4, 8, 16, 32, 60, 60 s} for attempts 1…8, each perturbed
59
     * by up to half its base value. Jitter prevents the 4-thread loadingPool from
60
     * retrying in lock-step after a shared RDF4J failure (GC pause / overload spike).
61
     * Worst-case wall time per failing task drops from ~35 min (post-change-1
62
     * timeouts × 30 flat retries) to ~11 min (8 retries × 60 s timeout + backoff
63
     * sleeps). This is the figure that sets the circuit-breaker trip time in
64
     * {@link JellyNanopubLoader}.
65
     */
66
    private static final int MAX_RETRIES = 8;
67
    private static final long[] BACKOFF_BASE_MS =
70✔
68
            {1_000L, 2_000L, 4_000L, 8_000L, 16_000L, 32_000L, 60_000L, 60_000L};
69

70
    /**
71
     * Returns the sleep delay in ms for the given 1-indexed retry attempt. Delay
72
     * is {@link #BACKOFF_BASE_MS}{@code [attempt-1]} perturbed by ±50 % uniform
73
     * jitter, clamped to be non-negative.
74
     *
75
     * @param attempt 1-indexed retry attempt number
76
     * @return the computed sleep delay in ms
77
     */
78
    static long computeBackoffMillis(int attempt) {
79
        long base = BACKOFF_BASE_MS[Math.min(attempt - 1, BACKOFF_BASE_MS.length - 1)];
×
80
        long jitter = ThreadLocalRandom.current().nextLong(base + 1) - base / 2;
×
81
        return Math.max(0L, base + jitter);
×
82
    }
83
    private Nanopub np;
84
    private NanopubSignatureElement el = null;
6✔
85
    private List<Statement> metaStatements = new ArrayList<>();
10✔
86
    private List<Statement> nanopubStatements = new ArrayList<>();
10✔
87
    private List<Statement> literalStatements = new ArrayList<>();
10✔
88
    private List<Statement> invalidateStatements = new ArrayList<>();
10✔
89
    private List<Statement> textStatements, allStatements;
90
    private Calendar timestamp = null;
6✔
91
    private Statement pubkeyStatement, pubkeyStatementX;
92
    private List<String> notes = new ArrayList<>();
10✔
93
    private boolean aborted = false;
6✔
94
    private static final Logger log = LoggerFactory.getLogger(NanopubLoader.class);
6✔
95

96

97
    NanopubLoader(Nanopub np, long counter) {
4✔
98
        this.np = np;
6✔
99
        if (counter >= 0) {
8✔
100
            log.info("Loading {}: {}", counter, np.getUri());
16✔
101
        } else {
102
            log.info("Loading: {}", np.getUri());
10✔
103
        }
104

105
        // TODO Ensure proper synchronization and DB rollbacks
106

107
        // TODO Check for null characters ("\0"), which can cause problems in Virtuoso.
108

109
        String ac = TrustyUriUtils.getArtifactCode(np.getUri().toString());
10✔
110
        if (!np.getHeadUri().toString().contains(ac) || !np.getAssertionUri().toString().contains(ac) || !np.getProvenanceUri().toString().contains(ac) || !np.getPubinfoUri().toString().contains(ac)) {
48!
111
            notes.add("could not load nanopub as not all graphs contained the artifact code");
×
112
            aborted = true;
×
113
            return;
×
114
        }
115

116
        try {
117
            el = SignatureUtils.getSignatureElement(np);
8✔
118
        } catch (MalformedCryptoElementException ex) {
×
119
            notes.add("Signature error");
×
120
        }
2✔
121
        if (!hasValidSignature(el)) {
8✔
122
            aborted = true;
6✔
123
            return;
2✔
124
        }
125

126
        pubkeyStatement = vf.createStatement(np.getUri(), NPA.HAS_VALID_SIGNATURE_FOR_PUBLIC_KEY, vf.createLiteral(el.getPublicKeyString()), NPA.GRAPH);
26✔
127
        // @ADMIN-TRIPLE-TABLE@ NANOPUB, npa:hasValidSignatureForPublicKey, FULL_PUBKEY, npa:graph, meta, full pubkey if signature is valid
128
        metaStatements.add(pubkeyStatement);
12✔
129
        pubkeyStatementX = vf.createStatement(np.getUri(), NPA.HAS_VALID_SIGNATURE_FOR_PUBLIC_KEY_HASH, vf.createLiteral(Utils.createHash(el.getPublicKeyString())), NPA.GRAPH);
28✔
130
        // @ADMIN-TRIPLE-TABLE@ NANOPUB, npa:hasValidSignatureForPublicKeyHash, PUBKEY_HASH, npa:graph, meta, hex-encoded SHA256 hash if signature is valid
131
        metaStatements.add(pubkeyStatementX);
12✔
132

133
        if (el.getSigners().size() == 1) {  // > 1 is deprecated
12!
134
            metaStatements.add(vf.createStatement(np.getUri(), NPX.SIGNED_BY, el.getSigners().iterator().next(), NPA.GRAPH));
32✔
135
            // @ADMIN-TRIPLE-TABLE@ NANOPUB, npx:signedBy, SIGNER, npa:graph, meta, ID of signer
136
        }
137

138
        Set<IRI> subIris = new HashSet<>();
8✔
139
        Set<IRI> otherNps = new HashSet<>();
8✔
140
        Set<IRI> invalidated = new HashSet<>();
8✔
141
        Set<IRI> retracted = new HashSet<>();
8✔
142
        Set<IRI> superseded = new HashSet<>();
8✔
143
        String combinedLiterals = "";
4✔
144
        for (Statement st : NanopubUtils.getStatements(np)) {
22✔
145
            nanopubStatements.add(st);
10✔
146

147
            if (st.getPredicate().toString().contains(ac)) {
12!
148
                subIris.add(st.getPredicate());
×
149
            } else {
150
                IRI b = getBaseTrustyUri(st.getPredicate());
8✔
151
                if (b != null) otherNps.add(b);
4!
152
            }
153
            if (st.getPredicate().equals(NPX.RETRACTS) && st.getObject() instanceof IRI) {
10!
154
                retracted.add((IRI) st.getObject());
×
155
            }
156
            if (st.getPredicate().equals(NPX.INVALIDATES) && st.getObject() instanceof IRI) {
10!
157
                invalidated.add((IRI) st.getObject());
×
158
            }
159
            if (st.getSubject().equals(np.getUri()) && st.getObject() instanceof IRI) {
20✔
160
                if (st.getPredicate().equals(NPX.SUPERSEDES)) {
10✔
161
                    superseded.add((IRI) st.getObject());
12✔
162
                }
163
                if (st.getObject().toString().matches(".*[^A-Za-z0-9\\-_]RA[A-Za-z0-9\\-_]{43}")) {
12✔
164
                    metaStatements.add(vf.createStatement(np.getUri(), st.getPredicate(), st.getObject(), NPA.NETWORK_GRAPH));
26✔
165
                    // @ADMIN-TRIPLE-TABLE@ NANOPUB1, RELATION, NANOPUB2, npa:networkGraph, meta, any inter-nanopub relation found in NANOPUB1
166
                }
167
                if (st.getContext().equals(np.getPubinfoUri())) {
12✔
168
                    if (st.getPredicate().equals(NPX.INTRODUCES) || st.getPredicate().equals(NPX.DESCRIBES) || st.getPredicate().equals(NPX.EMBEDS)) {
30!
169
                        metaStatements.add(vf.createStatement(np.getUri(), st.getPredicate(), st.getObject(), NPA.GRAPH));
26✔
170
                        // @ADMIN-TRIPLE-TABLE@ NANOPUB, npx:introduces, THING, npa:graph, meta, when such a triple is present in pubinfo of NANOPUB
171
                        // @ADMIN-TRIPLE-TABLE@ NANOPUB, npx:describes, THING, npa:graph, meta, when such a triple is present in pubinfo of NANOPUB
172
                        // @ADMIN-TRIPLE-TABLE@ NANOPUB, npx:embeds, THING, npa:graph, meta, when such a triple is present in pubinfo of NANOPUB
173
                    }
174
                }
175
            }
176
            if (st.getSubject().toString().contains(ac)) {
12✔
177
                subIris.add((IRI) st.getSubject());
14✔
178
            } else {
179
                IRI b = getBaseTrustyUri(st.getSubject());
8✔
180
                if (b != null) otherNps.add(b);
4!
181
            }
182
            if (st.getObject() instanceof IRI) {
8✔
183
                if (st.getObject().toString().contains(ac)) {
12✔
184
                    subIris.add((IRI) st.getObject());
14✔
185
                } else {
186
                    IRI b = getBaseTrustyUri(st.getObject());
8✔
187
                    if (b != null) otherNps.add(b);
12✔
188
                }
2✔
189
            } else {
190
                combinedLiterals += st.getObject().stringValue().replaceAll("\\s+", " ") + "\n";
18✔
191
//                                if (st.getSubject().equals(np.getUri()) && !st.getSubject().equals(HAS_FILTER_LITERAL)) {
192
//                                        literalStatements.add(vf.createStatement(np.getUri(), st.getPredicate(), st.getObject(), LITERAL_GRAPH));
193
//                                } else {
194
//                                        literalStatements.add(vf.createStatement(np.getUri(), HAS_LITERAL, st.getObject(), LITERAL_GRAPH));
195
//                                }
196
            }
197
        }
2✔
198
        subIris.remove(np.getUri());
10✔
199
        subIris.remove(np.getAssertionUri());
10✔
200
        subIris.remove(np.getProvenanceUri());
10✔
201
        subIris.remove(np.getPubinfoUri());
10✔
202
        for (IRI i : subIris) {
20✔
203
            metaStatements.add(vf.createStatement(np.getUri(), NPA.HAS_SUB_IRI, i, NPA.GRAPH));
22✔
204
            // @ADMIN-TRIPLE-TABLE@ NANOPUB, npa:hasSubIri, SUB_IRI, npa:graph, meta, for any IRI minted in the namespace of the NANOPUB
205
        }
2✔
206
        for (IRI i : otherNps) {
20✔
207
            metaStatements.add(vf.createStatement(np.getUri(), NPA.REFERS_TO_NANOPUB, i, NPA.NETWORK_GRAPH));
22✔
208
            // @ADMIN-TRIPLE-TABLE@ NANOPUB1, npa:refersToNanopub, NANOPUB2, npa:networkGraph, meta, generic inter-nanopub relation
209
        }
2✔
210
        for (IRI i : invalidated) {
12!
211
            invalidateStatements.add(vf.createStatement(np.getUri(), NPX.INVALIDATES, i, NPA.GRAPH));
×
212
            // @ADMIN-TRIPLE-TABLE@ NANOPUB, npx:invalidates, INVALIDATED_NANOPUB, npa:graph, meta, if the NANOPUB retracts or supersedes another nanopub
213
        }
×
214
        for (IRI i : retracted) {
12!
215
            invalidateStatements.add(vf.createStatement(np.getUri(), NPX.INVALIDATES, i, NPA.GRAPH));
×
216
            metaStatements.add(vf.createStatement(np.getUri(), NPX.RETRACTS, i, NPA.GRAPH));
×
217
            // @ADMIN-TRIPLE-TABLE@ NANOPUB, npx:retracts, RETRACTED_NANOPUB, npa:graph, meta, if the NANOPUB retracts another nanopub
218
        }
×
219
        for (IRI i : superseded) {
20✔
220
            invalidateStatements.add(vf.createStatement(np.getUri(), NPX.INVALIDATES, i, NPA.GRAPH));
22✔
221
            metaStatements.add(vf.createStatement(np.getUri(), NPX.SUPERSEDES, i, NPA.GRAPH));
22✔
222
            // @ADMIN-TRIPLE-TABLE@ NANOPUB, npx:supersedes, SUPERSEDED_NANOPUB, npa:graph, meta, if the NANOPUB supersedes another nanopub
223
        }
2✔
224

225
        metaStatements.add(vf.createStatement(np.getUri(), NPA.HAS_HEAD_GRAPH, np.getHeadUri(), NPA.GRAPH));
24✔
226
        // @ADMIN-TRIPLE-TABLE@ NANOPUB, npa:hasHeadGraph, HEAD_GRAPH, npa:graph, meta, direct link to the head graph of the NANOPUB
227
        metaStatements.add(vf.createStatement(np.getUri(), NPA.HAS_GRAPH, np.getHeadUri(), NPA.GRAPH));
24✔
228
        // @ADMIN-TRIPLE-TABLE@ NANOPUB, npa:hasGraph, GRAPH, npa:graph, meta, generic link to all four graphs of the given NANOPUB
229
        metaStatements.add(vf.createStatement(np.getUri(), NP.HAS_ASSERTION, np.getAssertionUri(), NPA.GRAPH));
24✔
230
        // @ADMIN-TRIPLE-TABLE@ NANOPUB, np:hasAssertion, ASSERTION_GRAPH, npa:graph, meta, direct link to the assertion graph of the NANOPUB
231
        metaStatements.add(vf.createStatement(np.getUri(), NPA.HAS_GRAPH, np.getAssertionUri(), NPA.GRAPH));
24✔
232
        metaStatements.add(vf.createStatement(np.getUri(), NP.HAS_PROVENANCE, np.getProvenanceUri(), NPA.GRAPH));
24✔
233
        // @ADMIN-TRIPLE-TABLE@ NANOPUB, np:hasProvenance, PROVENANCE_GRAPH, npa:graph, meta, direct link to the provenance graph of the NANOPUB
234
        metaStatements.add(vf.createStatement(np.getUri(), NPA.HAS_GRAPH, np.getProvenanceUri(), NPA.GRAPH));
24✔
235
        metaStatements.add(vf.createStatement(np.getUri(), NP.HAS_PUBINFO, np.getPubinfoUri(), NPA.GRAPH));
24✔
236
        // @ADMIN-TRIPLE-TABLE@ NANOPUB, np:hasPublicationInfo, PUBINFO_GRAPH, npa:graph, meta, direct link to the pubinfo graph of the NANOPUB
237
        metaStatements.add(vf.createStatement(np.getUri(), NPA.HAS_GRAPH, np.getPubinfoUri(), NPA.GRAPH));
24✔
238

239
        String artifactCode = TrustyUriUtils.getArtifactCode(np.getUri().stringValue());
10✔
240
        metaStatements.add(vf.createStatement(np.getUri(), NPA.ARTIFACT_CODE, vf.createLiteral(artifactCode), NPA.GRAPH));
26✔
241
        // @ADMIN-TRIPLE-TABLE@ NANOPUB, npa:artifactCode, ARTIFACT_CODE, npa:graph, meta, artifact code starting with 'RA...'
242

243
        if (isIntroNanopub(np)) {
6✔
244
            IntroNanopub introNp = new IntroNanopub(np);
10✔
245
            metaStatements.add(vf.createStatement(np.getUri(), NPA.IS_INTRODUCTION_OF, introNp.getUser(), NPA.GRAPH));
24✔
246
            // @ADMIN-TRIPLE-TABLE@ NANOPUB, npa:isIntroductionOf, AGENT, npa:graph, meta, linking intro nanopub to the agent it is introducing
247
            for (KeyDeclaration kc : introNp.getKeyDeclarations()) {
22✔
248
                metaStatements.add(vf.createStatement(np.getUri(), NPA.DECLARES_PUBKEY, vf.createLiteral(kc.getPublicKeyString()), NPA.GRAPH));
28✔
249
                // @ADMIN-TRIPLE-TABLE@ NANOPUB, npa:declaresPubkey, FULL_PUBKEY, npa:graph, meta, full pubkey declared by the given intro NANOPUB
250
            }
2✔
251
        }
252

253
        try {
254
            timestamp = SimpleTimestampPattern.getCreationTime(np);
8✔
255
        } catch (IllegalArgumentException ex) {
×
256
            notes.add("Illegal date/time");
×
257
        }
2✔
258
        if (timestamp != null) {
6!
259
            metaStatements.add(vf.createStatement(np.getUri(), DCTERMS.CREATED, vf.createLiteral(timestamp.getTime()), NPA.GRAPH));
30✔
260
            // @ADMIN-TRIPLE-TABLE@ NANOPUB, dct:created, CREATION_DATE, npa:graph, meta, normalized creation timestamp
261
        }
262

263
        String literalFilter = "_pubkey_" + Utils.createHash(el.getPublicKeyString());
12✔
264
        for (IRI typeIri : NanopubUtils.getTypes(np)) {
22✔
265
            metaStatements.add(vf.createStatement(np.getUri(), NPX.HAS_NANOPUB_TYPE, typeIri, NPA.GRAPH));
22✔
266
            // @ADMIN-TRIPLE-TABLE@ NANOPUB, npx:hasNanopubType, NANOPUB_TYPE, npa:graph, meta, type of NANOPUB
267
            literalFilter += " _type_" + Utils.createHash(typeIri);
10✔
268
        }
2✔
269
        // Side-effecting call: populates SpaceRegistry as gen:Space-typed nanopubs flow through.
270
        // Consumers of the registry (extraction, materialization) land in later steps of #62.
271
        detectAndRegisterSpaces(np);
6✔
272
        String label = NanopubUtils.getLabel(np);
6✔
273
        if (label != null) {
4!
274
            metaStatements.add(vf.createStatement(np.getUri(), RDFS.LABEL, vf.createLiteral(label), NPA.GRAPH));
26✔
275
            // @ADMIN-TRIPLE-TABLE@ NANOPUB, rdfs:label, LABEL, npa:graph, meta, label of NANOPUB
276
        }
277
        String description = NanopubUtils.getDescription(np);
6✔
278
        if (description != null) {
4✔
279
            metaStatements.add(vf.createStatement(np.getUri(), DCTERMS.DESCRIPTION, vf.createLiteral(description), NPA.GRAPH));
26✔
280
            // @ADMIN-TRIPLE-TABLE@ NANOPUB, dct:description, LABEL, npa:graph, meta, description of NANOPUB
281
        }
282
        for (IRI creatorIri : SimpleCreatorPattern.getCreators(np)) {
22✔
283
            metaStatements.add(vf.createStatement(np.getUri(), DCTERMS.CREATOR, creatorIri, NPA.GRAPH));
22✔
284
            // @ADMIN-TRIPLE-TABLE@ NANOPUB, dct:creator, CREATOR, npa:graph, meta, creator of NANOPUB (can be several)
285
        }
2✔
286
        for (IRI authorIri : SimpleCreatorPattern.getAuthors(np)) {
14!
287
            metaStatements.add(vf.createStatement(np.getUri(), PAV.AUTHORED_BY, authorIri, NPA.GRAPH));
×
288
            // @ADMIN-TRIPLE-TABLE@ NANOPUB, pav:authoredBy, AUTHOR, npa:graph, meta, author of NANOPUB (can be several)
289
        }
×
290

291
        if (!combinedLiterals.isEmpty()) {
6!
292
            literalStatements.add(vf.createStatement(np.getUri(), NPA.HAS_FILTER_LITERAL, vf.createLiteral(literalFilter + "\n" + combinedLiterals), NPA.GRAPH));
30✔
293
            // @ADMIN-TRIPLE-TABLE@ NANOPUB, npa:hasFilterLiteral, FILTER_LITERAL, npa:graph, literal, auxiliary literal for filtering by type and pubkey in text repo
294
        }
295

296
        // Any statements that express that the currently processed nanopub is already invalidated:
297
        List<Statement> invalidatingStatements = getInvalidatingStatements(np.getUri());
8✔
298

299
        metaStatements.addAll(invalidateStatements);
12✔
300

301
        allStatements = new ArrayList<>(nanopubStatements);
14✔
302
        allStatements.addAll(metaStatements);
12✔
303
        allStatements.addAll(invalidatingStatements);
10✔
304

305
        textStatements = new ArrayList<>(literalStatements);
14✔
306
        textStatements.addAll(metaStatements);
12✔
307
        textStatements.addAll(invalidatingStatements);
10✔
308
    }
2✔
309

310
    /**
311
     * Get the HTTP client used for fetching nanopublications.
312
     *
313
     * @return the HTTP client
314
     */
315
    static HttpClient getHttpClient() {
316
        if (httpClient == null) {
4✔
317
            httpClient = HttpClientBuilder.create().setDefaultRequestConfig(Utils.getHttpRequestConfig()).build();
10✔
318
        }
319
        return httpClient;
4✔
320
    }
321

322
    /**
323
     * Load the given nanopublication into the database.
324
     *
325
     * @param nanopubUri Nanopublication identifier (URI)
326
     */
327
    public static void load(String nanopubUri) {
328
        if (isNanopubLoaded(nanopubUri)) {
6!
329
            log.info("Already loaded: {}", nanopubUri);
×
330
        } else {
331
            Nanopub np = GetNanopub.get(nanopubUri, getHttpClient());
8✔
332
            load(np, -1);
6✔
333
        }
334
    }
2✔
335

336
    /**
337
     * Load a nanopub into the database.
338
     *
339
     * @param np      the nanopub to load
340
     * @param counter the load counter, only used for logging (or -1 if not known)
341
     * @throws RDF4JException if the loading fails
342
     */
343
    public static void load(Nanopub np, long counter) throws RDF4JException {
344
        NanopubLoader loader = new NanopubLoader(np, counter);
12✔
345
        loader.executeLoading();
4✔
346
    }
2✔
347

348
    @GeneratedFlagForDependentElements
349
    private void executeLoading() {
350
        var runningTasks = new ArrayList<Future<?>>();
351
        Consumer<Runnable> runTask = t -> runningTasks.add(loadingPool.submit(t));
×
352

353
        for (String note : notes) {
354
            loadNoteToRepo(np.getUri(), note);
355
        }
356

357
        if (!aborted) {
358
            // Submit all tasks except the "meta" task
359
            if (timestamp != null) {
360
                if (new Date().getTime() - timestamp.getTimeInMillis() < THIRTY_DAYS) {
361
                    runTask.accept(() -> loadNanopubToLatest(allStatements));
×
362
                }
363
            }
364

365
            runTask.accept(() -> loadNanopubToRepo(np.getUri(), textStatements, "text"));
×
366
            runTask.accept(() -> loadNanopubToRepo(np.getUri(), allStatements, "full"));
×
367
            // Note: "meta" task is deferred until all other tasks complete successfully
368

369
            runTask.accept(() -> loadNanopubToRepo(np.getUri(), allStatements, "pubkey_" + Utils.createHash(el.getPublicKeyString())));
×
370
            //                loadNanopubToRepo(np.getUri(), textStatements, "text-pubkey_" + Utils.createHash(el.getPublicKeyString()));
371
            for (IRI typeIri : NanopubUtils.getTypes(np)) {
372
                // Exclude locally minted IRIs:
373
                if (typeIri.stringValue().startsWith(np.getUri().stringValue())) continue;
374
                if (!typeIri.stringValue().matches("https?://.*")) continue;
375
                runTask.accept(() -> loadNanopubToRepo(np.getUri(), allStatements, "type_" + Utils.createHash(typeIri)));
×
376
                //                        loadNanopubToRepo(np.getUri(), textStatements, "text-type_" + Utils.createHash(typeIri));
377
            }
378
            //                for (IRI creatorIri : SimpleCreatorPattern.getCreators(np)) {
379
            //                        // Exclude locally minted IRIs:
380
            //                        if (creatorIri.stringValue().startsWith(np.getUri().stringValue())) continue;
381
            //                        if (!creatorIri.stringValue().matches("https?://.*")) continue;
382
            //                        loadNanopubToRepo(np.getUri(), allStatements, "user_" + Utils.createHash(creatorIri));
383
            //                        loadNanopubToRepo(np.getUri(), textStatements, "text-user_" + Utils.createHash(creatorIri));
384
            //                }
385
            //                for (IRI authorIri : SimpleCreatorPattern.getAuthors(np)) {
386
            //                        // Exclude locally minted IRIs:
387
            //                        if (authorIri.stringValue().startsWith(np.getUri().stringValue())) continue;
388
            //                        if (!authorIri.stringValue().matches("https?://.*")) continue;
389
            //                        loadNanopubToRepo(np.getUri(), allStatements, "user_" + Utils.createHash(authorIri));
390
            //                        loadNanopubToRepo(np.getUri(), textStatements, "text-user_" + Utils.createHash(authorIri));
391
            //                }
392

393
            for (Statement st : invalidateStatements) {
394
                runTask.accept(() -> loadInvalidateStatements(np, el.getPublicKeyString(), st, pubkeyStatement, pubkeyStatementX));
×
395
            }
396

397
            // Wait for all non-meta tasks to complete successfully before submitting the meta task
398
            for (var task : runningTasks) {
399
                try {
400
                    task.get();
401
                } catch (ExecutionException | InterruptedException ex) {
402
                    throw new RuntimeException("Error in nanopub loading thread", ex.getCause());
403
                }
404
            }
405

406
            // Now submit and wait for the "meta" task after all other tasks have completed successfully
407
            Future<?> metaTask = loadingPool.submit(() -> loadNanopubToRepo(np.getUri(), metaStatements, "meta"));
×
408
            try {
409
                metaTask.get();
410
            } catch (ExecutionException | InterruptedException ex) {
411
                throw new RuntimeException("Error in nanopub loading thread (meta task)", ex.getCause());
412
            }
413
        }
414
    }
415

416
    private static Long lastUpdateOfLatestRepo = null;
4✔
417
    private static long THIRTY_DAYS = 1000L * 60 * 60 * 24 * 30;
4✔
418
    private static long ONE_HOUR = 1000L * 60 * 60;
4✔
419

420
    @GeneratedFlagForDependentElements
421
    private static void loadNanopubToLatest(List<Statement> statements) {
422
        boolean success = false;
423
        int retries = 0;
424
        while (!success) {
425
            RepositoryConnection conn = TripleStore.get().getRepoConnection("last30d");
426
            try (conn) {
427
                // Read committed, because deleting old nanopubs is idempotent. Inserts do not collide
428
                // with deletes, because we are not inserting old nanopubs.
429
                conn.begin(IsolationLevels.READ_COMMITTED);
430
                conn.add(statements);
431
                if (lastUpdateOfLatestRepo == null || new Date().getTime() - lastUpdateOfLatestRepo > ONE_HOUR) {
432
                    log.trace("Remove old nanopubs...");
433
                    Literal thirtyDaysAgo = vf.createLiteral(new Date(new Date().getTime() - THIRTY_DAYS));
434
                    TupleQuery q = conn.prepareTupleQuery(QueryLanguage.SPARQL, "SELECT * { graph <" + NPA.GRAPH + "> { " + "?np <" + DCTERMS.CREATED + "> ?date . " + "filter ( ?date < ?thirtydaysago ) " + "} }");
435
                    q.setBinding("thirtydaysago", thirtyDaysAgo);
436
                    try (TupleQueryResult r = q.evaluate()) {
437
                        while (r.hasNext()) {
438
                            BindingSet b = r.next();
439
                            IRI oldNpId = (IRI) b.getBinding("np").getValue();
440
                            log.trace("Remove old nanopub: {}", oldNpId);
441
                            for (Value v : Utils.getObjectsForPattern(conn, NPA.GRAPH, oldNpId, NPA.HAS_GRAPH)) {
442
                                // Remove all four nanopub graphs:
443
                                conn.remove((Resource) null, (IRI) null, (Value) null, (IRI) v);
444
                            }
445
                            // Remove nanopubs in admin graphs:
446
                            conn.remove(oldNpId, null, null, NPA.GRAPH);
447
                            conn.remove(oldNpId, null, null, NPA.NETWORK_GRAPH);
448
                        }
449
                    }
450
                    lastUpdateOfLatestRepo = new Date().getTime();
451
                }
452
                conn.commit();
453
                success = true;
454
            } catch (Exception ex) {
455
                log.warn("Could not load nanopub to last30d repo.", ex);
456
                if (conn.isActive()) conn.rollback();
457
            }
458
            if (!success) {
459
                retries++;
460
                if (retries >= MAX_RETRIES) {
461
                    throw new RuntimeException("Failed to load nanopub to last30d repo after " + MAX_RETRIES + " retries");
462
                }
463
                long delay = computeBackoffMillis(retries);
464
                log.info("Retrying in {} ms (attempt {}/{})...", delay, retries, MAX_RETRIES);
465
                try {
466
                    Thread.sleep(delay);
467
                } catch (InterruptedException x) {
468
                    Thread.currentThread().interrupt();
469
                }
470
            }
471
        }
472
    }
473

474
    @GeneratedFlagForDependentElements
475
    private static void loadNanopubToRepo(IRI npId, List<Statement> statements, String repoName) {
476
        boolean success = false;
477
        int retries = 0;
478
        while (!success) {
479
            RepositoryConnection conn = TripleStore.get().getRepoConnection(repoName);
480
            try (conn) {
481
                // Serializable, because write skew would cause the chain of hashes to be broken.
482
                // The inserts must be done serially.
483
                conn.begin(IsolationLevels.SERIALIZABLE);
484
                var repoStatus = fetchRepoStatus(conn, npId);
485
                if (repoStatus.isLoaded) {
486
                    log.info("Already loaded: {}", npId);
487
                } else {
488
                    String newChecksum = NanopubUtils.updateXorChecksum(npId, repoStatus.checksum);
489
                    conn.remove(NPA.THIS_REPO, NPA.HAS_NANOPUB_COUNT, null, NPA.GRAPH);
490
                    conn.remove(NPA.THIS_REPO, NPA.HAS_NANOPUB_CHECKSUM, null, NPA.GRAPH);
491
                    conn.add(NPA.THIS_REPO, NPA.HAS_NANOPUB_COUNT, vf.createLiteral(repoStatus.count + 1), NPA.GRAPH);
492
                    // @ADMIN-TRIPLE-TABLE@ REPO, npa:hasNanopubCount, NANOPUB_COUNT, npa:graph, admin, number of nanopubs loaded
493
                    conn.add(NPA.THIS_REPO, NPA.HAS_NANOPUB_CHECKSUM, vf.createLiteral(newChecksum), NPA.GRAPH);
494
                    // @ADMIN-TRIPLE-TABLE@ REPO, npa:hasNanopubChecksum, NANOPUB_CHECKSUM, npa:graph, admin, checksum of all loaded nanopubs (order-independent XOR checksum on trusty URIs in Base64 notation)
495
                    conn.add(npId, NPA.HAS_LOAD_NUMBER, vf.createLiteral(repoStatus.count), NPA.GRAPH);
496
                    // @ADMIN-TRIPLE-TABLE@ NANOPUB, npa:hasLoadNumber, LOAD_NUMBER, npa:graph, admin, the sequential number at which this NANOPUB was loaded
497
                    conn.add(npId, NPA.HAS_LOAD_CHECKSUM, vf.createLiteral(newChecksum), NPA.GRAPH);
498
                    // @ADMIN-TRIPLE-TABLE@ NANOPUB, npa:hasLoadChecksum, LOAD_CHECKSUM, npa:graph, admin, the checksum of all loaded nanopubs after loading the given NANOPUB
499
                    conn.add(npId, NPA.HAS_LOAD_TIMESTAMP, vf.createLiteral(new Date()), NPA.GRAPH);
500
                    // @ADMIN-TRIPLE-TABLE@ NANOPUB, npa:hasLoadTimestamp, LOAD_TIMESTAMP, npa:graph, admin, the time point at which this NANOPUB was loaded
501
                    conn.add(statements);
502
                }
503
                conn.commit();
504
                success = true;
505
            } catch (Exception ex) {
506
                log.warn("Could not load nanopub to repo.", ex);
507
                if (conn.isActive()) conn.rollback();
508
            }
509
            if (!success) {
510
                retries++;
511
                if (retries >= MAX_RETRIES) {
512
                    throw new RuntimeException("Failed to load nanopub " + npId + " to repo " + repoName + " after " + MAX_RETRIES + " retries");
513
                }
514
                long delay = computeBackoffMillis(retries);
515
                log.info("Retrying in {} ms (attempt {}/{})...", delay, retries, MAX_RETRIES);
516
                try {
517
                    Thread.sleep(delay);
518
                } catch (InterruptedException x) {
519
                    Thread.currentThread().interrupt();
520
                }
521
            }
522
        }
523
    }
524

525
    private record RepoStatus(boolean isLoaded, long count, String checksum) {
×
526
    }
527

528
    /**
529
     * To execute before loading a nanopub: check if the nanopub is already loaded and what is the
530
     * current load counter and checksum. This effectively batches three queries into one.
531
     * This method must be called from within a transaction.
532
     *
533
     * @param conn repo connection
534
     * @param npId nanopub ID
535
     * @return the current status
536
     */
537
    @GeneratedFlagForDependentElements
538
    private static RepoStatus fetchRepoStatus(RepositoryConnection conn, IRI npId) {
539
        var result = conn.prepareTupleQuery(QueryLanguage.SPARQL, REPO_STATUS_QUERY_TEMPLATE.formatted(npId)).evaluate();
540
        try (result) {
541
            if (!result.hasNext()) {
542
                // This may happen if the repo was created, but is completely empty.
543
                return new RepoStatus(false, 0, NanopubUtils.INIT_CHECKSUM);
544
            }
545
            var row = result.next();
546
            return new RepoStatus(row.hasBinding("loadNumber"), Long.parseLong(row.getBinding("count").getValue().stringValue()), row.getBinding("checksum").getValue().stringValue());
547
        }
548
    }
549

550
    @GeneratedFlagForDependentElements
551
    private static void loadInvalidateStatements(Nanopub thisNp, String thisPubkey, Statement invalidateStatement, Statement pubkeyStatement, Statement pubkeyStatementX) {
552
        boolean success = false;
553
        int retries = 0;
554
        while (!success) {
555
            List<RepositoryConnection> connections = new ArrayList<>();
556
            RepositoryConnection metaConn = TripleStore.get().getRepoConnection("meta");
557
            try {
558
                IRI invalidatedNpId = (IRI) invalidateStatement.getObject();
559
                // Basic isolation because here we only read append-only data.
560
                metaConn.begin(IsolationLevels.READ_COMMITTED);
561

562
                Value pubkeyValue = Utils.getObjectForPattern(metaConn, NPA.GRAPH, invalidatedNpId, NPA.HAS_VALID_SIGNATURE_FOR_PUBLIC_KEY);
563
                if (pubkeyValue != null) {
564
                    String pubkey = pubkeyValue.stringValue();
565

566
                    if (!pubkey.equals(thisPubkey)) {
567
                        //log.info("Adding invalidation expressed in " + thisNp.getUri() + " also to repo for pubkey " + pubkey);
568
                        connections.add(loadStatements("pubkey_" + Utils.createHash(pubkey), invalidateStatement, pubkeyStatement, pubkeyStatementX));
569
//                                                connections.add(loadStatements("text-pubkey_" + Utils.createHash(pubkey), invalidateStatement, pubkeyStatement));
570
                    }
571

572
                    for (Value v : Utils.getObjectsForPattern(metaConn, NPA.GRAPH, invalidatedNpId, NPX.HAS_NANOPUB_TYPE)) {
573
                        IRI typeIri = (IRI) v;
574
                        // TODO Avoid calling getTypes and getCreators multiple times:
575
                        if (!NanopubUtils.getTypes(thisNp).contains(typeIri)) {
576
                            //log.info("Adding invalidation expressed in " + thisNp.getUri() + " also to repo for type " + typeIri);
577
                            connections.add(loadStatements("type_" + Utils.createHash(typeIri), invalidateStatement, pubkeyStatement, pubkeyStatementX));
578
//                                                        connections.add(loadStatements("text-type_" + Utils.createHash(typeIri), invalidateStatement, pubkeyStatement));
579
                        }
580
                    }
581

582
//                                        for (Value v : Utils.getObjectsForPattern(metaConn, NPA.GRAPH, invalidatedNpId, DCTERMS.CREATOR)) {
583
//                                                IRI creatorIri = (IRI) v;
584
//                                                if (!SimpleCreatorPattern.getCreators(thisNp).contains(creatorIri)) {
585
//                                                        //log.info("Adding invalidation expressed in " + thisNp.getUri() + " also to repo for user " + creatorIri);
586
//                                                        connections.add(loadStatements("user_" + Utils.createHash(creatorIri), invalidateStatement, pubkeyStatement));
587
//                                                        connections.add(loadStatements("text-user_" + Utils.createHash(creatorIri), invalidateStatement, pubkeyStatement));
588
//                                                }
589
//                                        }
590
                }
591

592
                metaConn.commit();
593
                // TODO handle case that some commits succeed and some fail
594
                for (RepositoryConnection c : connections) c.commit();
595
                success = true;
596
            } catch (Exception ex) {
597
                log.warn("Could not load invalidate statements.", ex);
598
                if (metaConn.isActive()) metaConn.rollback();
599
                for (RepositoryConnection c : connections) {
600
                    if (c.isActive()) c.rollback();
601
                }
602
            } finally {
603
                metaConn.close();
604
                for (RepositoryConnection c : connections) c.close();
605
            }
606
            if (!success) {
607
                retries++;
608
                if (retries >= MAX_RETRIES) {
609
                    throw new RuntimeException("Failed to load invalidate statements for " + thisNp.getUri() + " after " + MAX_RETRIES + " retries");
610
                }
611
                long delay = computeBackoffMillis(retries);
612
                log.info("Retrying in {} ms (attempt {}/{})...", delay, retries, MAX_RETRIES);
613
                try {
614
                    Thread.sleep(delay);
615
                } catch (InterruptedException x) {
616
                    Thread.currentThread().interrupt();
617
                }
618
            }
619
        }
620
    }
621

622
    @GeneratedFlagForDependentElements
623
    private static RepositoryConnection loadStatements(String repoName, Statement... statements) {
624
        RepositoryConnection conn = TripleStore.get().getRepoConnection(repoName);
625
        // Basic isolation: we only append new statements
626
        conn.begin(IsolationLevels.READ_COMMITTED);
627
        for (Statement st : statements) {
628
            conn.add(st);
629
        }
630
        return conn;
631
    }
632

633
    @GeneratedFlagForDependentElements
634
    static List<Statement> getInvalidatingStatements(IRI npId) {
635
        List<Statement> invalidatingStatements = new ArrayList<>();
636
        boolean success = false;
637
        int retries = 0;
638
        while (!success) {
639
            RepositoryConnection conn = TripleStore.get().getRepoConnection("meta");
640
            try (conn) {
641
                // Basic isolation because here we only read append-only data.
642
                conn.begin(IsolationLevels.READ_COMMITTED);
643

644
                TupleQueryResult r = conn.prepareTupleQuery(QueryLanguage.SPARQL, "SELECT * { graph <" + NPA.GRAPH + "> { " + "?np <" + NPX.INVALIDATES + "> <" + npId + "> ; <" + NPA.HAS_VALID_SIGNATURE_FOR_PUBLIC_KEY + "> ?pubkey . " + "} }").evaluate();
645
                try (r) {
646
                    while (r.hasNext()) {
647
                        BindingSet b = r.next();
648
                        invalidatingStatements.add(vf.createStatement((IRI) b.getBinding("np").getValue(), NPX.INVALIDATES, npId, NPA.GRAPH));
649
                        invalidatingStatements.add(vf.createStatement((IRI) b.getBinding("np").getValue(), NPA.HAS_VALID_SIGNATURE_FOR_PUBLIC_KEY, b.getBinding("pubkey").getValue(), NPA.GRAPH));
650
                    }
651
                }
652
                conn.commit();
653
                success = true;
654
            } catch (Exception ex) {
655
                log.warn("Could not load invalidating statements.", ex);
656
                if (conn.isActive()) conn.rollback();
657
            }
658
            if (!success) {
659
                retries++;
660
                if (retries >= MAX_RETRIES) {
661
                    throw new RuntimeException("Failed to get invalidating statements for " + npId + " after " + MAX_RETRIES + " retries");
662
                }
663
                long delay = computeBackoffMillis(retries);
664
                log.info("Retrying in {} ms (attempt {}/{})...", delay, retries, MAX_RETRIES);
665
                try {
666
                    Thread.sleep(delay);
667
                } catch (InterruptedException x) {
668
                    Thread.currentThread().interrupt();
669
                }
670
            }
671
        }
672
        return invalidatingStatements;
673
    }
674

675
    @GeneratedFlagForDependentElements
676
    private static void loadNoteToRepo(Resource subj, String note) {
677
        boolean success = false;
678
        int retries = 0;
679
        while (!success) {
680
            RepositoryConnection conn = TripleStore.get().getAdminRepoConnection();
681
            try (conn) {
682
                List<Statement> statements = new ArrayList<>();
683
                statements.add(vf.createStatement(subj, NPA.NOTE, vf.createLiteral(note), NPA.GRAPH));
684
                conn.add(statements);
685
                success = true;
686
            } catch (Exception ex) {
687
                log.warn("Could not load note to repo.", ex);
688
            }
689
            if (!success) {
690
                retries++;
691
                if (retries >= MAX_RETRIES) {
692
                    throw new RuntimeException("Failed to load note to repo for " + subj + " after " + MAX_RETRIES + " retries");
693
                }
694
                long delay = computeBackoffMillis(retries);
695
                log.info("Retrying in {} ms (attempt {}/{})...", delay, retries, MAX_RETRIES);
696
                try {
697
                    Thread.sleep(delay);
698
                } catch (InterruptedException x) {
699
                    Thread.currentThread().interrupt();
700
                }
701
            }
702
        }
703
    }
704

705
    static boolean hasValidSignature(NanopubSignatureElement el) {
706
        try {
707
            if (el != null && SignatureUtils.hasValidSignature(el) && el.getPublicKeyString() != null) {
16!
708
                return true;
4✔
709
            }
710
        } catch (GeneralSecurityException ex) {
2✔
711
            log.warn("Signature validation failed for signature element {}", el.getUri(), ex);
12✔
712
        }
2✔
713
        return false;
4✔
714
    }
715

716
    private static IRI getBaseTrustyUri(Value v) {
717
        if (!(v instanceof IRI)) return null;
6!
718
        String s = v.stringValue();
6✔
719
        if (!s.matches(".*[^A-Za-z0-9\\-_]RA[A-Za-z0-9\\-_]{43}([^A-Za-z0-9\\\\-_].{0,43})?")) {
8✔
720
            return null;
4✔
721
        }
722
        return vf.createIRI(s.replaceFirst("^(.*[^A-Za-z0-9\\-_]RA[A-Za-z0-9\\-_]{43})([^A-Za-z0-9\\\\-_].{0,43})?$", "$1"));
14✔
723
    }
724

725
    // TODO: Move this to nanopub library:
726
    private static boolean isIntroNanopub(Nanopub np) {
727
        for (Statement st : np.getAssertion()) {
22✔
728
            if (st.getPredicate().equals(NPX.DECLARED_BY)) return true;
14✔
729
        }
2✔
730
        return false;
4✔
731
    }
732

733
    /**
734
     * Detects whether the given nanopub is a Space-defining nanopub (typed
735
     * {@code gen:Space}) and, if so, registers each space it declares (one per
736
     * {@code <spaceIri> gen:hasRootDefinition <rootUri>} triple) in
737
     * {@link SpaceRegistry}. Nanopubs missing the {@code gen:hasRootDefinition}
738
     * triple are not recognized as space-defining — there is no transition fallback.
739
     *
740
     * @param np the nanopub to inspect
741
     * @return the set of space refs registered from this nanopub (possibly empty);
742
     *         currently used by tests to assert detection behavior. Production
743
     *         callers invoke this for its side effect on {@link SpaceRegistry};
744
     *         downstream consumers (extraction, materialization) follow in later
745
     *         steps of #62.
746
     */
747
    static Set<String> detectAndRegisterSpaces(Nanopub np) {
748
        if (!FeatureFlags.spacesEnabled()) return Collections.emptySet();
4!
749
        boolean isSpaceTyped = false;
4✔
750
        for (IRI typeIri : NanopubUtils.getTypes(np)) {
22✔
751
            if (typeIri.equals(GEN.SPACE)) {
8✔
752
                isSpaceTyped = true;
4✔
753
                break;
2✔
754
            }
755
        }
2✔
756
        if (!isSpaceTyped) return Collections.emptySet();
8✔
757
        Set<String> spaceRefs = new LinkedHashSet<>();
8✔
758
        for (Statement st : np.getAssertion()) {
22✔
759
            if (!st.getPredicate().equals(GEN.HAS_ROOT_DEFINITION)) continue;
12✔
760
            if (!(st.getSubject() instanceof IRI spaceIri)) continue;
18!
761
            if (!(st.getObject() instanceof IRI rootUri)) continue;
18!
762
            String rootNanopubId = TrustyUriUtils.getArtifactCode(rootUri.stringValue());
8✔
763
            if (rootNanopubId == null || rootNanopubId.isEmpty()) {
10!
764
                log.warn("Ignoring space {}: gen:hasRootDefinition target is not a trusty URI: {}", spaceIri, rootUri);
10✔
765
                continue;
2✔
766
            }
767
            SpaceRegistry.Registration registration = SpaceRegistry.get().registerSpace(rootNanopubId, spaceIri);
10✔
768
            spaceRefs.add(registration.spaceRef());
10✔
769
            if (registration.wasNew()) {
6!
770
                SpacesAdminStore.persistSpace(rootNanopubId, spaceIri);
6✔
771
            }
772
        }
2✔
773
        return spaceRefs;
4✔
774
    }
775

776
    /**
777
     * Check if a nanopub is already loaded in the admin graph.
778
     *
779
     * @param npId the nanopub ID
780
     * @return true if the nanopub is loaded, false otherwise
781
     */
782
    @GeneratedFlagForDependentElements
783
    static boolean isNanopubLoaded(String npId) {
784
        boolean loaded = false;
785
        RepositoryConnection conn = TripleStore.get().getRepoConnection("meta");
786
        try (conn) {
787
            if (Utils.getObjectForPattern(conn, NPA.GRAPH, vf.createIRI(npId), NPA.HAS_LOAD_NUMBER) != null) {
788
                loaded = true;
789
            }
790
        } catch (Exception ex) {
791
            log.warn("Could not check whether nanopub is loaded.", ex);
792
        }
793
        return loaded;
794
    }
795

796
    private static ValueFactory vf = SimpleValueFactory.getInstance();
4✔
797

798
    // TODO remove the constants and use the ones from the nanopub library instead
799

800
    /**
801
     * Template for the query that fetches the status of a repository.
802
     */
803
    // Template for .fetchRepoStatus
804
    private static final String REPO_STATUS_QUERY_TEMPLATE = """
56✔
805
            SELECT * { graph <%s> {
806
              OPTIONAL { <%s> <%s> ?loadNumber . }
807
              <%s> <%s> ?count ;
808
                   <%s> ?checksum .
809
            } }
810
            """.formatted(NPA.GRAPH, "%s", NPA.HAS_LOAD_NUMBER, NPA.THIS_REPO, NPA.HAS_NANOPUB_COUNT, NPA.HAS_NANOPUB_CHECKSUM);
4✔
811
}
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