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

knowledgepixels / nanopub-query / 24885173446

24 Apr 2026 10:36AM UTC coverage: 59.977% (+0.7%) from 59.265%
24885173446

Pull #79

github

web-flow
Merge 0326680ca into 9738a722d
Pull Request #79: refactor: remove v1 spaces infrastructure (prep for #62)

262 of 490 branches covered (53.47%)

Branch coverage included in aggregate %.

769 of 1229 relevant lines covered (62.57%)

9.11 hits per line

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

80.7
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

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

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

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

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

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

95

96
    NanopubLoader(Nanopub np, long counter) {
6✔
97
        this.np = np;
9✔
98
        if (counter >= 0) {
12✔
99
            log.info("Loading {}: {}", counter, np.getUri());
24✔
100
        } else {
101
            log.info("Loading: {}", np.getUri());
15✔
102
        }
103

104
        // TODO Ensure proper synchronization and DB rollbacks
105

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

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

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

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

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

137
        Set<IRI> subIris = new HashSet<>();
12✔
138
        Set<IRI> otherNps = new HashSet<>();
12✔
139
        Set<IRI> invalidated = new HashSet<>();
12✔
140
        Set<IRI> retracted = new HashSet<>();
12✔
141
        Set<IRI> superseded = new HashSet<>();
12✔
142
        String combinedLiterals = "";
6✔
143
        for (Statement st : NanopubUtils.getStatements(np)) {
33✔
144
            nanopubStatements.add(st);
15✔
145

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

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

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

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

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

262
        String literalFilter = "_pubkey_" + Utils.createHash(el.getPublicKeyString());
18✔
263
        for (IRI typeIri : NanopubUtils.getTypes(np)) {
33✔
264
            metaStatements.add(vf.createStatement(np.getUri(), NPX.HAS_NANOPUB_TYPE, typeIri, NPA.GRAPH));
33✔
265
            // @ADMIN-TRIPLE-TABLE@ NANOPUB, npx:hasNanopubType, NANOPUB_TYPE, npa:graph, meta, type of NANOPUB
266
            literalFilter += " _type_" + Utils.createHash(typeIri);
15✔
267
        }
3✔
268
        String label = NanopubUtils.getLabel(np);
9✔
269
        if (label != null) {
6!
270
            metaStatements.add(vf.createStatement(np.getUri(), RDFS.LABEL, vf.createLiteral(label), NPA.GRAPH));
39✔
271
            // @ADMIN-TRIPLE-TABLE@ NANOPUB, rdfs:label, LABEL, npa:graph, meta, label of NANOPUB
272
        }
273
        String description = NanopubUtils.getDescription(np);
9✔
274
        if (description != null) {
6✔
275
            metaStatements.add(vf.createStatement(np.getUri(), DCTERMS.DESCRIPTION, vf.createLiteral(description), NPA.GRAPH));
39✔
276
            // @ADMIN-TRIPLE-TABLE@ NANOPUB, dct:description, LABEL, npa:graph, meta, description of NANOPUB
277
        }
278
        for (IRI creatorIri : SimpleCreatorPattern.getCreators(np)) {
33✔
279
            metaStatements.add(vf.createStatement(np.getUri(), DCTERMS.CREATOR, creatorIri, NPA.GRAPH));
33✔
280
            // @ADMIN-TRIPLE-TABLE@ NANOPUB, dct:creator, CREATOR, npa:graph, meta, creator of NANOPUB (can be several)
281
        }
3✔
282
        for (IRI authorIri : SimpleCreatorPattern.getAuthors(np)) {
21!
283
            metaStatements.add(vf.createStatement(np.getUri(), PAV.AUTHORED_BY, authorIri, NPA.GRAPH));
×
284
            // @ADMIN-TRIPLE-TABLE@ NANOPUB, pav:authoredBy, AUTHOR, npa:graph, meta, author of NANOPUB (can be several)
285
        }
×
286

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

292
        // Any statements that express that the currently processed nanopub is already invalidated:
293
        List<Statement> invalidatingStatements = getInvalidatingStatements(np.getUri());
12✔
294

295
        metaStatements.addAll(invalidateStatements);
18✔
296

297
        allStatements = new ArrayList<>(nanopubStatements);
21✔
298
        allStatements.addAll(metaStatements);
18✔
299
        allStatements.addAll(invalidatingStatements);
15✔
300

301
        textStatements = new ArrayList<>(literalStatements);
21✔
302
        textStatements.addAll(metaStatements);
18✔
303
        textStatements.addAll(invalidatingStatements);
15✔
304
    }
3✔
305

306
    /**
307
     * Get the HTTP client used for fetching nanopublications.
308
     *
309
     * @return the HTTP client
310
     */
311
    static HttpClient getHttpClient() {
312
        if (httpClient == null) {
6✔
313
            httpClient = HttpClientBuilder.create().setDefaultRequestConfig(Utils.getHttpRequestConfig()).build();
15✔
314
        }
315
        return httpClient;
6✔
316
    }
317

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

332
    /**
333
     * Load a nanopub into the database.
334
     *
335
     * @param np      the nanopub to load
336
     * @param counter the load counter, only used for logging (or -1 if not known)
337
     * @throws RDF4JException if the loading fails
338
     */
339
    public static void load(Nanopub np, long counter) throws RDF4JException {
340
        NanopubLoader loader = new NanopubLoader(np, counter);
18✔
341
        loader.executeLoading();
6✔
342
    }
3✔
343

344
    @GeneratedFlagForDependentElements
345
    private void executeLoading() {
346
        var runningTasks = new ArrayList<Future<?>>();
347
        Consumer<Runnable> runTask = t -> runningTasks.add(loadingPool.submit(t));
×
348

349
        for (String note : notes) {
350
            loadNoteToRepo(np.getUri(), note);
351
        }
352

353
        if (!aborted) {
354
            // Submit all tasks except the "meta" task
355
            if (timestamp != null) {
356
                if (new Date().getTime() - timestamp.getTimeInMillis() < THIRTY_DAYS) {
357
                    if (FeatureFlags.last30dRepoEnabled()) {
358
                        runTask.accept(() -> loadNanopubToLatest(np.getUri(), allStatements));
×
359
                    }
360
                }
361
            }
362

363
            if (FeatureFlags.textRepoEnabled()) {
364
                runTask.accept(() -> loadNanopubToRepo(np.getUri(), textStatements, "text"));
×
365
            }
366
            if (FeatureFlags.fullRepoEnabled()) {
367
                runTask.accept(() -> loadNanopubToRepo(np.getUri(), allStatements, "full"));
×
368
            }
369
            // Note: "meta" task is deferred until all other tasks complete successfully
370

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

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

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

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

418
    private static Long lastUpdateOfLatestRepo = null;
6✔
419
    private static long THIRTY_DAYS = 1000L * 60 * 60 * 24 * 30;
6✔
420
    private static long ONE_HOUR = 1000L * 60 * 60;
6✔
421

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

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

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

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

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

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

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

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

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

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

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

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

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

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

707
    static boolean hasValidSignature(NanopubSignatureElement el) {
708
        try {
709
            if (el != null && SignatureUtils.hasValidSignature(el) && el.getPublicKeyString() != null) {
24!
710
                return true;
6✔
711
            }
712
        } catch (GeneralSecurityException ex) {
3✔
713
            log.warn("Signature validation failed for signature element {}", el.getUri(), ex);
18✔
714
        }
3✔
715
        return false;
6✔
716
    }
717

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

727
    // TODO: Move this to nanopub library:
728
    private static boolean isIntroNanopub(Nanopub np) {
729
        for (Statement st : np.getAssertion()) {
33✔
730
            if (st.getPredicate().equals(NPX.DECLARED_BY)) return true;
21✔
731
        }
3✔
732
        return false;
6✔
733
    }
734

735
    /**
736
     * Check if a nanopub is already loaded in the admin graph.
737
     *
738
     * @param npId the nanopub ID
739
     * @return true if the nanopub is loaded, false otherwise
740
     */
741
    @GeneratedFlagForDependentElements
742
    static boolean isNanopubLoaded(String npId) {
743
        boolean loaded = false;
744
        RepositoryConnection conn = TripleStore.get().getRepoConnection("meta");
745
        try (conn) {
746
            if (Utils.getObjectForPattern(conn, NPA.GRAPH, vf.createIRI(npId), NPA.HAS_LOAD_NUMBER) != null) {
747
                loaded = true;
748
            }
749
        } catch (Exception ex) {
750
            log.warn("Could not check whether nanopub is loaded.", ex);
751
        }
752
        return loaded;
753
    }
754

755
    private static ValueFactory vf = SimpleValueFactory.getInstance();
6✔
756

757
    // TODO remove the constants and use the ones from the nanopub library instead
758

759
    /**
760
     * Template for the query that fetches the status of a repository.
761
     */
762
    // Template for .fetchRepoStatus
763
    private static final String REPO_STATUS_QUERY_TEMPLATE = """
84✔
764
            SELECT * { graph <%s> {
765
              OPTIONAL { <%s> <%s> ?loadNumber . }
766
              <%s> <%s> ?count ;
767
                   <%s> ?checksum .
768
            } }
769
            """.formatted(NPA.GRAPH, "%s", NPA.HAS_LOAD_NUMBER, NPA.THIS_REPO, NPA.HAS_NANOPUB_COUNT, NPA.HAS_NANOPUB_CHECKSUM);
6✔
770
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc