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

knowledgepixels / nanopub-registry / 21168631779

20 Jan 2026 10:47AM UTC coverage: 18.824% (+0.06%) from 18.763%
21168631779

push

github

ashleycaselli
test(RegistryDBTest): remove old initLoadingCollections test method

96 of 586 branches covered (16.38%)

Branch coverage included in aggregate %.

352 of 1794 relevant lines covered (19.62%)

3.59 hits per line

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

39.34
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.MongoClient;
5
import com.mongodb.MongoNamespace;
6
import com.mongodb.MongoWriteException;
7
import com.mongodb.ServerAddress;
8
import com.mongodb.client.ClientSession;
9
import com.mongodb.client.MongoCollection;
10
import com.mongodb.client.MongoCursor;
11
import com.mongodb.client.MongoDatabase;
12
import com.mongodb.client.model.CountOptions;
13
import com.mongodb.client.model.UpdateOptions;
14
import net.trustyuri.TrustyUriUtils;
15
import org.bson.Document;
16
import org.bson.conversions.Bson;
17
import org.bson.types.Binary;
18
import org.eclipse.rdf4j.common.exception.RDF4JException;
19
import org.eclipse.rdf4j.model.IRI;
20
import org.eclipse.rdf4j.rio.RDFFormat;
21
import org.nanopub.MalformedNanopubException;
22
import org.nanopub.Nanopub;
23
import org.nanopub.NanopubUtils;
24
import org.nanopub.extra.security.MalformedCryptoElementException;
25
import org.nanopub.extra.security.NanopubSignatureElement;
26
import org.nanopub.extra.security.SignatureUtils;
27
import org.nanopub.jelly.JellyUtils;
28
import org.slf4j.Logger;
29
import org.slf4j.LoggerFactory;
30

31
import java.io.IOException;
32
import java.security.GeneralSecurityException;
33
import java.util.ArrayList;
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
                return;
×
94
            }
95
            IndexInitializer.initCollections(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
        Document d = collection(collection).find(mongoSession, new Document("_id", elementName)).first();
36✔
198
        if (d == null) {
6✔
199
            return null;
6✔
200
        }
201
        return d.get("value");
12✔
202
    }
203

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

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

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

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

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

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

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

300
    /**
301
     * Records the hash of a given value in the "hashes" collection.
302
     * If the hash already exists, it will be ignored.
303
     *
304
     * @param mongoSession the MongoDB client session
305
     * @param value        the value to hash and record
306
     */
307
    public static void recordHash(ClientSession mongoSession, String value) {
308
        try {
309
            insert(mongoSession, "hashes", new Document("value", value).append("hash", Utils.getHash(value)));
36✔
310
        } catch (MongoWriteException e) {
×
311
            // Duplicate key error -- ignore it
312
            if (e.getError().getCode() != 11000) throw e;
×
313
        }
3✔
314
    }
3✔
315

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

331
    /**
332
     * Loads a nanopublication into the database.
333
     *
334
     * @param mongoSession the MongoDB client session
335
     * @param nanopub      the nanopublication to load
336
     */
337
    public static boolean loadNanopub(ClientSession mongoSession, Nanopub nanopub) {
338
        return loadNanopub(mongoSession, nanopub, null);
×
339
    }
340

341
    /**
342
     * Loads a nanopublication into the database, optionally filtering by public key hash and types.
343
     *
344
     * @param mongoSession the MongoDB client session
345
     * @param nanopub      the nanopublication to load
346
     * @param pubkeyHash   the public key hash to filter by (can be null)
347
     * @param types        the types to filter by (can be empty)
348
     * @return true if the nanopublication was loaded, false otherwise
349
     */
350
    public static boolean loadNanopub(ClientSession mongoSession, Nanopub nanopub, String pubkeyHash, String... types) {
351
        if (nanopub.getTripleCount() > 1200) {
×
352
            logger.info("Nanopub has too many triples ({}): {}", nanopub.getTripleCount(), nanopub.getUri());
×
353
            return false;
×
354
        }
355
        if (nanopub.getByteCount() > 1000000) {
×
356
            logger.info("Nanopub is too large ({}): {}", nanopub.getByteCount(), nanopub.getUri());
×
357
            return false;
×
358
        }
359
        String pubkey = getPubkey(nanopub);
×
360
        if (pubkey == null) {
×
361
            logger.info("Ignoring invalid nanopub: {}", nanopub.getUri());
×
362
            return false;
×
363
        }
364
        String ph = Utils.getHash(pubkey);
×
365
        if (pubkeyHash != null && !pubkeyHash.equals(ph)) {
×
366
            logger.info("Ignoring nanopub with non-matching pubkey: {}", nanopub.getUri());
×
367
            return false;
×
368
        }
369
        recordHash(mongoSession, pubkey);
×
370

371
        String ac = TrustyUriUtils.getArtifactCode(nanopub.getUri().stringValue());
×
372
        if (ac == null) {
×
373
            // I don't think this ever happens, but checking here to be sure
374
            logger.info("ERROR. Unexpected Trusty URI: {}", nanopub.getUri());
×
375
            return false;
×
376
        }
377
        if (has(mongoSession, Collection.NANOPUBS.toString(), ac)) {
×
378
            logger.info("Already loaded: {}", nanopub.getUri());
×
379
        } else {
380
            Long counter = (Long) getMaxValue(mongoSession, Collection.NANOPUBS.toString(), "counter");
×
381
            if (counter == null) counter = 0l;
×
382
            String nanopubString;
383
            byte[] jellyContent;
384
            try {
385
                nanopubString = NanopubUtils.writeToString(nanopub, RDFFormat.TRIG);
×
386
                // Save the same thing in the Jelly format for faster loading
387
                jellyContent = JellyUtils.writeNanopubForDB(nanopub);
×
388
            } catch (IOException ex) {
×
389
                throw new RuntimeException(ex);
×
390
            }
×
391
            collection(Collection.NANOPUBS.toString()).insertOne(mongoSession, new Document("_id", ac).append("fullId", nanopub.getUri().stringValue()).append("counter", counter + 1).append("pubkey", ph).append("content", nanopubString).append("jelly", new Binary(jellyContent)));
×
392

393
            for (IRI invalidatedId : Utils.getInvalidatedNanopubIds(nanopub)) {
×
394
                String invalidatedAc = TrustyUriUtils.getArtifactCode(invalidatedId.stringValue());
×
395
                if (invalidatedAc == null) continue;  // This should never happen; checking here just to be sure
×
396

397
                // Add this nanopub also to all lists of invalidated nanopubs:
398
                collection("invalidations").insertOne(mongoSession, new Document("invalidatingNp", ac).append("invalidatingPubkey", ph).append("invalidatedNp", invalidatedAc));
×
399
                MongoCursor<Document> invalidatedEntries = collection("listEntries").find(mongoSession, new Document("np", invalidatedAc).append("pubkey", ph)).cursor();
×
400
                while (invalidatedEntries.hasNext()) {
×
401
                    Document invalidatedEntry = invalidatedEntries.next();
×
402
                    addToList(mongoSession, nanopub, ph, invalidatedEntry.getString("type"));
×
403
                }
×
404

405
                collection("listEntries").updateMany(mongoSession, new Document("np", invalidatedAc).append("pubkey", ph), new Document("$set", new Document("invalidated", true)));
×
406
                collection("trustEdges").updateMany(mongoSession, new Document("source", invalidatedAc), new Document("$set", new Document("invalidated", true)));
×
407
            }
×
408
        }
409

410
        if (pubkeyHash != null) {
×
411
            for (String type : types) {
×
412
                // TODO Check if nanopub really has the type?
413
                addToList(mongoSession, nanopub, pubkeyHash, Utils.getTypeHash(mongoSession, type));
×
414
                if (type.equals("$")) {
×
415
                    for (IRI t : NanopubUtils.getTypes(nanopub)) {
×
416
                        addToList(mongoSession, nanopub, pubkeyHash, Utils.getTypeHash(mongoSession, t));
×
417
                    }
×
418
                }
419
            }
420
        }
421

422
        // Add the invalidating nanopubs also to the lists of this nanopub:
423
        try (MongoCursor<Document> invalidations = collection("invalidations").find(mongoSession, new Document("invalidatedNp", ac).append("invalidatingPubkey", ph)).cursor()) {
×
424
            if (invalidations.hasNext()) {
×
425
                collection("listEntries").updateMany(mongoSession, new Document("np", ac).append("pubkey", ph), new Document("$set", new Document("invalidated", true)));
×
426
                collection("trustEdges").updateMany(mongoSession, new Document("source", ac), new Document("$set", new Document("invalidated", true)));
×
427
            }
428
            while (invalidations.hasNext()) {
×
429
                String iac = invalidations.next().getString("invalidatingNp");
×
430
                try {
431
                    Document npDoc = collection(Collection.NANOPUBS.toString()).find(mongoSession, new Document("_id", iac)).projection(new Document("jelly", 1)).first();
×
432
                    Nanopub inp = JellyUtils.readFromDB(npDoc.get("jelly", Binary.class).getData());
×
433
                    for (IRI type : NanopubUtils.getTypes(inp)) {
×
434
                        addToList(mongoSession, inp, ph, Utils.getTypeHash(mongoSession, type));
×
435
                    }
×
436
                } catch (RDF4JException | MalformedNanopubException ex) {
×
437
                    ex.printStackTrace();
×
438
                }
×
439
            }
×
440

441
        }
442

443
        return true;
×
444
    }
445

446
    private static void addToList(ClientSession mongoSession, Nanopub nanopub, String pubkeyHash, String typeHash) {
447
        String ac = TrustyUriUtils.getArtifactCode(nanopub.getUri().stringValue());
×
448
        try {
449
            insert(mongoSession, "lists", new Document("pubkey", pubkeyHash).append("type", typeHash));
×
450
        } catch (MongoWriteException e) {
×
451
            // Duplicate key error -- ignore it
452
            if (e.getError().getCode() != 11000) throw e;
×
453
        }
×
454

455
        if (has(mongoSession, "listEntries", new Document("pubkey", pubkeyHash).append("type", typeHash).append("np", ac))) {
×
456
            logger.info("Already listed: {}", nanopub.getUri());
×
457
        } else {
458

459
            Document doc = getMaxValueDocument(mongoSession, "listEntries", new Document("pubkey", pubkeyHash).append("type", typeHash), "position");
×
460
            long position;
461
            String checksum;
462
            if (doc == null) {
×
463
                position = 0l;
×
464
                checksum = NanopubUtils.updateXorChecksum(nanopub.getUri(), NanopubUtils.INIT_CHECKSUM);
×
465
            } else {
466
                position = doc.getLong("position") + 1;
×
467
                checksum = NanopubUtils.updateXorChecksum(nanopub.getUri(), doc.getString("checksum"));
×
468
            }
469
            collection("listEntries").insertOne(mongoSession, new Document("pubkey", pubkeyHash).append("type", typeHash).append("position", position).append("np", ac).append("checksum", checksum).append("invalidated", false));
×
470
        }
471
    }
×
472

473
    /**
474
     * Returns the public key string of the Nanopub's signature, or null if not available or invalid.
475
     *
476
     * @param nanopub the nanopub to extract the public key from
477
     * @return The public key string, or null if not available or invalid.
478
     */
479
    public static String getPubkey(Nanopub nanopub) {
480
        // TODO shouldn't this be moved to a utility class in nanopub-java? there is a similar method in NanopubElement class of nanodash
481
        NanopubSignatureElement el;
482
        try {
483
            el = SignatureUtils.getSignatureElement(nanopub);
9✔
484
            if (el != null && SignatureUtils.hasValidSignature(el) && el.getPublicKeyString() != null) {
24!
485
                return el.getPublicKeyString();
9✔
486
            }
487
        } catch (MalformedCryptoElementException | GeneralSecurityException ex) {
×
488
            logger.error("Error in checking the signature of the nanopub {}", nanopub.getUri());
×
489
        }
3✔
490
        return null;
6✔
491
    }
492

493
    /**
494
     * Calculates a hash representing the current state of the trust paths in the loading collection.
495
     *
496
     * @param mongoSession the MongoDB client session
497
     * @return the calculated trust state hash
498
     */
499
    public static String calculateTrustStateHash(ClientSession mongoSession) {
500
        MongoCursor<Document> tp = collection("trustPaths_loading").find(mongoSession).sort(ascending("_id")).cursor();
×
501
        // TODO Improve this so we don't create the full string just for calculating its hash:
502
        String s = "";
×
503
        while (tp.hasNext()) {
×
504
            Document d = tp.next();
×
505
            s += d.getString("_id") + " (" + d.getString("type") + ")\n";
×
506
        }
×
507
        return Utils.getHash(s);
×
508
    }
509

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