• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In
Build has been canceled!

knowledgepixels / nanopub-registry / 24727603152

21 Apr 2026 02:18PM UTC coverage: 31.808% (+0.3%) from 31.558%
24727603152

push

github

ashleycaselli
chore(logging): enhance info error handling in Nanopub processing

280 of 986 branches covered (28.4%)

Branch coverage included in aggregate %.

839 of 2532 relevant lines covered (33.14%)

5.5 hits per line

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

66.76
src/main/java/com/knowledgepixels/registry/RegistryDB.java
1
package com.knowledgepixels.registry;
2

3
import com.knowledgepixels.registry.db.IndexInitializer;
4
import com.mongodb.*;
5
import com.mongodb.client.ClientSession;
6
import com.mongodb.client.MongoCollection;
7
import com.mongodb.client.MongoCursor;
8
import com.mongodb.client.MongoDatabase;
9
import com.mongodb.client.model.CountOptions;
10
import com.mongodb.client.model.FindOneAndUpdateOptions;
11
import com.mongodb.client.model.ReturnDocument;
12
import com.mongodb.client.model.UpdateOptions;
13
import net.trustyuri.TrustyUriUtils;
14
import org.bson.Document;
15
import org.bson.conversions.Bson;
16
import org.bson.types.Binary;
17
import org.eclipse.rdf4j.common.exception.RDF4JException;
18
import org.eclipse.rdf4j.model.IRI;
19
import org.eclipse.rdf4j.rio.RDFFormat;
20
import org.nanopub.MalformedNanopubException;
21
import org.nanopub.Nanopub;
22
import org.nanopub.NanopubUtils;
23
import org.nanopub.extra.security.MalformedCryptoElementException;
24
import org.nanopub.extra.security.NanopubSignatureElement;
25
import org.nanopub.extra.security.SignatureUtils;
26
import org.nanopub.jelly.JellyUtils;
27
import org.slf4j.Logger;
28
import org.slf4j.LoggerFactory;
29

30
import java.io.IOException;
31
import java.security.GeneralSecurityException;
32
import java.util.ArrayList;
33
import java.util.Calendar;
34

35
import static com.mongodb.client.model.Indexes.ascending;
36

37
public class RegistryDB {
38

39
    private RegistryDB() {
40
    }
41

42
    private static final String REGISTRY_DB_NAME = Utils.getEnv("REGISTRY_DB_NAME", "nanopubRegistry");
12✔
43

44
    private static final Logger logger = LoggerFactory.getLogger(RegistryDB.class);
9✔
45

46
    private static MongoClient mongoClient;
47
    private static MongoDatabase mongoDB;
48

49
    /**
50
     * Returns the MongoDB database instance.
51
     *
52
     * @return the MongoDatabase instance
53
     */
54
    public static MongoDatabase getDB() {
55
        return mongoDB;
6✔
56
    }
57

58
    /**
59
     * Returns the MongoDB client instance.
60
     *
61
     * @return the MongoClient instance
62
     */
63
    public static MongoClient getClient() {
64
        return mongoClient;
6✔
65
    }
66

67
    /**
68
     * Returns the specified collection from the MongoDB database.
69
     *
70
     * @param name the name of the collection
71
     * @return the MongoCollection instance
72
     */
73
    public static MongoCollection<Document> collection(String name) {
74
        return mongoDB.getCollection(name);
12✔
75
    }
76

77
    /**
78
     * Initializes the MongoDB connection and sets up collections and indexes if not already initialized.
79
     */
80
    public static void init() {
81
        if (mongoClient != null) {
6✔
82
            logger.info("RegistryDB already initialized");
9✔
83
            return;
3✔
84
        }
85
        final String REGISTRY_DB_HOST = Utils.getEnv("REGISTRY_DB_HOST", "mongodb");
12✔
86
        final int REGISTRY_DB_PORT = Integer.parseInt(Utils.getEnv("REGISTRY_DB_PORT", String.valueOf(ServerAddress.defaultPort())));
18✔
87
        logger.info("Initializing RegistryDB connection to database '{}' at {}:{}", REGISTRY_DB_NAME, REGISTRY_DB_HOST, REGISTRY_DB_PORT);
54✔
88
        mongoClient = new MongoClient(REGISTRY_DB_HOST, REGISTRY_DB_PORT);
18✔
89
        mongoDB = mongoClient.getDatabase(REGISTRY_DB_NAME);
12✔
90

91
        try (ClientSession mongoSession = mongoClient.startSession()) {
9✔
92
            if (!isInitialized(mongoSession)) {
9!
93
                IndexInitializer.initCollections(mongoSession);
6✔
94
            }
95
            initCounter(mongoSession);
6✔
96
        }
97
    }
3✔
98

99
    /**
100
     * Checks if the database has been initialized.
101
     *
102
     * @param mongoSession the MongoDB client session
103
     * @return true if initialized, false otherwise
104
     */
105
    public static boolean isInitialized(ClientSession mongoSession) {
106
        return getValue(mongoSession, Collection.SERVER_INFO.toString(), "setupId") != null;
24!
107
    }
108

109
    /**
110
     * Renames a collection in the database. If the new collection name already exists, it will be dropped first.
111
     *
112
     * @param oldCollectionName the current name of the collection
113
     * @param newCollectionName the new name for the collection
114
     */
115
    public static void rename(String oldCollectionName, String newCollectionName) {
116
        // Designed as idempotent operation: calling multiple times has same effect as calling once
117
        if (hasCollection(oldCollectionName)) {
9✔
118
            if (hasCollection(newCollectionName)) {
9✔
119
                collection(newCollectionName).drop();
9✔
120
            }
121
            collection(oldCollectionName).renameCollection(new MongoNamespace(REGISTRY_DB_NAME, newCollectionName));
24✔
122
        }
123
    }
3✔
124

125
    /**
126
     * Checks if a collection with the given name exists in the database.
127
     *
128
     * @param collectionName the name of the collection to check
129
     * @return true if the collection exists, false otherwise
130
     */
131
    public static boolean hasCollection(String collectionName) {
132
        return mongoDB.listCollectionNames().into(new ArrayList<>()).contains(collectionName);
30✔
133
    }
134

135
    /**
136
     * Increases the trust state counter in the server info collection.
137
     *
138
     * @param mongoSession the MongoDB client session
139
     */
140
    public static void increaseStateCounter(ClientSession mongoSession) {
141
        MongoCursor<Document> cursor = collection(Collection.SERVER_INFO.toString()).find(mongoSession, new Document("_id", "trustStateCounter")).cursor();
36✔
142
        if (cursor.hasNext()) {
9✔
143
            long counter = cursor.next().getLong("value");
21✔
144
            collection(Collection.SERVER_INFO.toString()).updateOne(mongoSession, new Document("_id", "trustStateCounter"), new Document("$set", new Document("value", counter + 1)));
69✔
145
        } else {
3✔
146
            collection(Collection.SERVER_INFO.toString()).insertOne(mongoSession, new Document("_id", "trustStateCounter").append("value", 0L));
42✔
147
        }
148
    }
3✔
149

150
    /**
151
     * Checks if an element with the given name exists in the specified collection.
152
     *
153
     * @param mongoSession the MongoDB client session
154
     * @param collection   the name of the collection
155
     * @param elementName  the name of the element used as the _id field
156
     * @return true if the element exists, false otherwise
157
     */
158
    public static boolean has(ClientSession mongoSession, String collection, String elementName) {
159
        return has(mongoSession, collection, new Document("_id", elementName));
27✔
160
    }
161

162
    private static final CountOptions hasCountOptions = new CountOptions().limit(1);
21✔
163

164
    /**
165
     * Checks if any document matching the given filter exists in the specified collection.
166
     *
167
     * @param mongoSession the MongoDB client session
168
     * @param collection   the name of the collection
169
     * @param find         the filter to match documents
170
     * @return true if at least one matching document exists, false otherwise
171
     */
172
    public static boolean has(ClientSession mongoSession, String collection, Bson find) {
173
        return collection(collection).countDocuments(mongoSession, find, hasCountOptions) > 0;
39✔
174
    }
175

176
    /**
177
     * Retrieves a cursor for documents matching the given filter in the specified collection.
178
     *
179
     * @param mongoSession the MongoDB client session
180
     * @param collection   the name of the collection
181
     * @param find         the filter to match documents
182
     * @return a MongoCursor for the matching documents
183
     */
184
    public static MongoCursor<Document> get(ClientSession mongoSession, String collection, Bson find) {
185
        return collection(collection).find(mongoSession, find).cursor();
21✔
186
    }
187

188
    /**
189
     * Retrieves the value of an element with the given name from the specified collection.
190
     *
191
     * @param mongoSession the MongoDB client session
192
     * @param collection   the name of the collection
193
     * @param elementName  the name of the element used as the _id field
194
     * @return the value of the element, or null if not found
195
     */
196
    public static Object getValue(ClientSession mongoSession, String collection, String elementName) {
197
        logger.debug("Reading value of element '{}' from collection '{}'", elementName, collection);
15✔
198
        Document d = collection(collection).find(mongoSession, new Document("_id", elementName)).first();
36✔
199
        if (d == null) {
6✔
200
            return null;
6✔
201
        }
202
        return d.get("value");
12✔
203
    }
204

205
    /**
206
     * Retrieves the boolean value of an element with the given name from the specified collection.
207
     *
208
     * @param mongoSession the MongoDB client session
209
     * @param collection   the name of the collection
210
     * @param elementName  the name of the element used as the _id field
211
     * @return the value of the element, or null if not found
212
     */
213
    public static boolean isSet(ClientSession mongoSession, String collection, String elementName) {
214
        Document d = collection(collection).find(mongoSession, new Document("_id", elementName)).first();
36✔
215
        if (d == null) {
6✔
216
            return false;
6✔
217
        }
218
        return d.getBoolean("value");
15✔
219
    }
220

221
    /**
222
     * Retrieves a single document matching the given filter from the specified collection.
223
     *
224
     * @param mongoSession the MongoDB client session
225
     * @param collection   the name of the collection
226
     * @param find         the filter to match the document
227
     * @return the matching document, or null if not found
228
     */
229
    public static Document getOne(ClientSession mongoSession, String collection, Bson find) {
230
        return collection(collection).find(mongoSession, find).first();
24✔
231
    }
232

233
    /**
234
     * Retrieves the maximum value of a specified field from the documents in the given collection.
235
     *
236
     * @param mongoSession the MongoDB client session
237
     * @param collection   the name of the collection
238
     * @param fieldName    the field for which to find the maximum value
239
     * @return the maximum value of the specified field, or null if no documents exist
240
     */
241
    public static Object getMaxValue(ClientSession mongoSession, String collection, String fieldName) {
242
        Document doc = collection(collection).find(mongoSession).projection(new Document(fieldName, 1)).sort(new Document(fieldName, -1)).first();
63✔
243
        if (doc == null) {
6✔
244
            return null;
6✔
245
        }
246
        return doc.get(fieldName);
12✔
247
    }
248

249
    /**
250
     * Retrieves the document with the maximum value of a specified field from the documents matching the given filter in the specified collection.
251
     *
252
     * @param mongoSession the MongoDB client session
253
     * @param collection   the name of the collection
254
     * @param find         the filter to match documents
255
     * @param fieldName    the field for which to find the maximum value
256
     * @return the document with the maximum value of the specified field, or null if no matching documents exist
257
     */
258
    public static Document getMaxValueDocument(ClientSession mongoSession, String collection, Bson find, String fieldName) {
259
        return collection(collection).find(mongoSession, find).sort(new Document(fieldName, -1)).first();
45✔
260
    }
261

262
    /**
263
     * Sets or updates a document in the specified collection.
264
     *
265
     * @param mongoSession the MongoDB client session
266
     * @param collection   the name of the collection
267
     * @param update       the document to set or update (must contain an _id field)
268
     */
269
    public static void set(ClientSession mongoSession, String collection, Document update) {
270
        Bson find = new Document("_id", update.get("_id"));
24✔
271
        MongoCursor<Document> cursor = collection(collection).find(mongoSession, find).cursor();
21✔
272
        if (cursor.hasNext()) {
9✔
273
            collection(collection).updateOne(mongoSession, find, new Document("$set", update));
33✔
274
        }
275
    }
3✔
276

277
    /**
278
     * Inserts a document into the specified collection.
279
     *
280
     * @param mongoSession the MongoDB client session
281
     * @param collection   the name of the collection
282
     * @param doc          the document to insert
283
     */
284
    public static void insert(ClientSession mongoSession, String collection, Document doc) {
285
        collection(collection).insertOne(mongoSession, doc);
15✔
286
    }
3✔
287

288
    /**
289
     * Sets the value of an element with the given name in the specified collection.
290
     * If the element does not exist, it will be created.
291
     *
292
     * @param mongoSession the MongoDB client session
293
     * @param collection   the name of the collection
294
     * @param elementId    the name of the element used as the _id field
295
     * @param value        the value to set
296
     */
297
    public static void setValue(ClientSession mongoSession, String collection, String elementId, Object value) {
298
        collection(collection).updateOne(mongoSession, new Document("_id", elementId), new Document("$set", new Document("value", value)), new UpdateOptions().upsert(true));
72✔
299
    }
3✔
300

301
    /**
302
     * Records the hash of a given value in the "hashes" collection.
303
     * Uses upsert to avoid expensive exception-based duplicate handling.
304
     *
305
     * @param mongoSession the MongoDB client session
306
     * @param value        the value to hash and record
307
     */
308
    public static void recordHash(ClientSession mongoSession, String value) {
309
        String hash = Utils.getHash(value);
9✔
310
        try {
311
            collection("hashes").updateOne(mongoSession, new Document("value", value), new Document("$setOnInsert", new Document("value", value).append("hash", hash)), new UpdateOptions().upsert(true));
81✔
312
        } catch (MongoWriteException e) {
×
313
            // Concurrent upsert race: another thread inserted the same hash — safe to ignore
314
            if (e.getError().getCategory() != ErrorCategory.DUPLICATE_KEY) {
×
315
                throw e;
×
316
            }
317
        }
3✔
318
    }
3✔
319

320
    /**
321
     * Retrieves the original value corresponding to a given hash from the "hashes" collection.
322
     *
323
     * @param hash the hash to look up
324
     * @return the original value, or null if not found
325
     */
326
    public static String unhash(String hash) {
327
        try (var c = collection("hashes").find(new Document("hash", hash)).cursor()) {
30✔
328
            if (c.hasNext()) {
9✔
329
                return c.next().getString("value");
24✔
330
            }
331
            return null;
12✔
332
        }
12!
333
    }
334

335
    /**
336
     * Initializes the counter document to the current maximum counter value
337
     * in the nanopubs collection.
338
     * Uses $max to ensure the counter is never decreased. Safe to call on every startup.
339
     */
340
    private static void initCounter(ClientSession mongoSession) {
341
        Long maxCounter = (Long) getMaxValue(mongoSession, Collection.NANOPUBS.toString(), "counter");
21✔
342
        long effective = maxCounter != null ? maxCounter : 0L;
21✔
343
        collection("counters").updateOne(mongoSession, new Document("_id", "nanopubs"), new Document("$max", new Document("value", effective)), new UpdateOptions().upsert(true));
75✔
344
        if (maxCounter != null) {
6✔
345
            logger.info("Nanopub counter resumed at {} (max found in DB)", effective);
18✔
346
        } else {
347
            logger.info("Nanopub counter initialized to 0 (no existing nanopubs found)");
9✔
348
        }
349
    }
3✔
350

351
    /**
352
     * Returns the next counter value for a nanopub via atomic increment.
353
     */
354
    private static long getNextCounter(ClientSession mongoSession) {
355
        Document result = collection("counters").findOneAndUpdate(mongoSession, new Document("_id", "nanopubs"), new Document("$inc", new Document("value", 1L)), new FindOneAndUpdateOptions().upsert(true).returnDocument(ReturnDocument.AFTER));
84✔
356
        return result.getLong("value");
15✔
357
    }
358

359
    /**
360
     * Loads a nanopublication into the database.
361
     *
362
     * @param mongoSession the MongoDB client session
363
     * @param nanopub      the nanopublication to load
364
     */
365
    public static boolean loadNanopub(ClientSession mongoSession, Nanopub nanopub) {
366
        return loadNanopub(mongoSession, nanopub, null);
21✔
367
    }
368

369
    /**
370
     * Loads a nanopublication into the database, optionally filtering by public key hash and types.
371
     *
372
     * @param mongoSession the MongoDB client session
373
     * @param nanopub      the nanopublication to load
374
     * @param pubkeyHash   the public key hash to filter by (can be null)
375
     * @param types        the types to filter by (can be empty)
376
     * @return true if the nanopublication was loaded, false otherwise
377
     */
378
    public static boolean loadNanopub(ClientSession mongoSession, Nanopub nanopub, String pubkeyHash, String... types) {
379
        String pubkey = getPubkey(nanopub);
9✔
380
        if (pubkey == null) {
6✔
381
            logger.warn("Ignoring nanopub {}: no valid public key / signature found", nanopub.getUri());
15✔
382
            return false;
6✔
383
        }
384
        return loadNanopubVerified(mongoSession, nanopub, pubkey, pubkeyHash, types);
21✔
385
    }
386

387
    /**
388
     * Loads a nanopublication with a pre-verified public key, skipping signature verification.
389
     * Use this when the caller has already verified the signature via getPubkey().
390
     */
391
    static boolean loadNanopubVerified(ClientSession mongoSession, Nanopub nanopub, String verifiedPubkey, String pubkeyHash, String... types) {
392
        if (nanopub.getTripleCount() > 1200) {
12!
393
            logger.error("Rejecting nanopub {}: triple count {} exceeds limit of 1200", nanopub.getUri(), nanopub.getTripleCount());
×
394
            return false;
×
395
        }
396
        if (nanopub.getByteCount() > 1000000) {
15!
397
            logger.error("Rejecting nanopub {}: size {} bytes exceeds limit of 1000000", nanopub.getUri(), nanopub.getByteCount());
×
398
            return false;
×
399
        }
400
        Calendar creationTime;
401
        try {
402
            creationTime = nanopub.getCreationTime();
9✔
403
        } catch (Exception ex) {
×
404
            logger.warn("Nanopub {} has a malformed timestamp; proceeding without one", nanopub.getUri());
×
405
            creationTime = null;
×
406
        }
3✔
407
        if (creationTime != null && creationTime.getTimeInMillis() > System.currentTimeMillis() + 60000) {
27!
408
            logger.error("Rejecting nanopub {}: timestamp {} is more than 60s in the future", nanopub.getUri(), creationTime.toInstant());
×
409
            return false;
×
410
        }
411
        String nanopubUriStr = nanopub.getUri().stringValue();
12✔
412
        for (IRI graphUri : nanopub.getGraphUris()) {
33✔
413
            if (!graphUri.stringValue().startsWith(nanopubUriStr)) {
15!
414
                logger.error("Rejecting nanopub {}: graph URI {} does not start with the nanopub base URI", nanopub.getUri(), graphUri);
×
415
                return false;
×
416
            }
417
        }
3✔
418
        String ph = Utils.getHash(verifiedPubkey);
9✔
419
        if (pubkeyHash != null && !pubkeyHash.equals(ph)) {
18!
420
            logger.error("Rejecting nanopub {}: provided pubkey hash {} does not match computed hash {}", nanopub.getUri(), pubkeyHash, ph);
×
421
            return false;
×
422
        }
423
        recordHash(mongoSession, verifiedPubkey);
9✔
424

425
        String ac = TrustyUriUtils.getArtifactCode(nanopub.getUri().stringValue());
15✔
426
        if (ac == null) {
6!
427
            // I don't think this ever happens, but checking here to be sure
428
            logger.error("Rejecting nanopub {}: could not extract artifact code from Trusty URI", nanopub.getUri());
×
429
            return false;
×
430
        }
431
        if (has(mongoSession, Collection.NANOPUBS.toString(), ac)) {
18✔
432
            logger.debug("Skipping nanopub {}: already present in the database", nanopub.getUri());
18✔
433
        } else {
434
            String nanopubString;
435
            byte[] jellyContent;
436
            try {
437
                nanopubString = NanopubUtils.writeToString(nanopub, RDFFormat.TRIG);
12✔
438
                // Save the same thing in the Jelly format for faster loading
439
                jellyContent = JellyUtils.writeNanopubForDB(nanopub);
9✔
440
            } catch (IOException ex) {
×
441
                throw new RuntimeException(ex);
×
442
            }
3✔
443
            long counter = getNextCounter(mongoSession);
9✔
444
            boolean inserted = false;
6✔
445
            try {
446
                collection(Collection.NANOPUBS.toString()).insertOne(mongoSession, new Document("_id", ac).append("fullId", nanopub.getUri().stringValue()).append("counter", counter).append("pubkey", ph).append("content", nanopubString).append("jelly", new Binary(jellyContent)));
93✔
447
                inserted = true;
6✔
448
                logger.info("Loaded nanopub {} (counter: {}, pubkey hash: {})", nanopub.getUri(), counter, ph);
57✔
449
            } catch (MongoWriteException e) {
×
450
                if (e.getError().getCategory() != ErrorCategory.DUPLICATE_KEY) {
×
451
                    throw e;
×
452
                }
453
                // Another thread inserted this nanopub concurrently — safe to skip
454
                logger.debug("Skipping nanopub {}: inserted concurrently by another thread", nanopub.getUri());
×
455
            }
3✔
456

457
            if (inserted) {
6!
458
                for (IRI invalidatedId : Utils.getInvalidatedNanopubIds(nanopub)) {
33✔
459
                    String invalidatedAc = TrustyUriUtils.getArtifactCode(invalidatedId.stringValue());
12✔
460
                    if (invalidatedAc == null) {
6!
461
                        logger.warn("Nanopub {} references invalidated nanopub {} with an unresolvable artifact code; skipping", nanopub.getUri(), invalidatedId);
×
462
                        continue;  // This should never happen; checking here just to be sure
×
463
                    }
464

465
                    // Add this nanopub also to all lists of invalidated nanopubs:
466
                    logger.debug("Nanopub {} invalidates {}; updating list entries and trust edges", nanopub.getUri(), invalidatedId);
18✔
467
                    collection("invalidations").insertOne(mongoSession, new Document("invalidatingNp", ac).append("invalidatingPubkey", ph).append("invalidatedNp", invalidatedAc));
45✔
468
                    MongoCursor<Document> invalidatedEntries = collection("listEntries").find(mongoSession, new Document("np", invalidatedAc).append("pubkey", ph)).cursor();
42✔
469
                    while (invalidatedEntries.hasNext()) {
9!
470
                        Document invalidatedEntry = invalidatedEntries.next();
×
471
                        addToList(mongoSession, nanopub, ph, invalidatedEntry.getString("type"));
×
472
                    }
×
473

474
                    collection("listEntries").updateMany(mongoSession, new Document("np", invalidatedAc).append("pubkey", ph), new Document("$set", new Document("invalidated", true)));
69✔
475
                    collection("trustEdges").updateMany(mongoSession, new Document("source", invalidatedAc), new Document("$set", new Document("invalidated", true)));
60✔
476
                }
3✔
477
            }
478
        }
479

480
        if (pubkeyHash != null) {
6✔
481
            for (String type : types) {
48✔
482
                // TODO Check if nanopub really has the type?
483
                addToList(mongoSession, nanopub, pubkeyHash, Utils.getTypeHash(mongoSession, type));
21✔
484
                if (type.equals("$")) {
12!
485
                    for (IRI t : NanopubUtils.getTypes(nanopub)) {
33✔
486
                        String th = Utils.getTypeHash(mongoSession, t);
12✔
487
                        if (CoverageFilter.isCoveredType(th)) {
9!
488
                            addToList(mongoSession, nanopub, pubkeyHash, th);
15✔
489
                        }
490
                    }
3✔
491
                }
492
            }
493
        }
494

495
        // Add the invalidating nanopubs also to the lists of this nanopub:
496
        try (MongoCursor<Document> invalidations = collection("invalidations").find(mongoSession, new Document("invalidatedNp", ac).append("invalidatingPubkey", ph)).cursor()) {
42✔
497
            if (invalidations.hasNext()) {
9!
498
                collection("listEntries").updateMany(mongoSession, new Document("np", ac).append("pubkey", ph), new Document("$set", new Document("invalidated", true)));
×
499
                collection("trustEdges").updateMany(mongoSession, new Document("source", ac), new Document("$set", new Document("invalidated", true)));
×
500
            }
501
            while (invalidations.hasNext()) {
9!
502
                String iac = invalidations.next().getString("invalidatingNp");
×
503
                try {
504
                    Document npDoc = collection(Collection.NANOPUBS.toString()).find(mongoSession, new Document("_id", iac)).projection(new Document("jelly", 1)).first();
×
505
                    Nanopub inp = JellyUtils.readFromDB(npDoc.get("jelly", Binary.class).getData());
×
506
                    for (IRI type : NanopubUtils.getTypes(inp)) {
×
507
                        addToList(mongoSession, inp, ph, Utils.getTypeHash(mongoSession, type));
×
508
                    }
×
509
                } catch (RDF4JException | MalformedNanopubException ex) {
×
510
                    logger.error("Failed to load invalidating nanopub {} for invalidation record; skipping", iac, ex);
×
511
                }
×
512
            }
×
513

514
        }
515

516
        return true;
6✔
517
    }
518

519
    private static void addToList(ClientSession mongoSession, Nanopub nanopub, String pubkeyHash, String typeHash) {
520
        String ac = TrustyUriUtils.getArtifactCode(nanopub.getUri().stringValue());
15✔
521
        try {
522
            insert(mongoSession, "lists", new Document("pubkey", pubkeyHash).append("type", typeHash).append("maxPosition", -1L));
45✔
523
        } catch (MongoWriteException e) {
×
524
            // Duplicate key error -- ignore it
525
            if (e.getError().getCategory() != ErrorCategory.DUPLICATE_KEY) {
×
526
                throw e;
×
527
            }
528
        }
3✔
529

530
        if (has(mongoSession, "listEntries", new Document("pubkey", pubkeyHash).append("type", typeHash).append("np", ac))) {
45!
531
            logger.debug("Already listed: {}", nanopub.getUri());
×
532
        } else {
533
            initListPositionIfNeeded(mongoSession, pubkeyHash, typeHash);
12✔
534

535
            for (int attempt = 0; ; attempt++) {
6✔
536
                // Atomically claim next position
537
                Document updated = collection("lists").findOneAndUpdate(mongoSession, new Document("pubkey", pubkeyHash).append("type", typeHash), new Document("$inc", new Document("maxPosition", 1L)), new FindOneAndUpdateOptions().returnDocument(ReturnDocument.AFTER));
87✔
538
                long position = updated.getLong("maxPosition");
15✔
539

540
                // Get checksum from previous entry by exact position lookup (O(1) index hit)
541
                String checksum;
542
                if (position == 0) {
12!
543
                    checksum = NanopubUtils.updateXorChecksum(nanopub.getUri(), NanopubUtils.INIT_CHECKSUM);
18✔
544
                } else {
545
                    Document prevEntry = collection("listEntries").find(mongoSession, new Document("pubkey", pubkeyHash).append("type", typeHash).append("position", position - 1)).first();
×
546
                    String prevChecksum = (prevEntry != null) ? prevEntry.getString("checksum") : null;
×
547
                    if (prevChecksum == null) {
×
548
                        // Rare: previous entry not yet inserted by concurrent thread; fall back to sorted query
549
                        Document maxDoc = getMaxValueDocument(mongoSession, "listEntries", new Document("pubkey", pubkeyHash).append("type", typeHash), "position");
×
550
                        prevChecksum = (maxDoc != null) ? maxDoc.getString("checksum") : NanopubUtils.INIT_CHECKSUM;
×
551
                    }
552
                    checksum = NanopubUtils.updateXorChecksum(nanopub.getUri(), prevChecksum);
×
553
                }
554

555
                try {
556
                    collection("listEntries").insertOne(mongoSession, new Document("pubkey", pubkeyHash).append("type", typeHash).append("position", position).append("np", ac).append("checksum", checksum).append("invalidated", false));
78✔
557
                    break;
3✔
558
                } catch (MongoWriteException e) {
×
559
                    if (e.getError().getCategory() != ErrorCategory.DUPLICATE_KEY) {
×
560
                        throw e;
×
561
                    }
562
                    if (has(mongoSession, "listEntries", new Document("pubkey", pubkeyHash).append("type", typeHash).append("np", ac))) {
×
563
                        break; // Already listed by concurrent thread
×
564
                    }
565
                    if (attempt >= 100) {
×
566
                        throw new RuntimeException("Failed to insert list entry after " + (attempt + 1) + " attempts");
×
567
                    }
568
                }
569
            }
570
        }
571
    }
3✔
572

573
    /**
574
     * Lazily initializes the maxPosition field on a lists document for lists
575
     * created before this field existed. Uses a one-time sorted query, then
576
     * all subsequent calls use the atomic counter.
577
     */
578
    private static void initListPositionIfNeeded(ClientSession mongoSession, String pubkeyHash, String typeHash) {
579
        Document listDoc = collection("lists").find(mongoSession, new Document("pubkey", pubkeyHash).append("type", typeHash)).first();
45✔
580
        if (listDoc == null || listDoc.get("maxPosition") != null) {
18!
581
            return;
3✔
582
        }
583

584
        Document maxDoc = getMaxValueDocument(mongoSession, "listEntries", new Document("pubkey", pubkeyHash).append("type", typeHash), "position");
×
585
        long maxPos = (maxDoc != null) ? maxDoc.getLong("position") : -1L;
×
586

587
        // Conditional update: only set if maxPosition still doesn't exist (race-safe)
588
        collection("lists").updateOne(mongoSession, new Document("pubkey", pubkeyHash).append("type", typeHash).append("maxPosition", new Document("$exists", false)), new Document("$set", new Document("maxPosition", maxPos)));
×
589
    }
×
590

591
    /**
592
     * Builds a comma-separated list of checksums at geometric positions for a given list,
593
     * for use with the afterChecksums parameter during peer sync.
594
     * Returns checksums at positions: max, max-10, max-100, max-1000, max-10000, ...
595
     * Returns null if the list has no entries.
596
     */
597
    public static String buildChecksumFallbacks(ClientSession mongoSession, String pubkeyHash, String typeHash) {
598
        Document maxDoc = getMaxValueDocument(mongoSession, "listEntries", new Document("pubkey", pubkeyHash).append("type", typeHash), "position");
39✔
599
        if (maxDoc == null) {
6✔
600
            return null;
6✔
601
        }
602

603
        long maxPosition = maxDoc.getLong("position");
15✔
604
        StringBuilder sb = new StringBuilder();
12✔
605
        sb.append(maxDoc.getString("checksum"));
18✔
606

607
        for (long offset = 10; offset <= maxPosition; offset *= 10) {
33✔
608
            long targetPos = maxPosition - offset;
12✔
609
            Document entry = collection("listEntries").find(mongoSession, new Document("pubkey", pubkeyHash).append("type", typeHash).append("position", targetPos)).first();
57✔
610
            if (entry != null) {
6!
611
                sb.append(",").append(entry.getString("checksum"));
24✔
612
            }
613
        }
614
        return sb.toString();
9✔
615
    }
616

617
    /**
618
     * Returns the public key string of the Nanopub's signature, or null if not available or invalid.
619
     *
620
     * @param nanopub the nanopub to extract the public key from
621
     * @return The public key string, or null if not available or invalid.
622
     */
623
    public static String getPubkey(Nanopub nanopub) {
624
        // TODO shouldn't this be moved to a utility class in nanopub-java? there is a similar method in NanopubElement class of nanodash
625
        NanopubSignatureElement el;
626
        try {
627
            el = SignatureUtils.getSignatureElement(nanopub);
9✔
628
            if (el != null && SignatureUtils.hasValidSignature(el) && el.getPublicKeyString() != null) {
24!
629
                return el.getPublicKeyString();
9✔
630
            }
631
        } catch (MalformedCryptoElementException | GeneralSecurityException ex) {
×
632
            logger.error("Failed to verify signature of nanopub {}: {}", nanopub.getUri(), ex.getMessage(), ex);
×
633
        }
3✔
634
        return null;
6✔
635
    }
636

637
    /**
638
     * Calculates a hash representing the current state of the trust paths in the loading collection.
639
     *
640
     * @param mongoSession the MongoDB client session
641
     * @return the calculated trust state hash
642
     */
643
    public static String calculateTrustStateHash(ClientSession mongoSession) {
644
        MongoCursor<Document> tp = collection("trustPaths_loading").find(mongoSession).sort(ascending("_id")).cursor();
×
645
        // TODO Improve this so we don't create the full string just for calculating its hash:
646
        String s = "";
×
647
        while (tp.hasNext()) {
×
648
            Document d = tp.next();
×
649
            s += d.getString("_id") + " (" + d.getString("type") + ")\n";
×
650
        }
×
651
        return Utils.getHash(s);
×
652
    }
653

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