Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ from The Central Repository, with an entry in your pom.xml file as shown
Alternatively, you might want to use one of the [pre-built
jar files](https://github.com/Nanopublication/nanopub-java/releases).

The MongoDB driver is an optional dependency: if your project uses the classes that read
nanopubs from MongoDB (`NanopubDb`, or `NanopubStream.fromMongoCursor`), declare
`org.mongodb:mongodb-driver-sync` in your own pom.xml. The command-line jar includes it.


## Quickstart Java Instructions

Expand Down
7 changes: 4 additions & 3 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
<httpclient.version>4.5.14</httpclient.version>
<trustyuri.version>1.24.1</trustyuri.version>
<jelly-rdf4j.version>3.7.3</jelly-rdf4j.version>
<mongo-java-driver.version>3.12.14</mongo-java-driver.version>
<mongodb-driver.version>5.13.0</mongodb-driver.version>
<jakarta.activation.version>2.1.4</jakarta.activation.version>
<jakarta.xml.bind.version>4.0.5</jakarta.xml.bind.version>
<opencsv.version>5.12.0</opencsv.version>
Expand Down Expand Up @@ -179,8 +179,9 @@
</dependency>
<dependency>
<groupId>org.mongodb</groupId>
<artifactId>mongo-java-driver</artifactId>
<version>${mongo-java-driver.version}</version>
<artifactId>mongodb-driver-sync</artifactId>
<version>${mongodb-driver.version}</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>jakarta.activation</groupId>
Expand Down
61 changes: 35 additions & 26 deletions src/main/java/org/nanopub/extra/server/NanopubDb.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
package org.nanopub.extra.server;

import com.mongodb.*;
import com.mongodb.MongoClientSettings;
import com.mongodb.MongoCredential;
import com.mongodb.MongoException;
import com.mongodb.ServerAddress;
import com.mongodb.client.MongoClient;
import com.mongodb.client.MongoClients;
import com.mongodb.client.MongoCollection;
import com.mongodb.client.MongoDatabase;
import com.mongodb.client.model.Filters;
import org.bson.Document;
import org.eclipse.rdf4j.common.exception.RDF4JException;
import org.eclipse.rdf4j.rio.RDFFormat;
import org.nanopub.MalformedNanopubException;
Expand All @@ -9,19 +18,27 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.List;

/**
* A class to manage a Nanopublication database.
* <p>
* It needs the MongoDB Java driver ({@code org.mongodb:mongodb-driver-sync}), which is an
* optional dependency of nanopub-java: a project that uses this class has to declare the driver
* itself.
*/
// This code is partly copied from ch.tkuhn.nanopub.server.NanopubDb
public class NanopubDb {

// Use trig internally to keep namespaces:
private static RDFFormat internalFormat = RDFFormat.TRIG;

private static final Document PING_COMMAND = new Document("ping", 1);

private Logger logger = LoggerFactory.getLogger(this.getClass());

private MongoClient mongo;
private DB db;
private MongoDatabase db;

/**
* Constructor to initialize the NanopubDb with MongoDB connection parameters.
Expand All @@ -34,21 +51,15 @@ public class NanopubDb {
*/
public NanopubDb(String mongoDbHost, int mongoDbPort, String mongoDbName, String mongoDbUsername, String mongoDbPw) {
logger.debug("Initializing nanopub DB for mongodb://{}:{}/{}", mongoDbHost, mongoDbPort, mongoDbName);
ServerAddress serverAddress = new ServerAddress(mongoDbHost, mongoDbPort);

MongoClientSettings.Builder settings = MongoClientSettings.builder()
.applyToClusterSettings(cluster -> cluster.hosts(List.of(new ServerAddress(mongoDbHost, mongoDbPort))));
if (mongoDbUsername != null) {
MongoCredential credential = MongoCredential.createCredential(
mongoDbUsername,
mongoDbName,
mongoDbPw.toCharArray());
mongo = new MongoClient(serverAddress, credential, MongoClientOptions.builder().build());
} else {
mongo = new MongoClient(serverAddress);
settings.credential(MongoCredential.createCredential(mongoDbUsername, mongoDbName, mongoDbPw.toCharArray()));
}
db = mongo.getDB(mongoDbName);
mongo = MongoClients.create(settings.build());
db = mongo.getDatabase(mongoDbName);
}


/**
* Returns the MongoDB client object.
*
Expand All @@ -58,8 +69,6 @@ public MongoClient getMongoClient() {
return mongo;
}

private static DBObject pingCommand = new BasicDBObject("ping", "1");

/**
* Checks if the database is accessible.
* This method sends a ping command to the database and returns true if it succeeds.
Expand All @@ -69,39 +78,40 @@ public MongoClient getMongoClient() {
*/
public boolean isAccessible() {
try {
db.command(pingCommand);
db.runCommand(PING_COMMAND);
} catch (MongoException ex) {
return false;
}
return true;
}

private DBCollection getNanopubCollection() {
private MongoCollection<Document> getNanopubCollection() {
return db.getCollection("nanopubs");
}

private Document findNanopubDocument(String artifactCode) {
return getNanopubCollection().find(Filters.eq("_id", artifactCode)).first();
}

/**
* Returns a Nanopub object for the given artifact code.
*
* @param artifactCode the artifact code of the nanopub to retrieve
* @return the Nanopub object, or null if no nanopub with the given artifact code exists
*/
public Nanopub getNanopub(String artifactCode) {
BasicDBObject query = new BasicDBObject("_id", artifactCode);
DBCursor cursor = getNanopubCollection().find(query);
if (!cursor.hasNext()) {
Document document = findNanopubDocument(artifactCode);
if (document == null) {
return null;
}
String nanopubString = cursor.next().get("nanopub").toString();
Nanopub np = null;
String nanopubString = document.get("nanopub").toString();
try {
np = new NanopubImpl(nanopubString, internalFormat);
return new NanopubImpl(nanopubString, internalFormat);
} catch (MalformedNanopubException ex) {
throw new RuntimeException("Stored nanopub is not well-formed (this shouldn't happen)", ex);
} catch (RDF4JException ex) {
throw new RuntimeException("Stored nanopub is corrupted (this shouldn't happen)", ex);
}
return np;
}

/**
Expand All @@ -111,8 +121,7 @@ public Nanopub getNanopub(String artifactCode) {
* @return true if a nanopub with the given artifact code exists, false otherwise
*/
public boolean hasNanopub(String artifactCode) {
BasicDBObject query = new BasicDBObject("_id", artifactCode);
return getNanopubCollection().find(query).hasNext();
return findNanopubDocument(artifactCode) != null;
}

}
136 changes: 136 additions & 0 deletions src/test/java/org/nanopub/extra/server/NanopubDbTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
package org.nanopub.extra.server;

import com.mongodb.MongoClientSettings;
import com.mongodb.MongoCredential;
import com.mongodb.MongoTimeoutException;
import com.mongodb.ServerAddress;
import com.mongodb.client.FindIterable;
import com.mongodb.client.MongoClient;
import com.mongodb.client.MongoClients;
import com.mongodb.client.MongoCollection;
import com.mongodb.client.MongoDatabase;
import net.trustyuri.TrustyUriUtils;
import org.bson.BsonDocument;
import org.bson.BsonString;
import org.bson.Document;
import org.bson.conversions.Bson;
import org.eclipse.rdf4j.rio.RDFFormat;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.MockedStatic;
import org.nanopub.Nanopub;
import org.nanopub.NanopubImpl;
import org.nanopub.NanopubUtils;
import org.nanopub.testsuite.NanopubTestSuite;
import org.nanopub.testsuite.TestSuiteSubfolder;

import java.util.List;

import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

/**
* NanopubDb reads nanopubs, stored as TriG under their artifact code, from a MongoDB database
* through the current MongoDB driver.
*/
class NanopubDbTest {

private static final String HOST = "db.example.org";
private static final int PORT = 27018;
private static final String DB_NAME = "nanopub-server";

private final MongoClient client = mock(MongoClient.class);
private final MongoDatabase database = mock(MongoDatabase.class);
@SuppressWarnings("unchecked")
private final MongoCollection<Document> collection = mock(MongoCollection.class);
@SuppressWarnings("unchecked")
private final FindIterable<Document> found = mock(FindIterable.class);

private MockedStatic<MongoClients> mongoClients;
private MongoClientSettings settings;

@BeforeEach
void setUp() {
mongoClients = mockStatic(MongoClients.class);
mongoClients.when(() -> MongoClients.create(any(MongoClientSettings.class))).thenAnswer(invocation -> {
settings = invocation.getArgument(0);
return client;
});
when(client.getDatabase(DB_NAME)).thenReturn(database);
when(database.getCollection("nanopubs")).thenReturn(collection);
when(collection.find(any(Bson.class))).thenReturn(found);
}

@AfterEach
void tearDown() {
mongoClients.close();
}

private static Nanopub storedNanopub() throws Exception {
return new NanopubImpl(NanopubTestSuite.getLatest().getValid(TestSuiteSubfolder.SIGNED).getFirst().toFile());
}

@Test
void connectsToTheGivenServerWithoutCredentials() {
new NanopubDb(HOST, PORT, DB_NAME, null, null);
assertEquals(List.of(new ServerAddress(HOST, PORT)), settings.getClusterSettings().getHosts());
assertNull(settings.getCredential());
}

@Test
void authenticatesAgainstTheDatabase() {
new NanopubDb(HOST, PORT, DB_NAME, "reader", "secret");
MongoCredential credential = settings.getCredential();
assertEquals("reader", credential.getUserName());
assertEquals(DB_NAME, credential.getSource());
assertArrayEquals("secret".toCharArray(), credential.getPassword());
}

@Test
void returnsTheNanopubStoredUnderItsArtifactCode() throws Exception {
Nanopub nanopub = storedNanopub();
String artifactCode = TrustyUriUtils.getArtifactCode(nanopub.getUri().stringValue());
when(found.first()).thenReturn(new Document("_id", artifactCode)
.append("nanopub", NanopubUtils.writeToString(nanopub, RDFFormat.TRIG)));

NanopubDb db = new NanopubDb(HOST, PORT, DB_NAME, null, null);

assertEquals(nanopub.getUri(), db.getNanopub(artifactCode).getUri());
assertTrue(db.hasNanopub(artifactCode));
ArgumentCaptor<Bson> filter = ArgumentCaptor.forClass(Bson.class);
verify(collection, atLeastOnce()).find(filter.capture());
assertEquals(new BsonDocument("_id", new BsonString(artifactCode)), filter.getValue().toBsonDocument());
}

@Test
void returnsNothingForAnUnknownArtifactCode() {
when(found.first()).thenReturn(null);
NanopubDb db = new NanopubDb(HOST, PORT, DB_NAME, null, null);
assertNull(db.getNanopub("RAunknown"));
assertFalse(db.hasNanopub("RAunknown"));
}

@Test
void isAccessibleWhenThePingSucceeds() {
when(database.runCommand(any(Bson.class))).thenReturn(new Document("ok", 1.0));
assertTrue(new NanopubDb(HOST, PORT, DB_NAME, null, null).isAccessible());
}

@Test
void isNotAccessibleWhenThePingFails() {
when(database.runCommand(any(Bson.class))).thenThrow(new MongoTimeoutException("no server"));
assertFalse(new NanopubDb(HOST, PORT, DB_NAME, null, null).isAccessible());
}

}
Loading