diff --git a/neo4j-jdbc-http/pom.xml b/neo4j-jdbc-http/pom.xml index f4b2e17ff..25ec2420d 100644 --- a/neo4j-jdbc-http/pom.xml +++ b/neo4j-jdbc-http/pom.xml @@ -20,6 +20,22 @@ neo4j-jdbc-http Http implementation for the Neo4j JDBC Driver + + + + src/test/resources + false + + + src/test/resources + + neo4j.version + + true + + + + @@ -44,6 +60,22 @@ opencsv 4.0 + + + org.testcontainers + testcontainers + test + + + org.testcontainers + neo4j + test + + + org.neo4j.driver + neo4j-java-driver + test + diff --git a/neo4j-jdbc-http/src/main/java/org/neo4j/jdbc/http/driver/CypherExecutor.java b/neo4j-jdbc-http/src/main/java/org/neo4j/jdbc/http/driver/CypherExecutor.java index bd692035f..4ea62d9cc 100644 --- a/neo4j-jdbc-http/src/main/java/org/neo4j/jdbc/http/driver/CypherExecutor.java +++ b/neo4j-jdbc-http/src/main/java/org/neo4j/jdbc/http/driver/CypherExecutor.java @@ -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); @@ -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"))); @@ -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); } } @@ -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()) { @@ -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. * diff --git a/neo4j-jdbc-http/src/test/java/org/neo4j/jdbc/http/driver/CypherExecutorContainerIT.java b/neo4j-jdbc-http/src/test/java/org/neo4j/jdbc/http/driver/CypherExecutorContainerIT.java new file mode 100644 index 000000000..c133bef12 --- /dev/null +++ b/neo4j-jdbc-http/src/test/java/org/neo4j/jdbc/http/driver/CypherExecutorContainerIT.java @@ -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 = startEnterpriseDockerContainer(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 startEnterpriseDockerContainer(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 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 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 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; + } + +} \ No newline at end of file diff --git a/neo4j-jdbc-http/src/test/resources/neo4j.version b/neo4j-jdbc-http/src/test/resources/neo4j.version new file mode 100644 index 000000000..42cf0caf9 --- /dev/null +++ b/neo4j-jdbc-http/src/test/resources/neo4j.version @@ -0,0 +1 @@ +${neo4j.version} \ No newline at end of file