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

knowledgepixels / nanopub-query / 20775456684

07 Jan 2026 08:34AM UTC coverage: 71.961% (+0.6%) from 71.34%
20775456684

push

github

tkuhn
fix(NanopubLoader): Defer meta loading to fix half-loaded nanopubs

214 of 326 branches covered (65.64%)

Branch coverage included in aggregate %.

597 of 801 relevant lines covered (74.53%)

11.05 hits per line

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

81.56
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 java.security.GeneralSecurityException;
35
import java.util.*;
36
import java.util.concurrent.ExecutionException;
37
import java.util.concurrent.Executors;
38
import java.util.concurrent.Future;
39
import java.util.concurrent.ThreadPoolExecutor;
40
import java.util.function.Consumer;
41

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

47
    private static HttpClient httpClient;
48
    private static final ThreadPoolExecutor loadingPool = (ThreadPoolExecutor) Executors.newFixedThreadPool(4);
12✔
49
    private Nanopub np;
50
    private NanopubSignatureElement el = null;
9✔
51
    private List<Statement> metaStatements = new ArrayList<>();
15✔
52
    private List<Statement> nanopubStatements = new ArrayList<>();
15✔
53
    private List<Statement> literalStatements = new ArrayList<>();
15✔
54
    private List<Statement> invalidateStatements = new ArrayList<>();
15✔
55
    private List<Statement> textStatements, allStatements;
56
    private Calendar timestamp = null;
9✔
57
    private Statement pubkeyStatement, pubkeyStatementX;
58
    private List<String> notes = new ArrayList<>();
15✔
59
    private boolean aborted = false;
9✔
60
    private static final Logger log = LoggerFactory.getLogger(NanopubLoader.class);
9✔
61

62

63
    NanopubLoader(Nanopub np, long counter) {
6✔
64
        this.np = np;
9✔
65
        if (counter >= 0) {
12✔
66
            log.info("Loading {}: {}", counter, np.getUri());
24✔
67
        } else {
68
            log.info("Loading: {}", np.getUri());
15✔
69
        }
70

71
        // TODO Ensure proper synchronization and DB rollbacks
72

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

75
        String ac = TrustyUriUtils.getArtifactCode(np.getUri().toString());
15✔
76
        if (!np.getHeadUri().toString().contains(ac) || !np.getAssertionUri().toString().contains(ac) || !np.getProvenanceUri().toString().contains(ac) || !np.getPubinfoUri().toString().contains(ac)) {
72!
77
            notes.add("could not load nanopub as not all graphs contained the artifact code");
×
78
            aborted = true;
×
79
            return;
×
80
        }
81

82
        try {
83
            el = SignatureUtils.getSignatureElement(np);
12✔
84
        } catch (MalformedCryptoElementException ex) {
×
85
            notes.add("Signature error");
×
86
        }
3✔
87
        if (!hasValidSignature(el)) {
12✔
88
            aborted = true;
9✔
89
            return;
3✔
90
        }
91

92
        pubkeyStatement = vf.createStatement(np.getUri(), NPA.HAS_VALID_SIGNATURE_FOR_PUBLIC_KEY, vf.createLiteral(el.getPublicKeyString()), NPA.GRAPH);
39✔
93
        // @ADMIN-TRIPLE-TABLE@ NANOPUB, npa:hasValidSignatureForPublicKey, FULL_PUBKEY, npa:graph, meta, full pubkey if signature is valid
94
        metaStatements.add(pubkeyStatement);
18✔
95
        pubkeyStatementX = vf.createStatement(np.getUri(), NPA.HAS_VALID_SIGNATURE_FOR_PUBLIC_KEY_HASH, vf.createLiteral(Utils.createHash(el.getPublicKeyString())), NPA.GRAPH);
42✔
96
        // @ADMIN-TRIPLE-TABLE@ NANOPUB, npa:hasValidSignatureForPublicKeyHash, PUBKEY_HASH, npa:graph, meta, hex-encoded SHA256 hash if signature is valid
97
        metaStatements.add(pubkeyStatementX);
18✔
98

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

104
        Set<IRI> subIris = new HashSet<>();
12✔
105
        Set<IRI> otherNps = new HashSet<>();
12✔
106
        Set<IRI> invalidated = new HashSet<>();
12✔
107
        Set<IRI> retracted = new HashSet<>();
12✔
108
        Set<IRI> superseded = new HashSet<>();
12✔
109
        String combinedLiterals = "";
6✔
110
        for (Statement st : NanopubUtils.getStatements(np)) {
33✔
111
            nanopubStatements.add(st);
15✔
112

113
            if (st.getPredicate().toString().contains(ac)) {
18!
114
                subIris.add(st.getPredicate());
×
115
            } else {
116
                IRI b = getBaseTrustyUri(st.getPredicate());
12✔
117
                if (b != null) otherNps.add(b);
6!
118
            }
119
            if (st.getPredicate().equals(NPX.RETRACTS) && st.getObject() instanceof IRI) {
15!
120
                retracted.add((IRI) st.getObject());
×
121
            }
122
            if (st.getPredicate().equals(NPX.INVALIDATES) && st.getObject() instanceof IRI) {
15!
123
                invalidated.add((IRI) st.getObject());
×
124
            }
125
            if (st.getSubject().equals(np.getUri()) && st.getObject() instanceof IRI) {
30✔
126
                if (st.getPredicate().equals(NPX.SUPERSEDES)) {
15✔
127
                    superseded.add((IRI) st.getObject());
18✔
128
                }
129
                if (st.getObject().toString().matches(".*[^A-Za-z0-9\\-_]RA[A-Za-z0-9\\-_]{43}")) {
18✔
130
                    metaStatements.add(vf.createStatement(np.getUri(), st.getPredicate(), st.getObject(), NPA.NETWORK_GRAPH));
39✔
131
                    // @ADMIN-TRIPLE-TABLE@ NANOPUB1, RELATION, NANOPUB2, npa:networkGraph, meta, any inter-nanopub relation found in NANOPUB1
132
                }
133
                if (st.getContext().equals(np.getPubinfoUri())) {
18✔
134
                    if (st.getPredicate().equals(NPX.INTRODUCES) || st.getPredicate().equals(NPX.DESCRIBES) || st.getPredicate().equals(NPX.EMBEDS)) {
45!
135
                        metaStatements.add(vf.createStatement(np.getUri(), st.getPredicate(), st.getObject(), NPA.GRAPH));
39✔
136
                        // @ADMIN-TRIPLE-TABLE@ NANOPUB, npx:introduces, THING, npa:graph, meta, when such a triple is present in pubinfo of NANOPUB
137
                        // @ADMIN-TRIPLE-TABLE@ NANOPUB, npx:describes, THING, npa:graph, meta, when such a triple is present in pubinfo of NANOPUB
138
                        // @ADMIN-TRIPLE-TABLE@ NANOPUB, npx:embeds, THING, npa:graph, meta, when such a triple is present in pubinfo of NANOPUB
139
                    }
140
                }
141
            }
142
            if (st.getSubject().toString().contains(ac)) {
18✔
143
                subIris.add((IRI) st.getSubject());
21✔
144
            } else {
145
                IRI b = getBaseTrustyUri(st.getSubject());
12✔
146
                if (b != null) otherNps.add(b);
6!
147
            }
148
            if (st.getObject() instanceof IRI) {
12✔
149
                if (st.getObject().toString().contains(ac)) {
18✔
150
                    subIris.add((IRI) st.getObject());
21✔
151
                } else {
152
                    IRI b = getBaseTrustyUri(st.getObject());
12✔
153
                    if (b != null) otherNps.add(b);
18✔
154
                }
3✔
155
            } else {
156
                combinedLiterals += st.getObject().stringValue().replaceAll("\\s+", " ") + "\n";
27✔
157
//                                if (st.getSubject().equals(np.getUri()) && !st.getSubject().equals(HAS_FILTER_LITERAL)) {
158
//                                        literalStatements.add(vf.createStatement(np.getUri(), st.getPredicate(), st.getObject(), LITERAL_GRAPH));
159
//                                } else {
160
//                                        literalStatements.add(vf.createStatement(np.getUri(), HAS_LITERAL, st.getObject(), LITERAL_GRAPH));
161
//                                }
162
            }
163
        }
3✔
164
        subIris.remove(np.getUri());
15✔
165
        subIris.remove(np.getAssertionUri());
15✔
166
        subIris.remove(np.getProvenanceUri());
15✔
167
        subIris.remove(np.getPubinfoUri());
15✔
168
        for (IRI i : subIris) {
30✔
169
            metaStatements.add(vf.createStatement(np.getUri(), NPA.HAS_SUB_IRI, i, NPA.GRAPH));
33✔
170
            // @ADMIN-TRIPLE-TABLE@ NANOPUB, npa:hasSubIri, SUB_IRI, npa:graph, meta, for any IRI minted in the namespace of the NANOPUB
171
        }
3✔
172
        for (IRI i : otherNps) {
30✔
173
            metaStatements.add(vf.createStatement(np.getUri(), NPA.REFERS_TO_NANOPUB, i, NPA.NETWORK_GRAPH));
33✔
174
            // @ADMIN-TRIPLE-TABLE@ NANOPUB1, npa:refersToNanopub, NANOPUB2, npa:networkGraph, meta, generic inter-nanopub relation
175
        }
3✔
176
        for (IRI i : invalidated) {
18!
177
            invalidateStatements.add(vf.createStatement(np.getUri(), NPX.INVALIDATES, i, NPA.GRAPH));
×
178
            // @ADMIN-TRIPLE-TABLE@ NANOPUB, npx:invalidates, INVALIDATED_NANOPUB, npa:graph, meta, if the NANOPUB retracts or supersedes another nanopub
179
        }
×
180
        for (IRI i : retracted) {
18!
181
            invalidateStatements.add(vf.createStatement(np.getUri(), NPX.INVALIDATES, i, NPA.GRAPH));
×
182
            metaStatements.add(vf.createStatement(np.getUri(), NPX.RETRACTS, i, NPA.GRAPH));
×
183
            // @ADMIN-TRIPLE-TABLE@ NANOPUB, npx:retracts, RETRACTED_NANOPUB, npa:graph, meta, if the NANOPUB retracts another nanopub
184
        }
×
185
        for (IRI i : superseded) {
30✔
186
            invalidateStatements.add(vf.createStatement(np.getUri(), NPX.INVALIDATES, i, NPA.GRAPH));
33✔
187
            metaStatements.add(vf.createStatement(np.getUri(), NPX.SUPERSEDES, i, NPA.GRAPH));
33✔
188
            // @ADMIN-TRIPLE-TABLE@ NANOPUB, npx:supersedes, SUPERSEDED_NANOPUB, npa:graph, meta, if the NANOPUB supersedes another nanopub
189
        }
3✔
190

191
        metaStatements.add(vf.createStatement(np.getUri(), NPA.HAS_HEAD_GRAPH, np.getHeadUri(), NPA.GRAPH));
36✔
192
        // @ADMIN-TRIPLE-TABLE@ NANOPUB, npa:hasHeadGraph, HEAD_GRAPH, npa:graph, meta, direct link to the head graph of the NANOPUB
193
        metaStatements.add(vf.createStatement(np.getUri(), NPA.HAS_GRAPH, np.getHeadUri(), NPA.GRAPH));
36✔
194
        // @ADMIN-TRIPLE-TABLE@ NANOPUB, npa:hasGraph, GRAPH, npa:graph, meta, generic link to all four graphs of the given NANOPUB
195
        metaStatements.add(vf.createStatement(np.getUri(), NP.HAS_ASSERTION, np.getAssertionUri(), NPA.GRAPH));
36✔
196
        // @ADMIN-TRIPLE-TABLE@ NANOPUB, np:hasAssertion, ASSERTION_GRAPH, npa:graph, meta, direct link to the assertion graph of the NANOPUB
197
        metaStatements.add(vf.createStatement(np.getUri(), NPA.HAS_GRAPH, np.getAssertionUri(), NPA.GRAPH));
36✔
198
        metaStatements.add(vf.createStatement(np.getUri(), NP.HAS_PROVENANCE, np.getProvenanceUri(), NPA.GRAPH));
36✔
199
        // @ADMIN-TRIPLE-TABLE@ NANOPUB, np:hasProvenance, PROVENANCE_GRAPH, npa:graph, meta, direct link to the provenance graph of the NANOPUB
200
        metaStatements.add(vf.createStatement(np.getUri(), NPA.HAS_GRAPH, np.getProvenanceUri(), NPA.GRAPH));
36✔
201
        metaStatements.add(vf.createStatement(np.getUri(), NP.HAS_PUBINFO, np.getPubinfoUri(), NPA.GRAPH));
36✔
202
        // @ADMIN-TRIPLE-TABLE@ NANOPUB, np:hasPublicationInfo, PUBINFO_GRAPH, npa:graph, meta, direct link to the pubinfo graph of the NANOPUB
203
        metaStatements.add(vf.createStatement(np.getUri(), NPA.HAS_GRAPH, np.getPubinfoUri(), NPA.GRAPH));
36✔
204

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

209
        if (isIntroNanopub(np)) {
9✔
210
            IntroNanopub introNp = new IntroNanopub(np);
15✔
211
            metaStatements.add(vf.createStatement(np.getUri(), NPA.IS_INTRODUCTION_OF, introNp.getUser(), NPA.GRAPH));
36✔
212
            // @ADMIN-TRIPLE-TABLE@ NANOPUB, npa:isIntroductionOf, AGENT, npa:graph, meta, linking intro nanopub to the agent it is introducing
213
            for (KeyDeclaration kc : introNp.getKeyDeclarations()) {
33✔
214
                metaStatements.add(vf.createStatement(np.getUri(), NPA.DECLARES_PUBKEY, vf.createLiteral(kc.getPublicKeyString()), NPA.GRAPH));
42✔
215
                // @ADMIN-TRIPLE-TABLE@ NANOPUB, npa:declaresPubkey, FULL_PUBKEY, npa:graph, meta, full pubkey declared by the given intro NANOPUB
216
            }
3✔
217
        }
218

219
        try {
220
            timestamp = SimpleTimestampPattern.getCreationTime(np);
12✔
221
        } catch (IllegalArgumentException ex) {
×
222
            notes.add("Illegal date/time");
×
223
        }
3✔
224
        if (timestamp != null) {
9!
225
            metaStatements.add(vf.createStatement(np.getUri(), DCTERMS.CREATED, vf.createLiteral(timestamp.getTime()), NPA.GRAPH));
45✔
226
            // @ADMIN-TRIPLE-TABLE@ NANOPUB, dct:created, CREATION_DATE, npa:graph, meta, normalized creation timestamp
227
        }
228

229
        String literalFilter = "_pubkey_" + Utils.createHash(el.getPublicKeyString());
18✔
230
        for (IRI typeIri : NanopubUtils.getTypes(np)) {
33✔
231
            metaStatements.add(vf.createStatement(np.getUri(), NPX.HAS_NANOPUB_TYPE, typeIri, NPA.GRAPH));
33✔
232
            // @ADMIN-TRIPLE-TABLE@ NANOPUB, npx:hasNanopubType, NANOPUB_TYPE, npa:graph, meta, type of NANOPUB
233
            literalFilter += " _type_" + Utils.createHash(typeIri);
15✔
234
        }
3✔
235
        String label = NanopubUtils.getLabel(np);
9✔
236
        if (label != null) {
6!
237
            metaStatements.add(vf.createStatement(np.getUri(), RDFS.LABEL, vf.createLiteral(label), NPA.GRAPH));
39✔
238
            // @ADMIN-TRIPLE-TABLE@ NANOPUB, rdfs:label, LABEL, npa:graph, meta, label of NANOPUB
239
        }
240
        String description = NanopubUtils.getDescription(np);
9✔
241
        if (description != null) {
6✔
242
            metaStatements.add(vf.createStatement(np.getUri(), DCTERMS.DESCRIPTION, vf.createLiteral(description), NPA.GRAPH));
39✔
243
            // @ADMIN-TRIPLE-TABLE@ NANOPUB, dct:description, LABEL, npa:graph, meta, description of NANOPUB
244
        }
245
        for (IRI creatorIri : SimpleCreatorPattern.getCreators(np)) {
33✔
246
            metaStatements.add(vf.createStatement(np.getUri(), DCTERMS.CREATOR, creatorIri, NPA.GRAPH));
33✔
247
            // @ADMIN-TRIPLE-TABLE@ NANOPUB, dct:creator, CREATOR, npa:graph, meta, creator of NANOPUB (can be several)
248
        }
3✔
249
        for (IRI authorIri : SimpleCreatorPattern.getAuthors(np)) {
21!
250
            metaStatements.add(vf.createStatement(np.getUri(), PAV.AUTHORED_BY, authorIri, NPA.GRAPH));
×
251
            // @ADMIN-TRIPLE-TABLE@ NANOPUB, pav:authoredBy, AUTHOR, npa:graph, meta, author of NANOPUB (can be several)
252
        }
×
253

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

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

262
        metaStatements.addAll(invalidateStatements);
18✔
263

264
        allStatements = new ArrayList<>(nanopubStatements);
21✔
265
        allStatements.addAll(metaStatements);
18✔
266
        allStatements.addAll(invalidatingStatements);
15✔
267

268
        textStatements = new ArrayList<>(literalStatements);
21✔
269
        textStatements.addAll(metaStatements);
18✔
270
        textStatements.addAll(invalidatingStatements);
15✔
271
    }
3✔
272

273
    /**
274
     * Get the HTTP client used for fetching nanopublications.
275
     *
276
     * @return the HTTP client
277
     */
278
    static HttpClient getHttpClient() {
279
        if (httpClient == null) {
6✔
280
            httpClient = HttpClientBuilder.create().setDefaultRequestConfig(Utils.getHttpRequestConfig()).build();
15✔
281
        }
282
        return httpClient;
6✔
283
    }
284

285
    /**
286
     * Load the given nanopublication into the database.
287
     *
288
     * @param nanopubUri Nanopublication identifier (URI)
289
     */
290
    public static void load(String nanopubUri) {
291
        if (isNanopubLoaded(nanopubUri)) {
9!
292
            log.info("Already loaded: {}", nanopubUri);
×
293
        } else {
294
            Nanopub np = GetNanopub.get(nanopubUri, getHttpClient());
12✔
295
            load(np, -1);
9✔
296
        }
297
    }
3✔
298

299
    /**
300
     * Load a nanopub into the database.
301
     *
302
     * @param np      the nanopub to load
303
     * @param counter the load counter, only used for logging (or -1 if not known)
304
     * @throws RDF4JException if the loading fails
305
     */
306
    public static void load(Nanopub np, long counter) throws RDF4JException {
307
        NanopubLoader loader = new NanopubLoader(np, counter);
18✔
308
        loader.executeLoading();
6✔
309
    }
3✔
310

311
    @GeneratedFlagForDependentElements
312
    private void executeLoading() {
313
        var runningTasks = new ArrayList<Future<?>>();
314
        Consumer<Runnable> runTask = t -> runningTasks.add(loadingPool.submit(t));
×
315

316
        for (String note : notes) {
317
            loadNoteToRepo(np.getUri(), note);
318
        }
319

320
        if (!aborted) {
321
            // Submit all tasks except the "meta" task
322
            if (timestamp != null) {
323
                if (new Date().getTime() - timestamp.getTimeInMillis() < THIRTY_DAYS) {
324
                    runTask.accept(() -> loadNanopubToLatest(allStatements));
×
325
                }
326
            }
327

328
            runTask.accept(() -> loadNanopubToRepo(np.getUri(), textStatements, "text"));
×
329
            runTask.accept(() -> loadNanopubToRepo(np.getUri(), allStatements, "full"));
×
330
            // Note: "meta" task is deferred until all other tasks complete successfully
331

332
            runTask.accept(() -> loadNanopubToRepo(np.getUri(), allStatements, "pubkey_" + Utils.createHash(el.getPublicKeyString())));
×
333
            //                loadNanopubToRepo(np.getUri(), textStatements, "text-pubkey_" + Utils.createHash(el.getPublicKeyString()));
334
            for (IRI typeIri : NanopubUtils.getTypes(np)) {
335
                // Exclude locally minted IRIs:
336
                if (typeIri.stringValue().startsWith(np.getUri().stringValue())) continue;
337
                if (!typeIri.stringValue().matches("https?://.*")) continue;
338
                runTask.accept(() -> loadNanopubToRepo(np.getUri(), allStatements, "type_" + Utils.createHash(typeIri)));
×
339
                //                        loadNanopubToRepo(np.getUri(), textStatements, "text-type_" + Utils.createHash(typeIri));
340
            }
341
            //                for (IRI creatorIri : SimpleCreatorPattern.getCreators(np)) {
342
            //                        // Exclude locally minted IRIs:
343
            //                        if (creatorIri.stringValue().startsWith(np.getUri().stringValue())) continue;
344
            //                        if (!creatorIri.stringValue().matches("https?://.*")) continue;
345
            //                        loadNanopubToRepo(np.getUri(), allStatements, "user_" + Utils.createHash(creatorIri));
346
            //                        loadNanopubToRepo(np.getUri(), textStatements, "text-user_" + Utils.createHash(creatorIri));
347
            //                }
348
            //                for (IRI authorIri : SimpleCreatorPattern.getAuthors(np)) {
349
            //                        // Exclude locally minted IRIs:
350
            //                        if (authorIri.stringValue().startsWith(np.getUri().stringValue())) continue;
351
            //                        if (!authorIri.stringValue().matches("https?://.*")) continue;
352
            //                        loadNanopubToRepo(np.getUri(), allStatements, "user_" + Utils.createHash(authorIri));
353
            //                        loadNanopubToRepo(np.getUri(), textStatements, "text-user_" + Utils.createHash(authorIri));
354
            //                }
355

356
            for (Statement st : invalidateStatements) {
357
                runTask.accept(() -> loadInvalidateStatements(np, el.getPublicKeyString(), st, pubkeyStatement, pubkeyStatementX));
×
358
            }
359

360
            // Wait for all non-meta tasks to complete successfully before submitting the meta task
361
            for (var task : runningTasks) {
362
                try {
363
                    task.get();
364
                } catch (ExecutionException | InterruptedException ex) {
365
                    throw new RuntimeException("Error in nanopub loading thread", ex.getCause());
366
                }
367
            }
368

369
            // Now submit and wait for the "meta" task after all other tasks have completed successfully
370
            Future<?> metaTask = loadingPool.submit(() -> loadNanopubToRepo(np.getUri(), metaStatements, "meta"));
×
371
            try {
372
                metaTask.get();
373
            } catch (ExecutionException | InterruptedException ex) {
374
                throw new RuntimeException("Error in nanopub loading thread (meta task)", ex.getCause());
375
            }
376
        }
377
    }
378

379
    private static Long lastUpdateOfLatestRepo = null;
6✔
380
    private static long THIRTY_DAYS = 1000L * 60 * 60 * 24 * 30;
6✔
381
    private static long ONE_HOUR = 1000L * 60 * 60;
6✔
382

383
    @GeneratedFlagForDependentElements
384
    private static void loadNanopubToLatest(List<Statement> statements) {
385
        boolean success = false;
386
        while (!success) {
387
            RepositoryConnection conn = TripleStore.get().getRepoConnection("last30d");
388
            try (conn) {
389
                // Read committed, because deleting old nanopubs is idempotent. Inserts do not collide
390
                // with deletes, because we are not inserting old nanopubs.
391
                conn.begin(IsolationLevels.READ_COMMITTED);
392
                conn.add(statements);
393
                if (lastUpdateOfLatestRepo == null || new Date().getTime() - lastUpdateOfLatestRepo > ONE_HOUR) {
394
                    log.trace("Remove old nanopubs...");
395
                    Literal thirtyDaysAgo = vf.createLiteral(new Date(new Date().getTime() - THIRTY_DAYS));
396
                    TupleQuery q = conn.prepareTupleQuery(QueryLanguage.SPARQL, "SELECT * { graph <" + NPA.GRAPH + "> { " + "?np <" + DCTERMS.CREATED + "> ?date . " + "filter ( ?date < ?thirtydaysago ) " + "} }");
397
                    q.setBinding("thirtydaysago", thirtyDaysAgo);
398
                    try (TupleQueryResult r = q.evaluate()) {
399
                        while (r.hasNext()) {
400
                            BindingSet b = r.next();
401
                            IRI oldNpId = (IRI) b.getBinding("np").getValue();
402
                            log.trace("Remove old nanopub: {}", oldNpId);
403
                            for (Value v : Utils.getObjectsForPattern(conn, NPA.GRAPH, oldNpId, NPA.HAS_GRAPH)) {
404
                                // Remove all four nanopub graphs:
405
                                conn.remove((Resource) null, (IRI) null, (Value) null, (IRI) v);
406
                            }
407
                            // Remove nanopubs in admin graphs:
408
                            conn.remove(oldNpId, null, null, NPA.GRAPH);
409
                            conn.remove(oldNpId, null, null, NPA.NETWORK_GRAPH);
410
                        }
411
                    }
412
                    lastUpdateOfLatestRepo = new Date().getTime();
413
                }
414
                conn.commit();
415
                success = true;
416
            } catch (Exception ex) {
417
                log.info("Could not get environment variable", ex);
418
                if (conn.isActive()) conn.rollback();
419
            }
420
            if (!success) {
421
                log.info("Retrying in 10 second...");
422
                try {
423
                    Thread.sleep(10000);
424
                } catch (InterruptedException x) {
425
                }
426
            }
427
        }
428
    }
429

430
    @GeneratedFlagForDependentElements
431
    private static void loadNanopubToRepo(IRI npId, List<Statement> statements, String repoName) {
432
        boolean success = false;
433
        while (!success) {
434
            RepositoryConnection conn = TripleStore.get().getRepoConnection(repoName);
435
            try (conn) {
436
                // Serializable, because write skew would cause the chain of hashes to be broken.
437
                // The inserts must be done serially.
438
                conn.begin(IsolationLevels.SERIALIZABLE);
439
                var repoStatus = fetchRepoStatus(conn, npId);
440
                if (repoStatus.isLoaded) {
441
                    log.info("Already loaded: {}", npId);
442
                } else {
443
                    String newChecksum = NanopubUtils.updateXorChecksum(npId, repoStatus.checksum);
444
                    conn.remove(NPA.THIS_REPO, NPA.HAS_NANOPUB_COUNT, null, NPA.GRAPH);
445
                    conn.remove(NPA.THIS_REPO, NPA.HAS_NANOPUB_CHECKSUM, null, NPA.GRAPH);
446
                    conn.add(NPA.THIS_REPO, NPA.HAS_NANOPUB_COUNT, vf.createLiteral(repoStatus.count + 1), NPA.GRAPH);
447
                    // @ADMIN-TRIPLE-TABLE@ REPO, npa:hasNanopubCount, NANOPUB_COUNT, npa:graph, admin, number of nanopubs loaded
448
                    conn.add(NPA.THIS_REPO, NPA.HAS_NANOPUB_CHECKSUM, vf.createLiteral(newChecksum), NPA.GRAPH);
449
                    // @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)
450
                    conn.add(npId, NPA.HAS_LOAD_NUMBER, vf.createLiteral(repoStatus.count), NPA.GRAPH);
451
                    // @ADMIN-TRIPLE-TABLE@ NANOPUB, npa:hasLoadNumber, LOAD_NUMBER, npa:graph, admin, the sequential number at which this NANOPUB was loaded
452
                    conn.add(npId, NPA.HAS_LOAD_CHECKSUM, vf.createLiteral(newChecksum), NPA.GRAPH);
453
                    // @ADMIN-TRIPLE-TABLE@ NANOPUB, npa:hasLoadChecksum, LOAD_CHECKSUM, npa:graph, admin, the checksum of all loaded nanopubs after loading the given NANOPUB
454
                    conn.add(npId, NPA.HAS_LOAD_TIMESTAMP, vf.createLiteral(new Date()), NPA.GRAPH);
455
                    // @ADMIN-TRIPLE-TABLE@ NANOPUB, npa:hasLoadTimestamp, LOAD_TIMESTAMP, npa:graph, admin, the time point at which this NANOPUB was loaded
456
                    conn.add(statements);
457
                }
458
                conn.commit();
459
                success = true;
460
            } catch (Exception ex) {
461
                log.info("Could no load nanopub to repo. ", ex);
462
                if (conn.isActive()) conn.rollback();
463
            }
464
            if (!success) {
465
                log.info("Retrying in 10 second...");
466
                try {
467
                    Thread.sleep(10000);
468
                } catch (InterruptedException x) {
469
                }
470
            }
471
        }
472
    }
473

474
    private record RepoStatus(boolean isLoaded, long count, String checksum) {
×
475
    }
476

477
    /**
478
     * To execute before loading a nanopub: check if the nanopub is already loaded and what is the
479
     * current load counter and checksum. This effectively batches three queries into one.
480
     * This method must be called from within a transaction.
481
     *
482
     * @param conn repo connection
483
     * @param npId nanopub ID
484
     * @return the current status
485
     */
486
    @GeneratedFlagForDependentElements
487
    private static RepoStatus fetchRepoStatus(RepositoryConnection conn, IRI npId) {
488
        var result = conn.prepareTupleQuery(QueryLanguage.SPARQL, REPO_STATUS_QUERY_TEMPLATE.formatted(npId)).evaluate();
489
        try (result) {
490
            if (!result.hasNext()) {
491
                // This may happen if the repo was created, but is completely empty.
492
                return new RepoStatus(false, 0, NanopubUtils.INIT_CHECKSUM);
493
            }
494
            var row = result.next();
495
            return new RepoStatus(row.hasBinding("loadNumber"), Long.parseLong(row.getBinding("count").getValue().stringValue()), row.getBinding("checksum").getValue().stringValue());
496
        }
497
    }
498

499
    @GeneratedFlagForDependentElements
500
    private static void loadInvalidateStatements(Nanopub thisNp, String thisPubkey, Statement invalidateStatement, Statement pubkeyStatement, Statement pubkeyStatementX) {
501
        boolean success = false;
502
        while (!success) {
503
            List<RepositoryConnection> connections = new ArrayList<>();
504
            RepositoryConnection metaConn = TripleStore.get().getRepoConnection("meta");
505
            try {
506
                IRI invalidatedNpId = (IRI) invalidateStatement.getObject();
507
                // Basic isolation because here we only read append-only data.
508
                metaConn.begin(IsolationLevels.READ_COMMITTED);
509

510
                Value pubkeyValue = Utils.getObjectForPattern(metaConn, NPA.GRAPH, invalidatedNpId, NPA.HAS_VALID_SIGNATURE_FOR_PUBLIC_KEY);
511
                if (pubkeyValue != null) {
512
                    String pubkey = pubkeyValue.stringValue();
513

514
                    if (!pubkey.equals(thisPubkey)) {
515
                        //log.info("Adding invalidation expressed in " + thisNp.getUri() + " also to repo for pubkey " + pubkey);
516
                        connections.add(loadStatements("pubkey_" + Utils.createHash(pubkey), invalidateStatement, pubkeyStatement, pubkeyStatementX));
517
//                                                connections.add(loadStatements("text-pubkey_" + Utils.createHash(pubkey), invalidateStatement, pubkeyStatement));
518
                    }
519

520
                    for (Value v : Utils.getObjectsForPattern(metaConn, NPA.GRAPH, invalidatedNpId, NPX.HAS_NANOPUB_TYPE)) {
521
                        IRI typeIri = (IRI) v;
522
                        // TODO Avoid calling getTypes and getCreators multiple times:
523
                        if (!NanopubUtils.getTypes(thisNp).contains(typeIri)) {
524
                            //log.info("Adding invalidation expressed in " + thisNp.getUri() + " also to repo for type " + typeIri);
525
                            connections.add(loadStatements("type_" + Utils.createHash(typeIri), invalidateStatement, pubkeyStatement, pubkeyStatementX));
526
//                                                        connections.add(loadStatements("text-type_" + Utils.createHash(typeIri), invalidateStatement, pubkeyStatement));
527
                        }
528
                    }
529

530
//                                        for (Value v : Utils.getObjectsForPattern(metaConn, NPA.GRAPH, invalidatedNpId, DCTERMS.CREATOR)) {
531
//                                                IRI creatorIri = (IRI) v;
532
//                                                if (!SimpleCreatorPattern.getCreators(thisNp).contains(creatorIri)) {
533
//                                                        //log.info("Adding invalidation expressed in " + thisNp.getUri() + " also to repo for user " + creatorIri);
534
//                                                        connections.add(loadStatements("user_" + Utils.createHash(creatorIri), invalidateStatement, pubkeyStatement));
535
//                                                        connections.add(loadStatements("text-user_" + Utils.createHash(creatorIri), invalidateStatement, pubkeyStatement));
536
//                                                }
537
//                                        }
538
                }
539

540
                metaConn.commit();
541
                // TODO handle case that some commits succeed and some fail
542
                for (RepositoryConnection c : connections) c.commit();
543
                success = true;
544
            } catch (Exception ex) {
545
                log.info("Could no load invalidate statements. ", ex);
546
                if (metaConn.isActive()) metaConn.rollback();
547
                for (RepositoryConnection c : connections) {
548
                    if (c.isActive()) c.rollback();
549
                }
550
            } finally {
551
                metaConn.close();
552
                for (RepositoryConnection c : connections) c.close();
553
            }
554
            if (!success) {
555
                log.info("Retrying in 10 second...");
556
                try {
557
                    Thread.sleep(10000);
558
                } catch (InterruptedException x) {
559
                }
560
            }
561
        }
562
    }
563

564
    @GeneratedFlagForDependentElements
565
    private static RepositoryConnection loadStatements(String repoName, Statement... statements) {
566
        RepositoryConnection conn = TripleStore.get().getRepoConnection(repoName);
567
        // Basic isolation: we only append new statements
568
        conn.begin(IsolationLevels.READ_COMMITTED);
569
        for (Statement st : statements) {
570
            conn.add(st);
571
        }
572
        return conn;
573
    }
574

575
    @GeneratedFlagForDependentElements
576
    static List<Statement> getInvalidatingStatements(IRI npId) {
577
        List<Statement> invalidatingStatements = new ArrayList<>();
578
        boolean success = false;
579
        while (!success) {
580
            RepositoryConnection conn = TripleStore.get().getRepoConnection("meta");
581
            try (conn) {
582
                // Basic isolation because here we only read append-only data.
583
                conn.begin(IsolationLevels.READ_COMMITTED);
584

585
                TupleQueryResult r = conn.prepareTupleQuery(QueryLanguage.SPARQL, "SELECT * { graph <" + NPA.GRAPH + "> { " + "?np <" + NPX.INVALIDATES + "> <" + npId + "> ; <" + NPA.HAS_VALID_SIGNATURE_FOR_PUBLIC_KEY + "> ?pubkey . " + "} }").evaluate();
586
                try (r) {
587
                    while (r.hasNext()) {
588
                        BindingSet b = r.next();
589
                        invalidatingStatements.add(vf.createStatement((IRI) b.getBinding("np").getValue(), NPX.INVALIDATES, npId, NPA.GRAPH));
590
                        invalidatingStatements.add(vf.createStatement((IRI) b.getBinding("np").getValue(), NPA.HAS_VALID_SIGNATURE_FOR_PUBLIC_KEY, b.getBinding("pubkey").getValue(), NPA.GRAPH));
591
                    }
592
                }
593
                conn.commit();
594
                success = true;
595
            } catch (Exception ex) {
596
                log.info("Could no load invalidating statements. ", ex);
597
                if (conn.isActive()) conn.rollback();
598
            }
599
            if (!success) {
600
                log.info("Retrying in 10 second...");
601
                try {
602
                    Thread.sleep(10000);
603
                } catch (InterruptedException x) {
604
                }
605
            }
606
        }
607
        return invalidatingStatements;
608
    }
609

610
    @GeneratedFlagForDependentElements
611
    private static void loadNoteToRepo(Resource subj, String note) {
612
        boolean success = false;
613
        while (!success) {
614
            RepositoryConnection conn = TripleStore.get().getAdminRepoConnection();
615
            try (conn) {
616
                List<Statement> statements = new ArrayList<>();
617
                statements.add(vf.createStatement(subj, NPA.NOTE, vf.createLiteral(note), NPA.GRAPH));
618
                conn.add(statements);
619
                success = true;
620
            } catch (Exception ex) {
621
                log.info("Could no load note to repo. ", ex);
622
            }
623
            if (!success) {
624
                log.info("Retrying in 10 second...");
625
                try {
626
                    Thread.sleep(10000);
627
                } catch (InterruptedException x) {
628
                }
629
            }
630
        }
631
    }
632

633
    static boolean hasValidSignature(NanopubSignatureElement el) {
634
        try {
635
            if (el != null && SignatureUtils.hasValidSignature(el) && el.getPublicKeyString() != null) {
24!
636
                return true;
6✔
637
            }
638
        } catch (GeneralSecurityException ex) {
3✔
639
            log.info("Error for signature element {}", el.getUri());
15✔
640
            log.info("Error", ex);
12✔
641
        }
3✔
642
        return false;
6✔
643
    }
644

645
    private static IRI getBaseTrustyUri(Value v) {
646
        if (!(v instanceof IRI)) return null;
9!
647
        String s = v.stringValue();
9✔
648
        if (!s.matches(".*[^A-Za-z0-9\\-_]RA[A-Za-z0-9\\-_]{43}([^A-Za-z0-9\\\\-_].{0,43})?")) {
12✔
649
            return null;
6✔
650
        }
651
        return vf.createIRI(s.replaceFirst("^(.*[^A-Za-z0-9\\-_]RA[A-Za-z0-9\\-_]{43})([^A-Za-z0-9\\\\-_].{0,43})?$", "$1"));
21✔
652
    }
653

654
    // TODO: Move this to nanopub library:
655
    private static boolean isIntroNanopub(Nanopub np) {
656
        for (Statement st : np.getAssertion()) {
33✔
657
            if (st.getPredicate().equals(NPX.DECLARED_BY)) return true;
21✔
658
        }
3✔
659
        return false;
6✔
660
    }
661

662
    /**
663
     * Check if a nanopub is already loaded in the admin graph.
664
     *
665
     * @param npId the nanopub ID
666
     * @return true if the nanopub is loaded, false otherwise
667
     */
668
    @GeneratedFlagForDependentElements
669
    static boolean isNanopubLoaded(String npId) {
670
        boolean loaded = false;
671
        RepositoryConnection conn = TripleStore.get().getRepoConnection("meta");
672
        try (conn) {
673
            if (Utils.getObjectForPattern(conn, NPA.GRAPH, vf.createIRI(npId), NPA.HAS_LOAD_NUMBER) != null) {
674
                loaded = true;
675
            }
676
        } catch (Exception ex) {
677
            log.info("Could no load nanopub. ", ex);
678
        }
679
        return loaded;
680
    }
681

682
    private static ValueFactory vf = SimpleValueFactory.getInstance();
6✔
683

684
    // TODO remove the constants and use the ones from the nanopub library instead
685

686
    /**
687
     * Template for the query that fetches the status of a repository.
688
     */
689
    // Template for .fetchRepoStatus
690
    private static final String REPO_STATUS_QUERY_TEMPLATE = """
84✔
691
            SELECT * { graph <%s> {
692
              OPTIONAL { <%s> <%s> ?loadNumber . }
693
              <%s> <%s> ?count ;
694
                   <%s> ?checksum .
695
            } }
696
            """.formatted(NPA.GRAPH, "%s", NPA.HAS_LOAD_NUMBER, NPA.THIS_REPO, NPA.HAS_NANOPUB_COUNT, NPA.HAS_NANOPUB_CHECKSUM);
6✔
697
}
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