Skip to content

Commit

Permalink
Add multi-database support to HTTP driver
Browse files Browse the repository at this point in the history
Fixes #227
  • Loading branch information
Florent Biville committed May 6, 2020
1 parent 03886f2 commit 17a4b4d
Show file tree
Hide file tree
Showing 4 changed files with 231 additions and 9 deletions.
32 changes: 32 additions & 0 deletions neo4j-jdbc-http/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,22 @@
<artifactId>neo4j-jdbc-http</artifactId>
<description>Http implementation for the Neo4j JDBC Driver</description>

<build>
<testResources>
<testResource>
<directory>src/test/resources</directory>
<filtering>false</filtering>
</testResource>
<testResource>
<directory>src/test/resources</directory>
<includes>
<include>neo4j.version</include>
</includes>
<filtering>true</filtering>
</testResource>
</testResources>
</build>

<!-- ================ -->
<!-- = Dependency = -->
<!-- ================ -->
Expand All @@ -44,6 +60,22 @@
<artifactId>opencsv</artifactId>
<version>4.0</version>
</dependency>

<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>neo4j</artifactId>
<scope>test</scope>
</dependency>
<dependency><!-- used to create database !-->
<groupId>org.neo4j.driver</groupId>
<artifactId>neo4j-java-driver</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

</project>
Original file line number Diff line number Diff line change
Expand Up @@ -92,12 +92,17 @@ public class CypherExecutor {
*/
private String currentTransactionUrl;

/**
* Name of the database (default: neo4j)
*/
private final String databaseName;

/**
* Jackson mapper object.
*/
private static final ObjectMapper mapper = new ObjectMapper();

private static final String DB_DATA_TRANSACTION = "/db/data/transaction";
private static final String DB_DATA_TRANSACTION_TEMPLATE = "/db/%s/tx";

static {
mapper.configure(DeserializationFeature.USE_LONG_FOR_INTS, true);
Expand Down Expand Up @@ -158,7 +163,8 @@ public CypherExecutor(String host, Integer port, Boolean secure, Properties prop
this.http = builder.build();

// Create the url endpoint
this.transactionUrl = createTransactionUrl(host, port, this.secure);
this.databaseName = String.valueOf(properties.getOrDefault("database", "neo4j"));
this.transactionUrl = createTransactionUrl(this.databaseName, host, port, this.secure);

// Setting autocommit
this.setAutoCommit(Boolean.valueOf(properties.getProperty("autoCommit", "true")));
Expand All @@ -182,13 +188,15 @@ public boolean isAuthenticationRequired(String host, Integer port, Boolean secur
}
}

private String createTransactionUrl(String host, Integer port, Boolean secure) throws SQLException {
private String createTransactionUrl(String databaseName, String host, Integer port, Boolean secure) throws SQLException {
String transactionPath = transactionPath(databaseName);
try {
if(secure)
return new URL("https", host, port, DB_DATA_TRANSACTION).toString();
else
return new URL("http", host, port, DB_DATA_TRANSACTION).toString();
} catch (MalformedURLException e) {
if (secure) {
return new URL("https", host, port, transactionPath).toString();
}
return new URL("http", host, port, transactionPath).toString();
}
catch (MalformedURLException e) {
throw new SQLException("Invalid server URL", e);
}
}
Expand Down Expand Up @@ -312,7 +320,8 @@ public String getServerVersion() {
String result = null;

// Prepare the headers query
HttpGet request = new HttpGet(this.transactionUrl.replace(DB_DATA_TRANSACTION, "/db/data"));
HttpGet request = new HttpGet(this.transactionUrl
.replace(transactionPath(this.databaseName), "/db/data"));

// Adding default headers to the request
for (Header header : this.getDefaultHeaders()) {
Expand Down Expand Up @@ -377,6 +386,10 @@ public Integer getOpenTransactionId() {
return getTransactionId(this.currentTransactionUrl);
}

private String transactionPath(String databaseName) {
return String.format(DB_DATA_TRANSACTION_TEMPLATE, databaseName);
}

/**
* Give the default http client default header for Neo4j API.
*
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
package org.neo4j.jdbc.http.driver;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.URI;
import java.util.HashMap;
import java.util.List;
import java.util.Properties;
import java.util.function.Consumer;
import java.util.stream.Collectors;

import org.junit.AfterClass;
import org.junit.Assume;
import org.junit.BeforeClass;
import org.junit.Test;
import org.neo4j.driver.AuthTokens;
import org.neo4j.driver.Driver;
import org.neo4j.driver.GraphDatabase;
import org.neo4j.driver.Session;
import org.neo4j.driver.SessionConfig;
import org.testcontainers.containers.Neo4jContainer;

import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.collection.IsEmptyCollection.empty;
import static org.junit.Assert.assertThat;
import static org.neo4j.driver.SessionConfig.forDatabase;

public class CypherExecutorContainerIT {

private static final String EXTRA_DATABASE_NAME = "extra";

private static final String USERNAME = "neo4j";

private static final String PASSWORD = "password";

private static Neo4jContainer<?> neo4jContainer;

@BeforeClass
public static void startDockerImage() {
neo4jContainer = startEnterpriseDockerImage(imageCoordinates(), USERNAME, PASSWORD);
Assume.assumeTrue("neo4j container should be not null", neo4jContainer != null);
Assume.assumeTrue("neo4j container should be up and running", neo4jContainer.isRunning());
}

@BeforeClass
public static void createExtraDatabase() throws Exception {
createDatabase(EXTRA_DATABASE_NAME);
}

@AfterClass
public static void dropExtraDatabase() throws Exception {
dropDatabase(EXTRA_DATABASE_NAME);
}

@Test
public void testDefaultDatabaseExecution() throws Exception {
String host = neo4jContainer.getContainerIpAddress();
Integer port = neo4jContainer.getMappedPort(7474);
CypherExecutor executor = new CypherExecutor(host, port, false, baseProperties());

Neo4jResponse neo4jResponse = executor
.executeQuery(new Neo4jStatement("CREATE (:InDefault)", new HashMap<>(0), false));

assertThat(neo4jResponse.getErrors(), empty());
assertThat(neo4jResponse.getCode(), equalTo(200));
doInSession(forDatabase("neo4j"), (session) -> {
long count = session
.readTransaction(tx -> tx.run("MATCH (n:InDefault) RETURN COUNT(n) AS count").single().get("count")
.asLong());
assertThat(count, equalTo(1L));
});
doInSession(forDatabase(EXTRA_DATABASE_NAME), (session) -> {
long count = session
.readTransaction(tx -> tx.run("MATCH (n:InDefault) RETURN COUNT(n) AS count").single().get("count")
.asLong());
assertThat(count, equalTo(0L));
});
}

@Test
public void testNonDefaultDatabaseExecution() throws Exception {
String host = neo4jContainer.getContainerIpAddress();
Integer port = neo4jContainer.getMappedPort(7474);
Properties properties = new Properties();
properties.putAll(baseProperties());
properties.setProperty("database", EXTRA_DATABASE_NAME);
CypherExecutor executor = new CypherExecutor(host, port, false, properties);

Neo4jResponse neo4jResponse = executor
.executeQuery(new Neo4jStatement("CREATE (:InNonDefault)", new HashMap<>(0), false));

assertThat(neo4jResponse.getErrors(), empty());
assertThat(neo4jResponse.getCode(), equalTo(200));
doInSession(forDatabase("neo4j"), (session) -> {
long count = session
.readTransaction(tx -> tx.run("MATCH (n:InNonDefault) RETURN COUNT(n) AS count").single().get("count")
.asLong());
assertThat(count, equalTo(0L));
});
doInSession(forDatabase(EXTRA_DATABASE_NAME), (session) -> {
long count = session
.readTransaction(tx -> tx.run("MATCH (n:InNonDefault) RETURN COUNT(n) AS count").single().get("count")
.asLong());
assertThat(count, equalTo(1L));
});
}

private static Neo4jContainer<?> startEnterpriseDockerImage(String version, String username, String password) {
try {
Neo4jContainer<?> container = new Neo4jContainer<>(version)
.withEnv("NEO4J_AUTH", String.format("%s/%s", username, password))
.withEnv("NEO4J_ACCEPT_LICENSE_AGREEMENT", "yes");
container.start();
return container;
}
catch (Exception exception) {
exception.printStackTrace();
}
return null;
}

private static String imageCoordinates() {
return String.format("neo4j:%s-enterprise", projectNeo4jVersion());
}

private static String projectNeo4jVersion() {
String filteredClasspathResource = "/neo4j.version";
List<String> lines = readLines(filteredClasspathResource);
int lineCount = lines.size();
if (lineCount != 1) {
throw new RuntimeException(String
.format("%s should have only 1 (filtered) line, found: %d", filteredClasspathResource, lineCount));
}
return lines.iterator().next();
}

private static List<String> readLines(String classpathResource) {
try (BufferedReader reader = new BufferedReader(new InputStreamReader(CypherExecutorContainerIT.class
.getResourceAsStream(classpathResource)))) {

return reader.lines().collect(Collectors.toList());
}
catch (IOException e) {
throw new RuntimeException(e);
}
}

private static void createDatabase(String databaseName) throws Exception {
doInSession(forDatabase("system"), (Session session) -> session
.writeTransaction((tx) -> tx.run(String.format("CREATE DATABASE %s", databaseName))));
}

private static void dropDatabase(String databaseName) throws Exception {
doInSession(forDatabase("system"), (Session session) -> session
.writeTransaction((tx) -> tx.run(String.format("DROP DATABASE %s", databaseName))));
}

private static void doInSession(SessionConfig sessionConfig, Consumer<Session> sessionConsumer) throws Exception {
try (Driver driver = GraphDatabase
.driver(new URI(neo4jContainer.getBoltUrl()), AuthTokens.basic(USERNAME, PASSWORD));
Session session = driver.session(sessionConfig)) {

sessionConsumer.accept(session);
}
}

private static Properties baseProperties() {
Properties properties = new Properties();
properties.setProperty("userAgent", "Integration test");
properties.setProperty("user", USERNAME);
properties.setProperty("password", PASSWORD);
return properties;
}

}
1 change: 1 addition & 0 deletions neo4j-jdbc-http/src/test/resources/neo4j.version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
${neo4j.version}

0 comments on commit 17a4b4d

Please sign in to comment.