diff --git a/.github/workflows/spring-batch-neo4j.yml b/.github/workflows/spring-batch-neo4j.yml
index 531548ae..20218f90 100644
--- a/.github/workflows/spring-batch-neo4j.yml
+++ b/.github/workflows/spring-batch-neo4j.yml
@@ -11,11 +11,12 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout source code
- uses: actions/checkout@v2
- - name: Set up JDK 1.8
- uses: actions/setup-java@v1
+ uses: actions/checkout@v4
+ - name: Set up JDK 17
+ uses: actions/setup-java@v4
with:
- java-version: 1.8
+ distribution: 'temurin'
+ java-version: 17
- name: Build with Maven
run: mvn -B package --file pom.xml
working-directory: spring-batch-neo4j
diff --git a/spring-batch-neo4j/README.md b/spring-batch-neo4j/README.md
index ec4addc1..f7b6c82a 100644
--- a/spring-batch-neo4j/README.md
+++ b/spring-batch-neo4j/README.md
@@ -7,16 +7,11 @@ This extension contains an `ItemReader` and `ItemWriter` implementations for [Ne
The `Neo4jItemReader` can be configured as follows:
```java
-SessionFactory sessionFactory = ...
-Neo4jItemReader itemReader = new Neo4jItemReaderBuilder()
- .sessionFactory(sessionFactory)
- .name("itemReader")
- .targetType(String.class)
- .startStatement("n=node(*)")
- .orderByStatement("n.age")
- .matchStatement("n -- m")
- .whereStatement("has(n.name)")
- .returnStatement("m")
+Neo4jItemReader reader = new Neo4jItemReaderBuilder()
+ .neo4jTemplate(neo4jTemplate)
+ .name("userReader")
+ .statement(Cypher.match(userNode).returning(userNode))
+ .targetType(User.class)
.pageSize(50)
.build();
```
@@ -24,8 +19,93 @@ Neo4jItemReader itemReader = new Neo4jItemReaderBuilder()
The `Neo4jItemWriter` can be configured as follows:
```java
-SessionFactory sessionFactory = ...
-Neo4jItemWriter writer = new Neo4jItemWriterBuilder()
- .sessionFactory(sessionFactory)
+Neo4jItemWriter writer = new Neo4jItemWriterBuilder()
+ .neo4jTemplate(neo4jTemplate)
+ .neo4jDriver(driver)
+ .neo4jMappingContext(mappingContext)
.build();
+```
+
+## Minimal Spring Boot example
+
+Additional to the already existing dependencies in a new Spring Boot application,
+`spring-boot-starter-data-neo4j`, `spring-batch-neo4j` and the `spring-boot-starter-batch` are needed
+but `spring-jdbc` and `spring-boot-starter-jdbc` must be explicitly excluded.
+The exclusions are mandatory to avoid any need for JDBC-based connections, like JDBC URI etc.
+
+See the following _build.gradle_ dependency definition for a minimal example.
+
+```groovy
+dependencies {
+ implementation ('org.springframework.boot:spring-boot-starter-batch') {
+ exclude group: 'org.springframework', module: 'spring-jdbc'
+ exclude group: 'org.springframework.boot', module: 'spring-boot-starter-jdbc'
+ }
+ // current development version 0.2.0-SNAPSHOT
+ implementation 'org.springframework.batch.extensions:spring-batch-neo4j'
+ implementation 'org.springframework.boot:spring-boot-starter-data-neo4j'
+ testImplementation 'org.springframework.boot:spring-boot-starter-test'
+ testImplementation 'org.springframework.batch:spring-batch-test'
+}
+```
+
+An example of the usage can be seen in the following example, implementing the `CommandLineRunner` interface.
+
+```java
+@SpringBootApplication
+public class TestSpringBatchApplication implements CommandLineRunner {
+ // those dependencies are created by Spring Boot's
+ // spring-data-neo4j autoconfiguration
+ @Autowired
+ private Driver driver;
+ @Autowired
+ private Neo4jMappingContext mappingContext;
+ @Autowired
+ private Neo4jTemplate neo4jTemplate;
+
+ public static void main(String[] args) {
+ SpringApplication.run(TestSpringBatchApplication.class, args);
+ }
+
+ @Override
+ public void run(String... args) {
+ // writing
+ Neo4jItemWriter writer = new Neo4jItemWriterBuilder()
+ .neo4jTemplate(neo4jTemplate)
+ .neo4jDriver(driver)
+ .neo4jMappingContext(mappingContext)
+ .build();
+ writer.write(Chunk.of(new User("id1", "ab"), new User("id2", "bb")));
+
+ // reading
+ org.neo4j.cypherdsl.core.Node userNode = Cypher.node("User");
+ Neo4jItemReader reader = new Neo4jItemReaderBuilder()
+ .neo4jTemplate(neo4jTemplate)
+ .name("userReader")
+ .statement(Cypher.match(userNode).returning(userNode))
+ .targetType(User.class)
+ .build();
+ List allUsers = new ArrayList<>();
+ User user = null;
+ while ((user = reader.read()) != null) {
+ System.out.printf("Found user: %s%n", user.name);
+ allUsers.add(user);
+ }
+
+ // deleting
+ writer.setDelete(true);
+ writer.write(Chunk.of(allUsers.toArray(new User[]{})));
+ }
+
+ @Node("User")
+ public static class User {
+ @Id public final String id;
+ public final String name;
+
+ public User(String id, String name) {
+ this.id = id;
+ this.name = name;
+ }
+ }
+}
```
\ No newline at end of file
diff --git a/spring-batch-neo4j/pom.xml b/spring-batch-neo4j/pom.xml
index fb1c38b0..38d9801f 100644
--- a/spring-batch-neo4j/pom.xml
+++ b/spring-batch-neo4j/pom.xml
@@ -54,19 +54,19 @@
UTF-8
UTF-8
- 1.8
+ 17
- 4.3.3
- 3.2.21
+ 5.1.2
+ 7.2.1
3.18.1
- 4.13.2
- 3.6.0
+ 5.11.0
+ 5.12.0
- 3.8.1
+ 3.13.0
3.2.0
3.2.1
@@ -83,15 +83,15 @@
${spring.batch.version}
- org.neo4j
- neo4j-ogm-core
- ${neo4j-ogm-core.version}
+ org.springframework.data
+ spring-data-neo4j
+ ${spring-data-neo4j.version}
- junit
- junit
+ org.junit.jupiter
+ junit-jupiter-engine
${junit.version}
test
diff --git a/spring-batch-neo4j/src/main/java/org/springframework/batch/extensions/neo4j/Neo4jItemReader.java b/spring-batch-neo4j/src/main/java/org/springframework/batch/extensions/neo4j/Neo4jItemReader.java
index 179af22e..db9c30a0 100644
--- a/spring-batch-neo4j/src/main/java/org/springframework/batch/extensions/neo4j/Neo4jItemReader.java
+++ b/spring-batch-neo4j/src/main/java/org/springframework/batch/extensions/neo4j/Neo4jItemReader.java
@@ -16,20 +16,19 @@
package org.springframework.batch.extensions.neo4j;
-import java.util.ArrayList;
-import java.util.Iterator;
-import java.util.Map;
-
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
-import org.neo4j.ogm.session.Session;
-import org.neo4j.ogm.session.SessionFactory;
-
+import org.neo4j.cypherdsl.core.Statement;
+import org.neo4j.cypherdsl.core.StatementBuilder;
+import org.neo4j.cypherdsl.core.renderer.Renderer;
import org.springframework.batch.item.ItemReader;
import org.springframework.batch.item.data.AbstractPaginatedDataItemReader;
import org.springframework.beans.factory.InitializingBean;
+import org.springframework.data.neo4j.core.Neo4jTemplate;
import org.springframework.util.Assert;
-import org.springframework.util.StringUtils;
+
+import java.util.Iterator;
+import java.util.Map;
/**
*
@@ -38,7 +37,7 @@
*
*
*
- * It executes cypher queries built from the statement fragments provided to
+ * It executes cypher queries built from the statement provided to
* retrieve the requested data. The query is executed using paged requests of
* a size specified in {@link #setPageSize(int)}. Additional pages are requested
* as needed when the {@link #read()} method is called. On restart, the reader
@@ -46,7 +45,7 @@
*
*
*
- * Performance is dependent on your Neo4J configuration (embedded or remote) as
+ * Performance is dependent on your Neo4j configuration as
* well as page size. Setting a fairly large page size and using a commit
* interval that matches the page size should provide better performance.
*
@@ -58,167 +57,87 @@
* environment (no restart available).
*
*
+ * @param type of entity to load
* @author Michael Minella
* @author Mahmoud Ben Hassine
+ * @author Gerrit Meier
*/
public class Neo4jItemReader extends AbstractPaginatedDataItemReader implements InitializingBean {
- protected Log logger = LogFactory.getLog(getClass());
-
- private SessionFactory sessionFactory;
-
- private String startStatement;
- private String returnStatement;
- private String matchStatement;
- private String whereStatement;
- private String orderByStatement;
-
- private Class targetType;
-
- private Map parameterValues;
-
- /**
- * Optional parameters to be used in the cypher query.
- *
- * @param parameterValues the parameter values to be used in the cypher query
- */
- public void setParameterValues(Map parameterValues) {
- this.parameterValues = parameterValues;
- }
-
- protected final Map getParameterValues() {
- return this.parameterValues;
- }
-
- /**
- * The start segment of the cypher query. START is prepended
- * to the statement provided and should not be
- * included.
- *
- * @param startStatement the start fragment of the cypher query.
- */
- public void setStartStatement(String startStatement) {
- this.startStatement = startStatement;
- }
-
- /**
- * The return statement of the cypher query. RETURN is prepended
- * to the statement provided and should not be
- * included
- *
- * @param returnStatement the return fragment of the cypher query.
- */
- public void setReturnStatement(String returnStatement) {
- this.returnStatement = returnStatement;
- }
-
- /**
- * An optional match fragment of the cypher query. MATCH is
- * prepended to the statement provided and should not
- * be included.
- *
- * @param matchStatement the match fragment of the cypher query
- */
- public void setMatchStatement(String matchStatement) {
- this.matchStatement = matchStatement;
- }
-
- /**
- * An optional where fragment of the cypher query. WHERE is
- * prepended to the statement provided and should not
- * be included.
- *
- * @param whereStatement where fragment of the cypher query
- */
- public void setWhereStatement(String whereStatement) {
- this.whereStatement = whereStatement;
- }
-
- /**
- * A list of properties to order the results by. This is
- * required so that subsequent page requests pull back the
- * segment of results correctly. ORDER BY is prepended to
- * the statement provided and should not be included.
- *
- * @param orderByStatement order by fragment of the cypher query.
- */
- public void setOrderByStatement(String orderByStatement) {
- this.orderByStatement = orderByStatement;
- }
-
- protected SessionFactory getSessionFactory() {
- return sessionFactory;
- }
-
- /**
- * Establish the session factory for the reader.
- * @param sessionFactory the factory to use for the reader.
- */
- public void setSessionFactory(SessionFactory sessionFactory) {
- this.sessionFactory = sessionFactory;
- }
-
- /**
- * The object type to be returned from each call to {@link #read()}
- *
- * @param targetType the type of object to return.
- */
- public void setTargetType(Class targetType) {
- this.targetType = targetType;
- }
-
- protected final Class getTargetType() {
- return this.targetType;
- }
-
- protected String generateLimitCypherQuery() {
- StringBuilder query = new StringBuilder(128);
-
- query.append("START ").append(startStatement);
- query.append(matchStatement != null ? " MATCH " + matchStatement : "");
- query.append(whereStatement != null ? " WHERE " + whereStatement : "");
- query.append(" RETURN ").append(returnStatement);
- query.append(" ORDER BY ").append(orderByStatement);
- query.append(" SKIP " + (pageSize * page));
- query.append(" LIMIT " + pageSize);
-
- String resultingQuery = query.toString();
-
- if (logger.isDebugEnabled()) {
- logger.debug(resultingQuery);
- }
-
- return resultingQuery;
- }
-
- /**
- * Checks mandatory properties
- *
- * @see InitializingBean#afterPropertiesSet()
- */
- @Override
- public void afterPropertiesSet() throws Exception {
- Assert.state(sessionFactory != null,"A SessionFactory is required");
- Assert.state(targetType != null, "The type to be returned is required");
- Assert.state(StringUtils.hasText(startStatement), "A START statement is required");
- Assert.state(StringUtils.hasText(returnStatement), "A RETURN statement is required");
- Assert.state(StringUtils.hasText(orderByStatement), "A ORDER BY statement is required");
- }
-
- @SuppressWarnings("unchecked")
- @Override
- protected Iterator doPageRead() {
- Session session = getSessionFactory().openSession();
-
- Iterable queryResults = session.query(getTargetType(),
- generateLimitCypherQuery(),
- getParameterValues());
-
- if(queryResults != null) {
- return queryResults.iterator();
- }
- else {
- return new ArrayList().iterator();
- }
- }
+ private final Log logger = LogFactory.getLog(getClass());
+
+ private Neo4jTemplate neo4jTemplate;
+
+ private StatementBuilder.OngoingReadingAndReturn statement;
+
+ private Class targetType;
+
+ private Map parameterValues;
+
+ /**
+ * Optional parameters to be used in the cypher query.
+ *
+ * @param parameterValues the parameter values to be used in the cypher query
+ */
+ public void setParameterValues(Map parameterValues) {
+ this.parameterValues = parameterValues;
+ }
+
+ /**
+ * Cypher-DSL's {@link org.neo4j.cypherdsl.core.StatementBuilder.OngoingReadingAndReturn} statement
+ * without skip and limit segments. Those will get added by the pagination mechanism later.
+ *
+ * @param statement the Cypher-DSL statement-in-construction.
+ */
+ public void setStatement(StatementBuilder.OngoingReadingAndReturn statement) {
+ this.statement = statement;
+ }
+
+ /**
+ * Establish the Neo4jTemplate for the reader.
+ *
+ * @param neo4jTemplate the template to use for the reader.
+ */
+ public void setNeo4jTemplate(Neo4jTemplate neo4jTemplate) {
+ this.neo4jTemplate = neo4jTemplate;
+ }
+
+ /**
+ * The object type to be returned from each call to {@link #read()}
+ *
+ * @param targetType the type of object to return.
+ */
+ public void setTargetType(Class targetType) {
+ this.targetType = targetType;
+ }
+
+ private Statement generateStatement() {
+ Statement builtStatement = statement
+ .skip(page * pageSize)
+ .limit(pageSize)
+ .build();
+ if (logger.isDebugEnabled()) {
+ logger.debug(Renderer.getDefaultRenderer().render(builtStatement));
+ }
+
+ return builtStatement;
+ }
+
+ /**
+ * Checks mandatory properties
+ *
+ * @see InitializingBean#afterPropertiesSet()
+ */
+ @Override
+ public void afterPropertiesSet() {
+ Assert.state(neo4jTemplate != null, "A Neo4jTemplate is required");
+ Assert.state(targetType != null, "The type to be returned is required");
+ Assert.state(statement != null, "A statement is required");
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ protected Iterator doPageRead() {
+ return neo4jTemplate.findAll(generateStatement(), parameterValues, targetType).iterator();
+ }
}
diff --git a/spring-batch-neo4j/src/main/java/org/springframework/batch/extensions/neo4j/Neo4jItemWriter.java b/spring-batch-neo4j/src/main/java/org/springframework/batch/extensions/neo4j/Neo4jItemWriter.java
index d7e339ef..70eb5f51 100644
--- a/spring-batch-neo4j/src/main/java/org/springframework/batch/extensions/neo4j/Neo4jItemWriter.java
+++ b/spring-batch-neo4j/src/main/java/org/springframework/batch/extensions/neo4j/Neo4jItemWriter.java
@@ -16,17 +16,22 @@
package org.springframework.batch.extensions.neo4j;
-import java.util.List;
-
-import org.apache.commons.logging.Log;
-import org.apache.commons.logging.LogFactory;
-import org.neo4j.ogm.session.Session;
-import org.neo4j.ogm.session.SessionFactory;
-
+import org.neo4j.cypherdsl.core.Cypher;
+import org.neo4j.cypherdsl.core.Node;
+import org.neo4j.cypherdsl.core.Statement;
+import org.neo4j.cypherdsl.core.renderer.Renderer;
+import org.neo4j.driver.Driver;
+import org.springframework.batch.item.Chunk;
import org.springframework.batch.item.ItemWriter;
import org.springframework.beans.factory.InitializingBean;
+import org.springframework.data.neo4j.core.Neo4jTemplate;
+import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext;
+import org.springframework.data.neo4j.core.mapping.Neo4jPersistentEntity;
+import org.springframework.lang.NonNull;
import org.springframework.util.Assert;
-import org.springframework.util.CollectionUtils;
+
+import java.util.List;
+import java.util.Map;
/**
*
@@ -38,90 +43,114 @@
* behavior) so it can be used in multiple concurrent transactions.
*
*
+ * @param type of the entity to write
* @author Michael Minella
* @author Glenn Renfro
* @author Mahmoud Ben Hassine
- *
+ * @author Gerrit Meier
*/
public class Neo4jItemWriter implements ItemWriter, InitializingBean {
- protected static final Log logger = LogFactory
- .getLog(Neo4jItemWriter.class);
-
- private boolean delete = false;
-
- private SessionFactory sessionFactory;
-
- /**
- * Boolean flag indicating whether the writer should save or delete the item at write
- * time.
- * @param delete true if write should delete item, false if item should be saved.
- * Default is false.
- */
- public void setDelete(boolean delete) {
- this.delete = delete;
- }
-
- /**
- * Establish the session factory that will be used to create {@link Session} instances
- * for interacting with Neo4j.
- * @param sessionFactory sessionFactory to be used.
- */
- public void setSessionFactory(SessionFactory sessionFactory) {
- this.sessionFactory = sessionFactory;
- }
-
- /**
- * Checks mandatory properties
- *
- * @see InitializingBean#afterPropertiesSet()
- */
- @Override
- public void afterPropertiesSet() throws Exception {
- Assert.state(this.sessionFactory != null,
- "A SessionFactory is required");
- }
-
- /**
- * Write all items to the data store.
- *
- * @see org.springframework.batch.item.ItemWriter#write(java.util.List)
- */
- @Override
- public void write(List extends T> items) throws Exception {
- if(!CollectionUtils.isEmpty(items)) {
- doWrite(items);
- }
- }
-
- /**
- * Performs the actual write using the template. This can be overridden by
- * a subclass if necessary.
- *
- * @param items the list of items to be persisted.
- */
- protected void doWrite(List extends T> items) {
- if(delete) {
- delete(items);
- }
- else {
- save(items);
- }
- }
-
- private void delete(List extends T> items) {
- Session session = this.sessionFactory.openSession();
-
- for(T item : items) {
- session.delete(item);
- }
- }
-
- private void save(List extends T> items) {
- Session session = this.sessionFactory.openSession();
-
- for (T item : items) {
- session.save(item);
- }
- }
+ private boolean delete = false;
+
+ private Neo4jTemplate neo4jTemplate;
+ private Neo4jMappingContext neo4jMappingContext;
+ private Driver neo4jDriver;
+
+ /**
+ * Boolean flag indicating whether the writer should save or delete the item at write
+ * time.
+ *
+ * @param delete true if write should delete item, false if item should be saved.
+ * Default is false.
+ */
+ public void setDelete(boolean delete) {
+ this.delete = delete;
+ }
+
+ /**
+ * Establish the neo4jTemplate for interacting with Neo4j.
+ *
+ * @param neo4jTemplate neo4jTemplate to be used.
+ */
+ public void setNeo4jTemplate(Neo4jTemplate neo4jTemplate) {
+ this.neo4jTemplate = neo4jTemplate;
+ }
+
+ /**
+ * Set the Neo4j driver to be used for the delete operation
+ *
+ * @param neo4jDriver configured Neo4j driver instance
+ */
+ public void setNeo4jDriver(Driver neo4jDriver) {
+ this.neo4jDriver = neo4jDriver;
+ }
+
+ /**
+ * Neo4jMappingContext needed for determine the id type of the entity instances.
+ *
+ * @param neo4jMappingContext initialized mapping context
+ */
+ public void setNeo4jMappingContext(Neo4jMappingContext neo4jMappingContext) {
+ this.neo4jMappingContext = neo4jMappingContext;
+ }
+
+ /**
+ * Checks mandatory properties
+ *
+ * @see InitializingBean#afterPropertiesSet()
+ */
+ @Override
+ public void afterPropertiesSet() {
+ Assert.state(this.neo4jTemplate != null, "A Neo4jTemplate is required");
+ Assert.state(this.neo4jMappingContext != null, "A Neo4jMappingContext is required");
+ Assert.state(this.neo4jDriver != null, "A Neo4j driver is required");
+ }
+
+ /**
+ * Write all items to the data store.
+ *
+ * @see org.springframework.batch.item.ItemWriter#write(Chunk chunk)
+ */
+ @Override
+ public void write(@NonNull Chunk extends T> chunk) {
+ if (!chunk.isEmpty()) {
+ doWrite(chunk.getItems());
+ }
+ }
+
+ /**
+ * Performs the actual write using the template. This can be overridden by
+ * a subclass if necessary.
+ *
+ * @param items the list of items to be persisted.
+ */
+ protected void doWrite(List extends T> items) {
+ if (delete) {
+ delete(items);
+ } else {
+ save(items);
+ }
+ }
+
+ private void delete(List extends T> items) {
+ for (T item : items) {
+ // Figure out id field individually because different
+ // id strategies could have been taken for classes within a
+ // business model hierarchy.
+ Neo4jPersistentEntity> nodeDescription = (Neo4jPersistentEntity>) this.neo4jMappingContext.getNodeDescription(item.getClass());
+ Object identifier = nodeDescription.getIdentifierAccessor(item).getRequiredIdentifier();
+ Node named = Cypher.anyNode().named(nodeDescription.getPrimaryLabel());
+ Statement statement = Cypher.match(named)
+ .where(nodeDescription.getIdDescription().asIdExpression(nodeDescription.getPrimaryLabel()).eq(Cypher.parameter("id")))
+ .detachDelete(named).build();
+
+ String renderedStatement = Renderer.getDefaultRenderer().render(statement);
+ this.neo4jDriver.executableQuery(renderedStatement).withParameters(Map.of("id", identifier)).execute();
+ }
+ }
+
+ private void save(List extends T> items) {
+ this.neo4jTemplate.saveAll(items);
+ }
}
diff --git a/spring-batch-neo4j/src/main/java/org/springframework/batch/extensions/neo4j/builder/Neo4jItemReaderBuilder.java b/spring-batch-neo4j/src/main/java/org/springframework/batch/extensions/neo4j/builder/Neo4jItemReaderBuilder.java
index 9f2d4921..8c5c6dc5 100644
--- a/spring-batch-neo4j/src/main/java/org/springframework/batch/extensions/neo4j/builder/Neo4jItemReaderBuilder.java
+++ b/spring-batch-neo4j/src/main/java/org/springframework/batch/extensions/neo4j/builder/Neo4jItemReaderBuilder.java
@@ -16,258 +16,190 @@
package org.springframework.batch.extensions.neo4j.builder;
-import java.util.Map;
-
-import org.neo4j.ogm.session.SessionFactory;
-
+import org.neo4j.cypherdsl.core.StatementBuilder;
import org.springframework.batch.extensions.neo4j.Neo4jItemReader;
+import org.springframework.data.neo4j.core.Neo4jTemplate;
import org.springframework.util.Assert;
+import java.util.Map;
+
/**
* A builder for the {@link Neo4jItemReader}.
*
+ * @param type of the entity to read
* @author Glenn Renfro
+ * @author Gerrit Meier
* @see Neo4jItemReader
*/
public class Neo4jItemReaderBuilder {
- private SessionFactory sessionFactory;
-
- private String startStatement;
-
- private String returnStatement;
-
- private String matchStatement;
-
- private String whereStatement;
-
- private String orderByStatement;
-
- private Class targetType;
-
- private Map parameterValues;
-
- private int pageSize = 10;
-
- private boolean saveState = true;
-
- private String name;
-
- private int maxItemCount = Integer.MAX_VALUE;
-
- private int currentItemCount;
-
- /**
- * Configure if the state of the {@link org.springframework.batch.item.ItemStreamSupport}
- * should be persisted within the {@link org.springframework.batch.item.ExecutionContext}
- * for restart purposes.
- *
- * @param saveState defaults to true
- * @return The current instance of the builder.
- */
- public Neo4jItemReaderBuilder saveState(boolean saveState) {
- this.saveState = saveState;
-
- return this;
- }
-
- /**
- * The name used to calculate the key within the
- * {@link org.springframework.batch.item.ExecutionContext}. Required if
- * {@link #saveState(boolean)} is set to true.
- *
- * @param name name of the reader instance
- * @return The current instance of the builder.
- * @see org.springframework.batch.item.ItemStreamSupport#setName(String)
- */
- public Neo4jItemReaderBuilder name(String name) {
- this.name = name;
-
- return this;
- }
-
- /**
- * Configure the max number of items to be read.
- *
- * @param maxItemCount the max items to be read
- * @return The current instance of the builder.
- * @see org.springframework.batch.item.support.AbstractItemCountingItemStreamItemReader#setMaxItemCount(int)
- */
- public Neo4jItemReaderBuilder maxItemCount(int maxItemCount) {
- this.maxItemCount = maxItemCount;
-
- return this;
- }
-
- /**
- * Index for the current item. Used on restarts to indicate where to start from.
- *
- * @param currentItemCount current index
- * @return this instance for method chaining
- * @see org.springframework.batch.item.support.AbstractItemCountingItemStreamItemReader#setCurrentItemCount(int)
- */
- public Neo4jItemReaderBuilder currentItemCount(int currentItemCount) {
- this.currentItemCount = currentItemCount;
-
- return this;
- }
-
- /**
- * Establish the session factory for the reader.
- * @param sessionFactory the factory to use for the reader.
- * @return this instance for method chaining
- * @see Neo4jItemReader#setSessionFactory(SessionFactory)
- */
- public Neo4jItemReaderBuilder sessionFactory(SessionFactory sessionFactory) {
- this.sessionFactory = sessionFactory;
-
- return this;
- }
-
- /**
- * The number of items to be read with each page.
- *
- * @param pageSize the number of items
- * @return this instance for method chaining
- * @see Neo4jItemReader#setPageSize(int)
- */
- public Neo4jItemReaderBuilder pageSize(int pageSize) {
- this.pageSize = pageSize;
-
- return this;
- }
-
- /**
- * Optional parameters to be used in the cypher query.
- *
- * @param parameterValues the parameter values to be used in the cypher query
- * @return this instance for method chaining
- * @see Neo4jItemReader#setParameterValues(Map)
- */
- public Neo4jItemReaderBuilder parameterValues(Map parameterValues) {
- this.parameterValues = parameterValues;
-
- return this;
- }
-
- /**
- * The start segment of the cypher query. START is prepended to the statement provided
- * and should not be included.
- *
- * @param startStatement the start fragment of the cypher query.
- * @return this instance for method chaining
- * @see Neo4jItemReader#setStartStatement(String)
- */
- public Neo4jItemReaderBuilder startStatement(String startStatement) {
- this.startStatement = startStatement;
-
- return this;
- }
-
- /**
- * The return statement of the cypher query. RETURN is prepended to the statement
- * provided and should not be included
- *
- * @param returnStatement the return fragment of the cypher query.
- * @return this instance for method chaining
- * @see Neo4jItemReader#setReturnStatement(String)
- */
- public Neo4jItemReaderBuilder returnStatement(String returnStatement) {
- this.returnStatement = returnStatement;
-
- return this;
- }
-
- /**
- * An optional match fragment of the cypher query. MATCH is prepended to the statement
- * provided and should not be included.
- *
- * @param matchStatement the match fragment of the cypher query
- * @return this instance for method chaining
- * @see Neo4jItemReader#setMatchStatement(String)
- */
- public Neo4jItemReaderBuilder matchStatement(String matchStatement) {
- this.matchStatement = matchStatement;
-
- return this;
- }
-
- /**
- * An optional where fragment of the cypher query. WHERE is prepended to the statement
- * provided and should not be included.
- *
- * @param whereStatement where fragment of the cypher query
- * @return this instance for method chaining
- * @see Neo4jItemReader#setWhereStatement(String)
- */
- public Neo4jItemReaderBuilder whereStatement(String whereStatement) {
- this.whereStatement = whereStatement;
-
- return this;
- }
-
- /**
- * A list of properties to order the results by. This is required so that subsequent
- * page requests pull back the segment of results correctly. ORDER BY is prepended to
- * the statement provided and should not be included.
- *
- * @param orderByStatement order by fragment of the cypher query.
- * @return this instance for method chaining
- * @see Neo4jItemReader#setOrderByStatement(String)
- */
- public Neo4jItemReaderBuilder orderByStatement(String orderByStatement) {
- this.orderByStatement = orderByStatement;
-
- return this;
- }
-
- /**
- * The object type to be returned from each call to {@link Neo4jItemReader#read()}
- *
- * @param targetType the type of object to return.
- * @return this instance for method chaining
- * @see Neo4jItemReader#setTargetType(Class)
- */
- public Neo4jItemReaderBuilder targetType(Class targetType) {
- this.targetType = targetType;
-
- return this;
- }
-
- /**
- * Returns a fully constructed {@link Neo4jItemReader}.
- *
- * @return a new {@link Neo4jItemReader}
- */
- public Neo4jItemReader build() {
- if (this.saveState) {
- Assert.hasText(this.name, "A name is required when saveState is set to true");
- }
- Assert.notNull(this.sessionFactory, "sessionFactory is required.");
- Assert.notNull(this.targetType, "targetType is required.");
- Assert.hasText(this.startStatement, "startStatement is required.");
- Assert.hasText(this.returnStatement, "returnStatement is required.");
- Assert.hasText(this.orderByStatement, "orderByStatement is required.");
- Assert.isTrue(this.pageSize > 0, "pageSize must be greater than zero");
- Assert.isTrue(this.maxItemCount > 0, "maxItemCount must be greater than zero");
- Assert.isTrue(this.maxItemCount > this.currentItemCount , "maxItemCount must be greater than currentItemCount");
-
- Neo4jItemReader reader = new Neo4jItemReader<>();
- reader.setMatchStatement(this.matchStatement);
- reader.setOrderByStatement(this.orderByStatement);
- reader.setPageSize(this.pageSize);
- reader.setParameterValues(this.parameterValues);
- reader.setSessionFactory(this.sessionFactory);
- reader.setTargetType(this.targetType);
- reader.setStartStatement(this.startStatement);
- reader.setReturnStatement(this.returnStatement);
- reader.setWhereStatement(this.whereStatement);
- reader.setName(this.name);
- reader.setSaveState(this.saveState);
- reader.setCurrentItemCount(this.currentItemCount);
- reader.setMaxItemCount(this.maxItemCount);
-
- return reader;
- }
+ private Neo4jTemplate neo4jTemplate;
+
+ private StatementBuilder.OngoingReadingAndReturn statement;
+
+ private Class targetType;
+
+ private Map parameterValues;
+
+ private int pageSize = 10;
+
+ private boolean saveState = true;
+
+ private String name;
+
+ private int maxItemCount = Integer.MAX_VALUE;
+
+ private int currentItemCount;
+
+ /**
+ * Configure if the state of the {@link org.springframework.batch.item.ItemStreamSupport}
+ * should be persisted within the {@link org.springframework.batch.item.ExecutionContext}
+ * for restart purposes.
+ *
+ * @param saveState defaults to true
+ * @return The current instance of the builder.
+ */
+ public Neo4jItemReaderBuilder saveState(boolean saveState) {
+ this.saveState = saveState;
+
+ return this;
+ }
+
+ /**
+ * The name used to calculate the key within the
+ * {@link org.springframework.batch.item.ExecutionContext}. Required if
+ * {@link #saveState(boolean)} is set to true.
+ *
+ * @param name name of the reader instance
+ * @return The current instance of the builder.
+ * @see org.springframework.batch.item.ItemStreamSupport#setName(String)
+ */
+ public Neo4jItemReaderBuilder name(String name) {
+ this.name = name;
+
+ return this;
+ }
+
+ /**
+ * Configure the max number of items to be read.
+ *
+ * @param maxItemCount the max items to be read
+ * @return The current instance of the builder.
+ * @see org.springframework.batch.item.support.AbstractItemCountingItemStreamItemReader#setMaxItemCount(int)
+ */
+ public Neo4jItemReaderBuilder maxItemCount(int maxItemCount) {
+ this.maxItemCount = maxItemCount;
+
+ return this;
+ }
+
+ /**
+ * Index for the current item. Used on restarts to indicate where to start from.
+ *
+ * @param currentItemCount current index
+ * @return this instance for method chaining
+ * @see org.springframework.batch.item.support.AbstractItemCountingItemStreamItemReader#setCurrentItemCount(int)
+ */
+ public Neo4jItemReaderBuilder currentItemCount(int currentItemCount) {
+ this.currentItemCount = currentItemCount;
+
+ return this;
+ }
+
+ /**
+ * Establish the neo4jTemplate for the reader.
+ *
+ * @param neo4jTemplate the template to use for the reader.
+ * @return this instance for method chaining
+ * @see Neo4jItemReader#setNeo4jTemplate(Neo4jTemplate)
+ */
+ public Neo4jItemReaderBuilder neo4jTemplate(Neo4jTemplate neo4jTemplate) {
+ this.neo4jTemplate = neo4jTemplate;
+
+ return this;
+ }
+
+ /**
+ * The number of items to be read with each page.
+ *
+ * @param pageSize the number of items
+ * @return this instance for method chaining
+ * @see Neo4jItemReader#setPageSize(int)
+ */
+ public Neo4jItemReaderBuilder pageSize(int pageSize) {
+ this.pageSize = pageSize;
+
+ return this;
+ }
+
+ /**
+ * Optional parameters to be used in the cypher query.
+ *
+ * @param parameterValues the parameter values to be used in the cypher query
+ * @return this instance for method chaining
+ * @see Neo4jItemReader#setParameterValues(Map)
+ */
+ public Neo4jItemReaderBuilder parameterValues(Map parameterValues) {
+ this.parameterValues = parameterValues;
+
+ return this;
+ }
+
+ /**
+ * Cypher-DSL's {@link org.neo4j.cypherdsl.core.StatementBuilder.OngoingReadingAndReturn} statement
+ * without skip and limit segments. Those will get added by the pagination mechanism later.
+ *
+ * @param statement the cypher query without SKIP or LIMIT
+ * @return this instance for method chaining
+ * @see Neo4jItemReader#setStatement(org.neo4j.cypherdsl.core.StatementBuilder.OngoingReadingAndReturn)
+ */
+ public Neo4jItemReaderBuilder statement(StatementBuilder.OngoingReadingAndReturn statement) {
+ this.statement = statement;
+
+ return this;
+ }
+
+ /**
+ * The object type to be returned from each call to {@link Neo4jItemReader#read()}
+ *
+ * @param targetType the type of object to return.
+ * @return this instance for method chaining
+ * @see Neo4jItemReader#setTargetType(Class)
+ */
+ public Neo4jItemReaderBuilder targetType(Class targetType) {
+ this.targetType = targetType;
+
+ return this;
+ }
+
+ /**
+ * Returns a fully constructed {@link Neo4jItemReader}.
+ *
+ * @return a new {@link Neo4jItemReader}
+ */
+ public Neo4jItemReader build() {
+ if (this.saveState) {
+ Assert.hasText(this.name, "A name is required when saveState is set to true");
+ }
+ Assert.notNull(this.neo4jTemplate, "neo4jTemplate is required.");
+ Assert.notNull(this.targetType, "targetType is required.");
+ Assert.notNull(this.statement, "statement is required.");
+ Assert.isTrue(this.pageSize > 0, "pageSize must be greater than zero");
+ Assert.isTrue(this.maxItemCount > 0, "maxItemCount must be greater than zero");
+ Assert.isTrue(this.maxItemCount > this.currentItemCount, "maxItemCount must be greater than currentItemCount");
+
+ Neo4jItemReader reader = new Neo4jItemReader<>();
+ reader.setPageSize(this.pageSize);
+ reader.setParameterValues(this.parameterValues);
+ reader.setNeo4jTemplate(this.neo4jTemplate);
+ reader.setTargetType(this.targetType);
+ reader.setStatement(this.statement);
+ reader.setName(this.name);
+ reader.setSaveState(this.saveState);
+ reader.setCurrentItemCount(this.currentItemCount);
+ reader.setMaxItemCount(this.maxItemCount);
+
+ return reader;
+ }
}
diff --git a/spring-batch-neo4j/src/main/java/org/springframework/batch/extensions/neo4j/builder/Neo4jItemWriterBuilder.java b/spring-batch-neo4j/src/main/java/org/springframework/batch/extensions/neo4j/builder/Neo4jItemWriterBuilder.java
index 6e1919f3..5ac1a392 100644
--- a/spring-batch-neo4j/src/main/java/org/springframework/batch/extensions/neo4j/builder/Neo4jItemWriterBuilder.java
+++ b/spring-batch-neo4j/src/main/java/org/springframework/batch/extensions/neo4j/builder/Neo4jItemWriterBuilder.java
@@ -1,10 +1,10 @@
/*
* Copyright 2017-2021 the original author or authors.
- *
+ *
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
- *
+ *
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
@@ -16,61 +16,91 @@
package org.springframework.batch.extensions.neo4j.builder;
-import org.neo4j.ogm.session.Session;
-import org.neo4j.ogm.session.SessionFactory;
-
+import org.neo4j.driver.Driver;
import org.springframework.batch.extensions.neo4j.Neo4jItemWriter;
+import org.springframework.data.neo4j.core.Neo4jTemplate;
+import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext;
import org.springframework.util.Assert;
/**
* A builder implementation for the {@link Neo4jItemWriter}
*
+ * @param type of the entity to write
* @author Glenn Renfro
+ * @author Gerrit Meier
* @see Neo4jItemWriter
*/
public class Neo4jItemWriterBuilder {
- private boolean delete = false;
+ private boolean delete = false;
- private SessionFactory sessionFactory;
+ private Neo4jTemplate neo4jTemplate;
+ private Driver neo4jDriver;
+ private Neo4jMappingContext neo4jMappingContext;
- /**
- * Boolean flag indicating whether the writer should save or delete the item at write
- * time.
- * @param delete true if write should delete item, false if item should be saved.
- * Default is false.
- * @return The current instance of the builder
- * @see Neo4jItemWriter#setDelete(boolean)
- */
- public Neo4jItemWriterBuilder delete(boolean delete) {
- this.delete = delete;
+ /**
+ * Boolean flag indicating whether the writer should save or delete the item at write
+ * time.
+ *
+ * @param delete true if write should delete item, false if item should be saved.
+ * Default is false.
+ * @return The current instance of the builder
+ * @see Neo4jItemWriter#setDelete(boolean)
+ */
+ public Neo4jItemWriterBuilder delete(boolean delete) {
+ this.delete = delete;
+ return this;
+ }
- return this;
- }
+ /**
+ * Establish the session factory that will be used to create {@link Neo4jTemplate} instances
+ * for interacting with Neo4j.
+ *
+ * @param neo4jTemplate neo4jTemplate to be used.
+ * @return The current instance of the builder
+ * @see Neo4jItemWriter#setNeo4jTemplate(Neo4jTemplate)
+ */
+ public Neo4jItemWriterBuilder neo4jTemplate(Neo4jTemplate neo4jTemplate) {
+ this.neo4jTemplate = neo4jTemplate;
+ return this;
+ }
- /**
- * Establish the session factory that will be used to create {@link Session} instances
- * for interacting with Neo4j.
- * @param sessionFactory sessionFactory to be used.
- * @return The current instance of the builder
- * @see Neo4jItemWriter#setSessionFactory(SessionFactory)
- */
- public Neo4jItemWriterBuilder sessionFactory(SessionFactory sessionFactory) {
- this.sessionFactory = sessionFactory;
+ /**
+ * Set the preconfigured Neo4j driver to be used within the built writer instance.
+ *
+ * @param neo4jDriver preconfigured Neo4j driver instance
+ * @return The current instance of the builder
+ */
+ public Neo4jItemWriterBuilder neo4jDriver(Driver neo4jDriver) {
+ this.neo4jDriver = neo4jDriver;
+ return this;
+ }
- return this;
- }
+ /**
+ * Set the Neo4jMappingContext to be used within the built writer instance.
+ *
+ * @param neo4jMappingContext initialized Neo4jMappingContext instance
+ * @return The current instance of the builder
+ */
+ public Neo4jItemWriterBuilder neo4jMappingContext(Neo4jMappingContext neo4jMappingContext) {
+ this.neo4jMappingContext = neo4jMappingContext;
+ return this;
+ }
- /**
- * Validates and builds a {@link org.springframework.batch.extensions.neo4j.Neo4jItemWriter}.
- *
- * @return a {@link Neo4jItemWriter}
- */
- public Neo4jItemWriter build() {
- Assert.notNull(sessionFactory, "sessionFactory is required.");
- Neo4jItemWriter writer = new Neo4jItemWriter<>();
- writer.setDelete(this.delete);
- writer.setSessionFactory(this.sessionFactory);
- return writer;
- }
+ /**
+ * Validates and builds a {@link org.springframework.batch.extensions.neo4j.Neo4jItemWriter}.
+ *
+ * @return a {@link Neo4jItemWriter}
+ */
+ public Neo4jItemWriter build() {
+ Assert.notNull(neo4jTemplate, "neo4jTemplate is required.");
+ Assert.notNull(neo4jDriver, "neo4jDriver is required.");
+ Assert.notNull(neo4jMappingContext, "neo4jMappingContext is required.");
+ Neo4jItemWriter writer = new Neo4jItemWriter<>();
+ writer.setDelete(this.delete);
+ writer.setNeo4jTemplate(this.neo4jTemplate);
+ writer.setNeo4jDriver(this.neo4jDriver);
+ writer.setNeo4jMappingContext(this.neo4jMappingContext);
+ return writer;
+ }
}
diff --git a/spring-batch-neo4j/src/test/java/org/springframework/batch/extensions/neo4j/Neo4jItemReaderTests.java b/spring-batch-neo4j/src/test/java/org/springframework/batch/extensions/neo4j/Neo4jItemReaderTests.java
index 825ac8df..321f621a 100644
--- a/spring-batch-neo4j/src/test/java/org/springframework/batch/extensions/neo4j/Neo4jItemReaderTests.java
+++ b/spring-batch-neo4j/src/test/java/org/springframework/batch/extensions/neo4j/Neo4jItemReaderTests.java
@@ -16,187 +16,123 @@
package org.springframework.batch.extensions.neo4j;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentCaptor;
+import org.neo4j.cypherdsl.core.Cypher;
+import org.neo4j.cypherdsl.core.Node;
+import org.neo4j.cypherdsl.core.Statement;
+import org.springframework.data.neo4j.core.Neo4jTemplate;
+
import java.util.Arrays;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.Map;
+import java.util.List;
-import org.junit.Rule;
-import org.junit.Test;
-import org.mockito.ArgumentCaptor;
-import org.mockito.Mock;
-import org.mockito.junit.MockitoJUnit;
-import org.mockito.junit.MockitoRule;
-import org.neo4j.ogm.session.Session;
-import org.neo4j.ogm.session.SessionFactory;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertTrue;
-import static org.junit.Assert.fail;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.isNull;
+import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class Neo4jItemReaderTests {
- @Rule
- public MockitoRule rule = MockitoJUnit.rule().silent();
-
- @Mock
- private Iterable result;
- @Mock
- private SessionFactory sessionFactory;
- @Mock
- private Session session;
-
- private Neo4jItemReader buildSessionBasedReader() throws Exception {
- Neo4jItemReader reader = new Neo4jItemReader<>();
-
- reader.setSessionFactory(this.sessionFactory);
- reader.setTargetType(String.class);
- reader.setStartStatement("n=node(*)");
- reader.setReturnStatement("*");
- reader.setOrderByStatement("n.age");
- reader.setPageSize(50);
- reader.afterPropertiesSet();
-
- return reader;
- }
-
- @Test
- public void testAfterPropertiesSet() throws Exception {
-
- Neo4jItemReader reader = new Neo4jItemReader<>();
-
- try {
- reader.afterPropertiesSet();
- fail("SessionFactory was not set but exception was not thrown.");
- } catch (IllegalStateException iae) {
- assertEquals("A SessionFactory is required", iae.getMessage());
- } catch (Throwable t) {
- fail("Wrong exception was thrown:" + t);
- }
-
- reader.setSessionFactory(this.sessionFactory);
-
- try {
- reader.afterPropertiesSet();
- fail("Target Type was not set but exception was not thrown.");
- } catch (IllegalStateException iae) {
- assertEquals("The type to be returned is required", iae.getMessage());
- } catch (Throwable t) {
- fail("Wrong exception was thrown:" + t);
- }
-
- reader.setTargetType(String.class);
-
- try {
- reader.afterPropertiesSet();
- fail("START was not set but exception was not thrown.");
- } catch (IllegalStateException iae) {
- assertEquals("A START statement is required", iae.getMessage());
- } catch (Throwable t) {
- fail("Wrong exception was thrown:" + t);
- }
-
- reader.setStartStatement("n=node(*)");
-
- try {
- reader.afterPropertiesSet();
- fail("RETURN was not set but exception was not thrown.");
- } catch (IllegalStateException iae) {
- assertEquals("A RETURN statement is required", iae.getMessage());
- } catch (Throwable t) {
- fail("Wrong exception was thrown:" + t);
- }
-
- reader.setReturnStatement("n.name, n.phone");
-
- try {
- reader.afterPropertiesSet();
- fail("ORDER BY was not set but exception was not thrown.");
- } catch (IllegalStateException iae) {
- assertEquals("A ORDER BY statement is required", iae.getMessage());
- } catch (Throwable t) {
- fail("Wrong exception was thrown:" + t);
- }
-
- reader.setOrderByStatement("n.age");
-
- reader.afterPropertiesSet();
-
- reader = new Neo4jItemReader<>();
- reader.setSessionFactory(this.sessionFactory);
- reader.setTargetType(String.class);
- reader.setStartStatement("n=node(*)");
- reader.setReturnStatement("n.name, n.phone");
- reader.setOrderByStatement("n.age");
-
- reader.afterPropertiesSet();
- }
-
- @SuppressWarnings("unchecked")
- @Test
- public void testNullResultsWithSession() throws Exception {
-
- Neo4jItemReader itemReader = buildSessionBasedReader();
-
- ArgumentCaptor query = ArgumentCaptor.forClass(String.class);
-
- when(this.sessionFactory.openSession()).thenReturn(this.session);
- when(this.session.query(eq(String.class), query.capture(), isNull())).thenReturn(null);
-
- assertFalse(itemReader.doPageRead().hasNext());
- assertEquals("START n=node(*) RETURN * ORDER BY n.age SKIP 0 LIMIT 50", query.getValue());
- }
-
- @SuppressWarnings("unchecked")
- @Test
- public void testNoResultsWithSession() throws Exception {
- Neo4jItemReader itemReader = buildSessionBasedReader();
- ArgumentCaptor query = ArgumentCaptor.forClass(String.class);
-
- when(this.sessionFactory.openSession()).thenReturn(this.session);
- when(this.session.query(eq(String.class), query.capture(), isNull())).thenReturn(result);
- when(result.iterator()).thenReturn(Collections.emptyIterator());
-
- assertFalse(itemReader.doPageRead().hasNext());
- assertEquals("START n=node(*) RETURN * ORDER BY n.age SKIP 0 LIMIT 50", query.getValue());
- }
-
- @SuppressWarnings("serial")
- @Test
- public void testResultsWithMatchAndWhereWithSession() throws Exception {
- Neo4jItemReader itemReader = buildSessionBasedReader();
- itemReader.setMatchStatement("n -- m");
- itemReader.setWhereStatement("has(n.name)");
- itemReader.setReturnStatement("m");
- itemReader.afterPropertiesSet();
-
- when(this.sessionFactory.openSession()).thenReturn(this.session);
- when(this.session.query(String.class, "START n=node(*) MATCH n -- m WHERE has(n.name) RETURN m ORDER BY n.age SKIP 0 LIMIT 50", null)).thenReturn(result);
- when(result.iterator()).thenReturn(Arrays.asList("foo", "bar", "baz").iterator());
-
- assertTrue(itemReader.doPageRead().hasNext());
- }
-
- @SuppressWarnings("serial")
- @Test
- public void testResultsWithMatchAndWhereWithParametersWithSession() throws Exception {
- Neo4jItemReader itemReader = buildSessionBasedReader();
- Map params = new HashMap<>();
- params.put("foo", "bar");
- itemReader.setParameterValues(params);
- itemReader.setMatchStatement("n -- m");
- itemReader.setWhereStatement("has(n.name)");
- itemReader.setReturnStatement("m");
- itemReader.afterPropertiesSet();
-
- when(this.sessionFactory.openSession()).thenReturn(this.session);
- when(this.session.query(String.class, "START n=node(*) MATCH n -- m WHERE has(n.name) RETURN m ORDER BY n.age SKIP 0 LIMIT 50", params)).thenReturn(result);
- when(result.iterator()).thenReturn(Arrays.asList("foo", "bar", "baz").iterator());
-
- assertTrue(itemReader.doPageRead().hasNext());
- }
+ private Neo4jTemplate neo4jTemplate;
+
+ @BeforeEach
+ void setup() {
+ neo4jTemplate = mock(Neo4jTemplate.class);
+ }
+
+ private Neo4jItemReader buildSessionBasedReader() {
+ Neo4jItemReader reader = new Neo4jItemReader<>();
+
+ reader.setNeo4jTemplate(this.neo4jTemplate);
+ reader.setTargetType(String.class);
+ Node n = Cypher.anyNode().named("n");
+ reader.setStatement(Cypher.match(n).returning(n));
+ reader.setPageSize(50);
+ reader.afterPropertiesSet();
+
+ return reader;
+ }
+
+ @Test
+ public void testAfterPropertiesSet() {
+
+ Neo4jItemReader reader = new Neo4jItemReader<>();
+
+ try {
+ reader.afterPropertiesSet();
+ fail("SessionFactory was not set but exception was not thrown.");
+ } catch (IllegalStateException iae) {
+ assertEquals("A Neo4jTemplate is required", iae.getMessage());
+ } catch (Throwable t) {
+ fail("Wrong exception was thrown:" + t);
+ }
+
+ reader.setNeo4jTemplate(this.neo4jTemplate);
+
+ try {
+ reader.afterPropertiesSet();
+ fail("Target Type was not set but exception was not thrown.");
+ } catch (IllegalStateException iae) {
+ assertEquals("The type to be returned is required", iae.getMessage());
+ } catch (Throwable t) {
+ fail("Wrong exception was thrown:" + t);
+ }
+
+ reader.setTargetType(String.class);
+
+ reader.setStatement(Cypher.match(Cypher.anyNode()).returning(Cypher.anyNode()));
+
+ reader.afterPropertiesSet();
+
+ reader = new Neo4jItemReader<>();
+ reader.setNeo4jTemplate(this.neo4jTemplate);
+ reader.setTargetType(String.class);
+ reader.setStatement(Cypher.match(Cypher.anyNode()).returning(Cypher.anyNode()));
+
+ reader.afterPropertiesSet();
+ }
+
+ @Test
+ public void testNullResultsWithSession() {
+
+ Neo4jItemReader itemReader = buildSessionBasedReader();
+
+ ArgumentCaptor query = ArgumentCaptor.forClass(Statement.class);
+
+ when(this.neo4jTemplate.findAll(query.capture(), isNull(), eq(String.class))).thenReturn(List.of());
+
+ assertFalse(itemReader.doPageRead().hasNext());
+ Node node = Cypher.anyNode().named("n");
+ assertEquals(Cypher.match(node).returning(node).skip(0).limit(50).build().getCypher(), query.getValue().getCypher());
+
+ }
+
+ @Test
+ public void testNoResultsWithSession() {
+ Neo4jItemReader itemReader = buildSessionBasedReader();
+ ArgumentCaptor query = ArgumentCaptor.forClass(Statement.class);
+
+ when(this.neo4jTemplate.findAll(query.capture(), any(), eq(String.class))).thenReturn(List.of());
+
+ assertFalse(itemReader.doPageRead().hasNext());
+ Node node = Cypher.anyNode().named("n");
+ assertEquals(Cypher.match(node).returning(node).skip(0).limit(50).build().getCypher(), query.getValue().getCypher());
+ }
+
+ @Test
+ public void testResultsWithMatchAndWhereWithSession() {
+ Neo4jItemReader itemReader = buildSessionBasedReader();
+ itemReader.afterPropertiesSet();
+
+ when(this.neo4jTemplate.findAll(any(Statement.class), isNull(), eq(String.class))).thenReturn(Arrays.asList("foo", "bar", "baz"));
+
+ assertTrue(itemReader.doPageRead().hasNext());
+ }
+
}
diff --git a/spring-batch-neo4j/src/test/java/org/springframework/batch/extensions/neo4j/Neo4jItemWriterTests.java b/spring-batch-neo4j/src/test/java/org/springframework/batch/extensions/neo4j/Neo4jItemWriterTests.java
index b4eb6514..7bcba925 100644
--- a/spring-batch-neo4j/src/test/java/org/springframework/batch/extensions/neo4j/Neo4jItemWriterTests.java
+++ b/spring-batch-neo4j/src/test/java/org/springframework/batch/extensions/neo4j/Neo4jItemWriterTests.java
@@ -16,134 +16,453 @@
package org.springframework.batch.extensions.neo4j;
-import java.util.ArrayList;
-import java.util.List;
-
-import org.junit.Rule;
-import org.junit.Test;
-import org.mockito.Mock;
-import org.mockito.junit.MockitoJUnit;
-import org.mockito.junit.MockitoRule;
-import org.neo4j.ogm.session.Session;
-import org.neo4j.ogm.session.SessionFactory;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.fail;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.verifyNoInteractions;
-import static org.mockito.Mockito.when;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.internal.verification.Times;
+import org.neo4j.cypherdsl.core.Cypher;
+import org.neo4j.driver.Driver;
+import org.neo4j.driver.ExecutableQuery;
+import org.neo4j.driver.QueryConfig;
+import org.neo4j.driver.Record;
+import org.springframework.batch.item.Chunk;
+import org.springframework.data.mapping.Association;
+import org.springframework.data.mapping.PersistentEntity;
+import org.springframework.data.mapping.model.BasicPersistentEntity;
+import org.springframework.data.neo4j.core.Neo4jTemplate;
+import org.springframework.data.neo4j.core.convert.Neo4jPersistentPropertyConverter;
+import org.springframework.data.neo4j.core.mapping.*;
+import org.springframework.data.util.TypeInformation;
+
+import java.lang.annotation.Annotation;
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.util.*;
+import java.util.function.Predicate;
+import java.util.stream.Collector;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.fail;
+import static org.mockito.Mockito.*;
public class Neo4jItemWriterTests {
- @Rule
- public MockitoRule rule = MockitoJUnit.rule().silent();
-
- private Neo4jItemWriter writer;
-
- @Mock
- private SessionFactory sessionFactory;
- @Mock
- private Session session;
-
- @Test
- public void testAfterPropertiesSet() throws Exception{
-
- writer = new Neo4jItemWriter<>();
-
- try {
- writer.afterPropertiesSet();
- fail("SessionFactory was not set but exception was not thrown.");
- } catch (IllegalStateException iae) {
- assertEquals("A SessionFactory is required", iae.getMessage());
- } catch (Throwable t) {
- fail("Wrong exception was thrown.");
- }
-
- writer.setSessionFactory(this.sessionFactory);
-
- writer.afterPropertiesSet();
-
- writer = new Neo4jItemWriter<>();
-
- writer.setSessionFactory(this.sessionFactory);
-
- writer.afterPropertiesSet();
- }
-
- @Test
- public void testWriteNullSession() throws Exception {
-
- writer = new Neo4jItemWriter<>();
-
- writer.setSessionFactory(this.sessionFactory);
- writer.afterPropertiesSet();
-
- writer.write(null);
-
- verifyNoInteractions(this.session);
- }
-
- @Test
- public void testWriteNullWithSession() throws Exception {
- writer = new Neo4jItemWriter<>();
-
- writer.setSessionFactory(this.sessionFactory);
- writer.afterPropertiesSet();
-
- when(this.sessionFactory.openSession()).thenReturn(this.session);
- writer.write(null);
-
- verifyNoInteractions(this.session);
- }
-
- @Test
- public void testWriteNoItemsWithSession() throws Exception {
- writer = new Neo4jItemWriter<>();
-
- writer.setSessionFactory(this.sessionFactory);
- writer.afterPropertiesSet();
-
- when(this.sessionFactory.openSession()).thenReturn(this.session);
- writer.write(new ArrayList<>());
-
- verifyNoInteractions(this.session);
- }
-
- @Test
- public void testWriteItemsWithSession() throws Exception {
- writer = new Neo4jItemWriter<>();
-
- writer.setSessionFactory(this.sessionFactory);
- writer.afterPropertiesSet();
-
- List items = new ArrayList<>();
- items.add("foo");
- items.add("bar");
-
- when(this.sessionFactory.openSession()).thenReturn(this.session);
- writer.write(items);
-
- verify(this.session).save("foo");
- verify(this.session).save("bar");
- }
-
- @Test
- public void testDeleteItemsWithSession() throws Exception {
- writer = new Neo4jItemWriter<>();
-
- writer.setSessionFactory(this.sessionFactory);
- writer.afterPropertiesSet();
-
- List items = new ArrayList<>();
- items.add("foo");
- items.add("bar");
-
- writer.setDelete(true);
-
- when(this.sessionFactory.openSession()).thenReturn(this.session);
- writer.write(items);
-
- verify(this.session).delete("foo");
- verify(this.session).delete("bar");
- }
+ private Neo4jItemWriter writer;
+
+ private Neo4jTemplate neo4jTemplate;
+ private Driver neo4jDriver;
+ private Neo4jMappingContext neo4jMappingContext;
+
+ @BeforeEach
+ void setup() {
+ neo4jTemplate = mock(Neo4jTemplate.class);
+ neo4jDriver = mock(Driver.class);
+ neo4jMappingContext = mock(Neo4jMappingContext.class);
+ }
+
+ @Test
+ public void testAfterPropertiesSet() {
+
+ writer = new Neo4jItemWriter<>();
+
+ try {
+ writer.afterPropertiesSet();
+ fail("Neo4jTemplate was not set but exception was not thrown.");
+ } catch (IllegalStateException iae) {
+ assertEquals("A Neo4jTemplate is required", iae.getMessage());
+ } catch (Throwable t) {
+ fail("Wrong exception was thrown.");
+ }
+
+ writer.setNeo4jTemplate(this.neo4jTemplate);
+
+ try {
+ writer.afterPropertiesSet();
+ fail("Neo4jMappingContext was not set but exception was not thrown.");
+ } catch (IllegalStateException iae) {
+ assertEquals("A Neo4jMappingContext is required", iae.getMessage());
+ } catch (Throwable t) {
+ fail("Wrong exception was thrown.");
+ }
+
+ writer.setNeo4jMappingContext(this.neo4jMappingContext);
+
+ try {
+ writer.afterPropertiesSet();
+ fail("Neo4jDriver was not set but exception was not thrown.");
+ } catch (IllegalStateException iae) {
+ assertEquals("A Neo4j driver is required", iae.getMessage());
+ } catch (Throwable t) {
+ fail("Wrong exception was thrown.");
+ }
+
+ writer.setNeo4jDriver(this.neo4jDriver);
+
+ writer.afterPropertiesSet();
+ }
+
+ @Test
+ public void testWriteNoItems() {
+ writer = new Neo4jItemWriter<>();
+
+ writer.setNeo4jTemplate(this.neo4jTemplate);
+ writer.setNeo4jDriver(this.neo4jDriver);
+ writer.setNeo4jMappingContext(this.neo4jMappingContext);
+ writer.afterPropertiesSet();
+
+ writer.write(Chunk.of());
+
+ verifyNoInteractions(this.neo4jTemplate);
+ }
+
+ @Test
+ public void testWriteItems() {
+ writer = new Neo4jItemWriter<>();
+
+ writer.setNeo4jTemplate(this.neo4jTemplate);
+ writer.setNeo4jDriver(this.neo4jDriver);
+ writer.setNeo4jMappingContext(this.neo4jMappingContext);
+ writer.afterPropertiesSet();
+
+ writer.write(Chunk.of(new MyEntity("foo"), new MyEntity("bar")));
+
+ verify(this.neo4jTemplate).saveAll(List.of(new MyEntity("foo"), new MyEntity("bar")));
+ }
+
+ @Test
+ public void testDeleteItems() {
+ TypeInformation typeInformation = TypeInformation.of(MyEntity.class);
+ NodeDescription entity = new TestEntity<>(typeInformation);
+ when(neo4jMappingContext.getNodeDescription(MyEntity.class)).thenAnswer(invocationOnMock -> entity);
+ when(neo4jDriver.executableQuery(anyString())).thenReturn(new ExecutableQuery() {
+ @Override
+ public ExecutableQuery withParameters(Map parameters) {
+ return this;
+ }
+
+ @Override
+ public ExecutableQuery withConfig(QueryConfig config) {
+ return null;
+ }
+
+ @Override
+ public T execute(Collector recordCollector, ResultFinisher resultFinisher) {
+ return null;
+ }
+ });
+
+ writer = new Neo4jItemWriter<>();
+
+ writer.setNeo4jTemplate(this.neo4jTemplate);
+ writer.setNeo4jDriver(this.neo4jDriver);
+ writer.setNeo4jMappingContext(this.neo4jMappingContext);
+ writer.afterPropertiesSet();
+
+ writer.setDelete(true);
+
+ Chunk myEntities = Chunk.of(new MyEntity("id1"), new MyEntity("id2"));
+ writer.write(myEntities);
+
+ verify(this.neo4jDriver, new Times(2)).executableQuery("MATCH (MyEntity) WHERE MyEntity.idField = $id DETACH DELETE MyEntity");
+ }
+
+ private record MyEntity(String idField) {
+ }
+
+ private static class TestEntity extends BasicPersistentEntity
+ implements Neo4jPersistentEntity {
+
+ public TestEntity(TypeInformation information) {
+ super(information);
+ addPersistentProperty(new Neo4jPersistentProperty() {
+ @Override
+ public Neo4jPersistentPropertyConverter> getOptionalConverter() {
+ return null;
+ }
+
+ @Override
+ public boolean isEntityWithRelationshipProperties() {
+ return false;
+ }
+
+ @Override
+ public PersistentEntity, Neo4jPersistentProperty> getOwner() {
+ return null;
+ }
+
+ @Override
+ public String getName() {
+ return "idField";
+ }
+
+ @Override
+ public Class> getType() {
+ return String.class;
+ }
+
+ @Override
+ public TypeInformation> getTypeInformation() {
+ return TypeInformation.of(String.class);
+ }
+
+ @Override
+ public Iterable extends TypeInformation>> getPersistentEntityTypeInformation() {
+ return null;
+ }
+
+ @Override
+ public Method getGetter() {
+ return null;
+ }
+
+ @Override
+ public Method getSetter() {
+ return null;
+ }
+
+ @Override
+ public Method getWither() {
+ return null;
+ }
+
+ @Override
+ public Field getField() {
+ try {
+ return MyEntity.class.getDeclaredField("idField");
+ } catch (NoSuchFieldException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Override
+ public String getSpelExpression() {
+ return null;
+ }
+
+ @Override
+ public Association getAssociation() {
+ return null;
+ }
+
+ @Override
+ public boolean isEntity() {
+ return false;
+ }
+
+ @Override
+ public boolean isIdProperty() {
+ return true;
+ }
+
+ @Override
+ public boolean isVersionProperty() {
+ return false;
+ }
+
+ @Override
+ public boolean isCollectionLike() {
+ return false;
+ }
+
+ @Override
+ public boolean isMap() {
+ return false;
+ }
+
+ @Override
+ public boolean isArray() {
+ return false;
+ }
+
+ @Override
+ public boolean isTransient() {
+ return false;
+ }
+
+ @Override
+ public boolean isWritable() {
+ return true;
+ }
+
+ @Override
+ public boolean isReadable() {
+ return true;
+ }
+
+ @Override
+ public boolean isImmutable() {
+ return false;
+ }
+
+ @Override
+ public boolean isAssociation() {
+ return false;
+ }
+
+ @Override
+ public Class> getComponentType() {
+ return null;
+ }
+
+ @Override
+ public Class> getRawType() {
+ return String.class;
+ }
+
+ @Override
+ public Class> getMapValueType() {
+ return null;
+ }
+
+ @Override
+ public Class> getActualType() {
+ return String.class;
+ }
+
+ @Override
+ public A findAnnotation(Class annotationType) {
+ return null;
+ }
+
+ @Override
+ public A findPropertyOrOwnerAnnotation(Class annotationType) {
+ return null;
+ }
+
+ @Override
+ public boolean isAnnotationPresent(Class extends Annotation> annotationType) {
+ return false;
+ }
+
+ @Override
+ public boolean usePropertyAccess() {
+ return false;
+ }
+
+ @Override
+ public Class> getAssociationTargetType() {
+ return null;
+ }
+
+ @Override
+ public TypeInformation> getAssociationTargetTypeInformation() {
+ return null;
+ }
+
+ @Override
+ public String getFieldName() {
+ return null;
+ }
+
+ @Override
+ public String getPropertyName() {
+ return null;
+ }
+
+ @Override
+ public boolean isInternalIdProperty() {
+ return false;
+ }
+
+ @Override
+ public boolean isRelationship() {
+ return false;
+ }
+
+ @Override
+ public boolean isComposite() {
+ return false;
+ }
+ });
+ }
+
+ @Override
+ public Optional getDynamicLabelsProperty() {
+ return Optional.empty();
+ }
+
+ @Override
+ public boolean isRelationshipPropertiesEntity() {
+ return false;
+ }
+
+ @Override
+ public String getPrimaryLabel() {
+ return "MyEntity";
+ }
+
+ @Override
+ public String getMostAbstractParentLabel(NodeDescription> mostAbstractNodeDescription) {
+ return null;
+ }
+
+ @Override
+ public List getAdditionalLabels() {
+ return null;
+ }
+
+ @Override
+ public Class getUnderlyingClass() {
+ return null;
+ }
+
+ @Override
+ public IdDescription getIdDescription() {
+ return IdDescription.forAssignedIds(Cypher.name("thing"), "idField");
+ }
+
+ @Override
+ public Collection getGraphProperties() {
+ return null;
+ }
+
+ @Override
+ public Collection getGraphPropertiesInHierarchy() {
+ return null;
+ }
+
+ @Override
+ public Optional getGraphProperty(String fieldName) {
+ return Optional.empty();
+ }
+
+ @Override
+ public Collection getRelationships() {
+ return null;
+ }
+
+ @Override
+ public Collection getRelationshipsInHierarchy(Predicate propertyPredicate) {
+ return null;
+ }
+
+ @Override
+ public void addChildNodeDescription(NodeDescription> child) {
+
+ }
+
+ @Override
+ public Collection> getChildNodeDescriptionsInHierarchy() {
+ return null;
+ }
+
+ @Override
+ public void setParentNodeDescription(NodeDescription> parent) {
+
+ }
+
+ @Override
+ public NodeDescription> getParentNodeDescription() {
+ return null;
+ }
+
+ @Override
+ public boolean containsPossibleCircles(Predicate includeField) {
+ return false;
+ }
+
+ @Override
+ public boolean describesInterface() {
+ return false;
+ }
+ }
}
diff --git a/spring-batch-neo4j/src/test/java/org/springframework/batch/extensions/neo4j/builder/Neo4jItemReaderBuilderTests.java b/spring-batch-neo4j/src/test/java/org/springframework/batch/extensions/neo4j/builder/Neo4jItemReaderBuilderTests.java
index 49b5002d..f05cdf3a 100644
--- a/spring-batch-neo4j/src/test/java/org/springframework/batch/extensions/neo4j/builder/Neo4jItemReaderBuilderTests.java
+++ b/spring-batch-neo4j/src/test/java/org/springframework/batch/extensions/neo4j/builder/Neo4jItemReaderBuilderTests.java
@@ -16,275 +16,176 @@
package org.springframework.batch.extensions.neo4j.builder;
-import java.util.Arrays;
-import java.util.HashMap;
-import java.util.Map;
-
-import org.junit.Rule;
-import org.junit.Test;
-import org.mockito.Mock;
-import org.mockito.junit.MockitoJUnit;
-import org.mockito.junit.MockitoRule;
-import org.neo4j.ogm.session.Session;
-import org.neo4j.ogm.session.SessionFactory;
-
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.neo4j.cypherdsl.core.Cypher;
+import org.neo4j.cypherdsl.core.Statement;
+import org.neo4j.cypherdsl.core.StatementBuilder;
import org.springframework.batch.extensions.neo4j.Neo4jItemReader;
+import org.springframework.data.neo4j.core.Neo4jTemplate;
+
+import java.util.Arrays;
+import java.util.List;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNull;
-import static org.junit.Assert.fail;
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
/**
* @author Glenn Renfro
+ * @author Gerrit Meier
*/
public class Neo4jItemReaderBuilderTests {
- @Rule
- public MockitoRule rule = MockitoJUnit.rule().silent();
-
- @Mock
- private Iterable result;
-
- @Mock
- private SessionFactory sessionFactory;
-
- @Mock
- private Session session;
-
- @Test
- public void testFullyQualifiedItemReader() throws Exception {
- Neo4jItemReader itemReader = new Neo4jItemReaderBuilder()
- .sessionFactory(this.sessionFactory)
- .targetType(String.class)
- .startStatement("n=node(*)")
- .orderByStatement("n.age")
- .pageSize(50).name("bar")
- .matchStatement("n -- m")
- .whereStatement("has(n.name)")
- .returnStatement("m").build();
-
- when(this.sessionFactory.openSession()).thenReturn(this.session);
- when(this.session.query(String.class,
- "START n=node(*) MATCH n -- m WHERE has(n.name) RETURN m ORDER BY n.age SKIP 0 LIMIT 50", null))
- .thenReturn(result);
- when(result.iterator()).thenReturn(Arrays.asList("foo", "bar", "baz").iterator());
-
- assertEquals("The expected value was not returned by reader.", "foo", itemReader.read());
- assertEquals("The expected value was not returned by reader.", "bar", itemReader.read());
- assertEquals("The expected value was not returned by reader.", "baz", itemReader.read());
- }
-
- @Test
- public void testCurrentSize() throws Exception {
- Neo4jItemReader itemReader = new Neo4jItemReaderBuilder()
- .sessionFactory(this.sessionFactory)
- .targetType(String.class)
- .startStatement("n=node(*)")
- .orderByStatement("n.age")
- .pageSize(50).name("bar")
- .returnStatement("m")
- .currentItemCount(0)
- .maxItemCount(1)
- .build();
-
- when(this.sessionFactory.openSession()).thenReturn(this.session);
- when(this.session.query(String.class, "START n=node(*) RETURN m ORDER BY n.age SKIP 0 LIMIT 50", null))
- .thenReturn(result);
- when(result.iterator()).thenReturn(Arrays.asList("foo", "bar", "baz").iterator());
-
- assertEquals("The expected value was not returned by reader.", "foo", itemReader.read());
- assertNull("The expected value was not should be null.", itemReader.read());
- }
-
- @Test
- public void testResultsWithMatchAndWhereWithParametersWithSession() throws Exception {
- Map params = new HashMap<>();
- params.put("foo", "bar");
- Neo4jItemReader itemReader = new Neo4jItemReaderBuilder()
- .sessionFactory(this.sessionFactory)
- .targetType(String.class)
- .startStatement("n=node(*)")
- .returnStatement("*")
- .orderByStatement("n.age")
- .pageSize(50)
- .name("foo")
- .parameterValues(params)
- .matchStatement("n -- m")
- .whereStatement("has(n.name)")
- .returnStatement("m")
- .build();
-
- when(this.sessionFactory.openSession()).thenReturn(this.session);
- when(this.session.query(String.class,
- "START n=node(*) MATCH n -- m WHERE has(n.name) RETURN m ORDER BY n.age SKIP 0 LIMIT 50", params))
- .thenReturn(result);
- when(result.iterator()).thenReturn(Arrays.asList("foo", "bar", "baz").iterator());
-
- assertEquals("The expected value was not returned by reader.", "foo", itemReader.read());
- }
-
- @Test
- public void testNoSessionFactory() {
- try {
- new Neo4jItemReaderBuilder()
- .targetType(String.class)
- .startStatement("n=node(*)")
- .returnStatement("*")
- .orderByStatement("n.age")
- .pageSize(50)
- .name("bar").build();
-
- fail("IllegalArgumentException should have been thrown");
- }
- catch (IllegalArgumentException iae) {
- assertEquals("IllegalArgumentException message did not match the expected result.",
- "sessionFactory is required.", iae.getMessage());
- }
- }
-
- @Test
- public void testZeroPageSize() {
- validateExceptionMessage(new Neo4jItemReaderBuilder()
- .sessionFactory(this.sessionFactory)
- .targetType(String.class)
- .startStatement("n=node(*)")
- .returnStatement("*")
- .orderByStatement("n.age")
- .pageSize(0)
- .name("foo")
- .matchStatement("n -- m")
- .whereStatement("has(n.name)")
- .returnStatement("m"),
- "pageSize must be greater than zero");
- }
-
- @Test
- public void testZeroMaxItemCount() {
- validateExceptionMessage(new Neo4jItemReaderBuilder()
- .sessionFactory(this.sessionFactory)
- .targetType(String.class)
- .startStatement("n=node(*)")
- .returnStatement("*")
- .orderByStatement("n.age")
- .pageSize(5)
- .maxItemCount(0)
- .name("foo")
- .matchStatement("n -- m")
- .whereStatement("has(n.name)")
- .returnStatement("m"),
- "maxItemCount must be greater than zero");
- }
-
- @Test
- public void testCurrentItemCountGreaterThanMaxItemCount() {
- validateExceptionMessage(new Neo4jItemReaderBuilder()
- .sessionFactory(this.sessionFactory)
- .targetType(String.class)
- .startStatement("n=node(*)")
- .returnStatement("*")
- .orderByStatement("n.age")
- .pageSize(5)
- .maxItemCount(5)
- .currentItemCount(6)
- .name("foo")
- .matchStatement("n -- m")
- .whereStatement("has(n.name)")
- .returnStatement("m"),
- "maxItemCount must be greater than currentItemCount");
- }
-
- @Test
- public void testNullName() {
- validateExceptionMessage(
- new Neo4jItemReaderBuilder()
- .sessionFactory(this.sessionFactory)
- .targetType(String.class)
- .startStatement("n=node(*)")
- .returnStatement("*")
- .orderByStatement("n.age")
- .pageSize(50),
- "A name is required when saveState is set to true");
-
- // tests that name is not required if saveState is set to false.
- new Neo4jItemReaderBuilder()
- .sessionFactory(this.sessionFactory)
- .targetType(String.class)
- .startStatement("n=node(*)")
- .returnStatement("*")
- .orderByStatement("n.age")
- .saveState(false)
- .pageSize(50)
- .build();
- }
-
- @Test
- public void testNullTargetType() {
- validateExceptionMessage(
- new Neo4jItemReaderBuilder()
- .sessionFactory(this.sessionFactory)
- .startStatement("n=node(*)")
- .returnStatement("*")
- .orderByStatement("n.age")
- .pageSize(50)
- .name("bar")
- .matchStatement("n -- m")
- .whereStatement("has(n.name)")
- .returnStatement("m"),
- "targetType is required.");
- }
-
- @Test
- public void testNullStartStatement() {
- validateExceptionMessage(
- new Neo4jItemReaderBuilder()
- .sessionFactory(this.sessionFactory)
- .targetType(String.class)
- .returnStatement("*")
- .orderByStatement("n.age")
- .pageSize(50).name("bar")
- .matchStatement("n -- m")
- .whereStatement("has(n.name)")
- .returnStatement("m"),
- "startStatement is required.");
- }
-
- @Test
- public void testNullReturnStatement() {
- validateExceptionMessage(new Neo4jItemReaderBuilder()
- .sessionFactory(this.sessionFactory)
- .targetType(String.class)
- .startStatement("n=node(*)")
- .orderByStatement("n.age")
- .pageSize(50).name("bar")
- .matchStatement("n -- m")
- .whereStatement("has(n.name)"), "returnStatement is required.");
- }
-
- @Test
- public void testNullOrderByStatement() {
- validateExceptionMessage(
- new Neo4jItemReaderBuilder()
- .sessionFactory(this.sessionFactory)
- .targetType(String.class)
- .startStatement("n=node(*)")
- .returnStatement("*")
- .pageSize(50)
- .name("bar")
- .matchStatement("n -- m")
- .whereStatement("has(n.name)")
- .returnStatement("m"),
- "orderByStatement is required.");
- }
-
- private void validateExceptionMessage(Neo4jItemReaderBuilder> builder, String message) {
- try {
- builder.build();
- fail("IllegalArgumentException should have been thrown");
- }
- catch (IllegalArgumentException iae) {
- assertEquals("IllegalArgumentException message did not match the expected result.", message,
- iae.getMessage());
- }
- }
+ private List result;
+ private Neo4jTemplate neo4jTemplate;
+ private StatementBuilder.OngoingReadingAndReturn dummyStatement = Cypher.match(Cypher.anyNode()).returning(Cypher.anyNode());
+
+ @SuppressWarnings("unchecked")
+ @BeforeEach
+ void setup() {
+ result = mock(List.class);
+ neo4jTemplate = mock(Neo4jTemplate.class);
+ }
+
+ @Test
+ public void testFullyQualifiedItemReader() throws Exception {
+ dummyStatement = Cypher.match(Cypher.anyNode()).returning(Cypher.anyNode());
+ Neo4jItemReader itemReader = new Neo4jItemReaderBuilder()
+ .neo4jTemplate(this.neo4jTemplate)
+ .targetType(String.class)
+ .statement(dummyStatement)
+ .pageSize(50).name("bar")
+ .build();
+
+ when(this.neo4jTemplate.findAll(any(Statement.class), any(), eq(String.class)))
+ .thenReturn(result);
+ when(result.iterator()).thenReturn(Arrays.asList("foo", "bar", "baz").iterator());
+
+ assertEquals("foo", itemReader.read());
+ assertEquals("bar", itemReader.read());
+ assertEquals("baz", itemReader.read());
+ }
+
+ @Test
+ public void testCurrentSize() throws Exception {
+ Neo4jItemReader itemReader = new Neo4jItemReaderBuilder()
+ .neo4jTemplate(this.neo4jTemplate)
+ .targetType(String.class)
+ .statement(dummyStatement)
+ .pageSize(50).name("bar")
+ .currentItemCount(0)
+ .maxItemCount(1)
+ .build();
+
+ when(this.neo4jTemplate.findAll(any(Statement.class), any(), eq(String.class)))
+ .thenReturn(result);
+ when(result.iterator()).thenReturn(Arrays.asList("foo", "bar", "baz").iterator());
+
+ assertEquals("foo", itemReader.read());
+ assertNull(itemReader.read());
+ }
+
+
+ @Test
+ public void testNoSessionFactory() {
+ try {
+ new Neo4jItemReaderBuilder()
+ .targetType(String.class)
+ .pageSize(50)
+ .name("bar").build();
+
+ fail("IllegalArgumentException should have been thrown");
+ } catch (IllegalArgumentException iae) {
+ assertEquals("neo4jTemplate is required.", iae.getMessage());
+ }
+ }
+
+ @Test
+ public void testZeroPageSize() {
+ validateExceptionMessage(new Neo4jItemReaderBuilder()
+ .neo4jTemplate(this.neo4jTemplate)
+ .targetType(String.class)
+ .statement(dummyStatement)
+ .pageSize(0)
+ .name("foo"),
+ "pageSize must be greater than zero");
+ }
+
+ @Test
+ public void testZeroMaxItemCount() {
+ validateExceptionMessage(new Neo4jItemReaderBuilder()
+ .neo4jTemplate(this.neo4jTemplate)
+ .targetType(String.class)
+ .statement(dummyStatement)
+ .pageSize(5)
+ .maxItemCount(0)
+ .name("foo"),
+ "maxItemCount must be greater than zero");
+ }
+
+ @Test
+ public void testCurrentItemCountGreaterThanMaxItemCount() {
+ validateExceptionMessage(new Neo4jItemReaderBuilder()
+ .neo4jTemplate(this.neo4jTemplate)
+ .targetType(String.class)
+ .statement(dummyStatement)
+ .pageSize(5)
+ .maxItemCount(5)
+ .currentItemCount(6)
+ .name("foo"),
+ "maxItemCount must be greater than currentItemCount");
+ }
+
+ @Test
+ public void testNullName() {
+ validateExceptionMessage(
+ new Neo4jItemReaderBuilder()
+ .neo4jTemplate(this.neo4jTemplate)
+ .targetType(String.class)
+ .statement(dummyStatement)
+ .pageSize(50),
+ "A name is required when saveState is set to true");
+
+ // tests that name is not required if saveState is set to false.
+ new Neo4jItemReaderBuilder()
+ .neo4jTemplate(this.neo4jTemplate)
+ .targetType(String.class)
+ .statement(dummyStatement)
+ .saveState(false)
+ .pageSize(50)
+ .build();
+ }
+
+ @Test
+ public void testNullTargetType() {
+ validateExceptionMessage(
+ new Neo4jItemReaderBuilder()
+ .neo4jTemplate(this.neo4jTemplate)
+ .statement(dummyStatement)
+ .pageSize(50)
+ .name("bar"),
+ "targetType is required.");
+ }
+
+ @Test
+ public void testNullStatement() {
+ validateExceptionMessage(
+ new Neo4jItemReaderBuilder()
+ .neo4jTemplate(this.neo4jTemplate)
+ .targetType(String.class)
+ .pageSize(50).name("bar"),
+ "statement is required.");
+ }
+
+ private void validateExceptionMessage(Neo4jItemReaderBuilder> builder, String message) {
+ try {
+ builder.build();
+ fail("IllegalArgumentException should have been thrown");
+ } catch (IllegalArgumentException iae) {
+ assertEquals(message, iae.getMessage());
+ }
+ }
}
diff --git a/spring-batch-neo4j/src/test/java/org/springframework/batch/extensions/neo4j/builder/Neo4jItemWriterBuilderTests.java b/spring-batch-neo4j/src/test/java/org/springframework/batch/extensions/neo4j/builder/Neo4jItemWriterBuilderTests.java
index a92c51e1..206737df 100644
--- a/spring-batch-neo4j/src/test/java/org/springframework/batch/extensions/neo4j/builder/Neo4jItemWriterBuilderTests.java
+++ b/spring-batch-neo4j/src/test/java/org/springframework/batch/extensions/neo4j/builder/Neo4jItemWriterBuilderTests.java
@@ -16,80 +16,117 @@
package org.springframework.batch.extensions.neo4j.builder;
-import java.util.ArrayList;
-import java.util.List;
-
-import org.junit.Rule;
-import org.junit.Test;
-import org.mockito.Mock;
-import org.mockito.junit.MockitoJUnit;
-import org.mockito.junit.MockitoRule;
-import org.neo4j.ogm.session.Session;
-import org.neo4j.ogm.session.SessionFactory;
-
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.neo4j.cypherdsl.core.Cypher;
+import org.neo4j.cypherdsl.core.Functions;
+import org.neo4j.driver.Driver;
+import org.neo4j.driver.ExecutableQuery;
import org.springframework.batch.extensions.neo4j.Neo4jItemWriter;
+import org.springframework.batch.item.Chunk;
+import org.springframework.data.mapping.IdentifierAccessor;
+import org.springframework.data.neo4j.core.Neo4jTemplate;
+import org.springframework.data.neo4j.core.mapping.IdDescription;
+import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext;
+import org.springframework.data.neo4j.core.mapping.Neo4jPersistentEntity;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.fail;
-import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.fail;
+import static org.mockito.Mockito.*;
/**
* @author Glenn Renfro
+ * @author Gerrit Meier
*/
public class Neo4jItemWriterBuilderTests {
- @Rule
- public MockitoRule rule = MockitoJUnit.rule().silent();
-
- @Mock
- private SessionFactory sessionFactory;
- @Mock
- private Session session;
-
- @Test
- public void testBasicWriter() throws Exception{
- Neo4jItemWriter writer = new Neo4jItemWriterBuilder()
- .sessionFactory(this.sessionFactory)
- .build();
- List items = new ArrayList<>();
- items.add("foo");
- items.add("bar");
-
- when(this.sessionFactory.openSession()).thenReturn(this.session);
- writer.write(items);
-
- verify(this.session).save("foo");
- verify(this.session).save("bar");
- verify(this.session, never()).delete("foo");
- verify(this.session, never()).delete("bar");
- }
-
- @Test
- public void testBasicDelete() throws Exception{
- Neo4jItemWriter writer = new Neo4jItemWriterBuilder().delete(true).sessionFactory(this.sessionFactory).build();
- List items = new ArrayList<>();
- items.add("foo");
- items.add("bar");
-
- when(this.sessionFactory.openSession()).thenReturn(this.session);
- writer.write(items);
-
- verify(this.session).delete("foo");
- verify(this.session).delete("bar");
- verify(this.session, never()).save("foo");
- verify(this.session, never()).save("bar");
- }
-
- @Test
- public void testNoSessionFactory() {
- try {
- new Neo4jItemWriterBuilder().build();
- fail("SessionFactory was not set but exception was not thrown.");
- } catch (IllegalArgumentException iae) {
- assertEquals("sessionFactory is required.", iae.getMessage());
- }
- }
+ private Neo4jTemplate neo4jTemplate;
+
+ private Driver neo4jDriver;
+
+ private Neo4jMappingContext neo4jMappingContext;
+
+ @BeforeEach
+ void setup() {
+ neo4jDriver = mock(Driver.class);
+ neo4jTemplate = mock(Neo4jTemplate.class);
+ neo4jMappingContext = mock(Neo4jMappingContext.class);
+ }
+
+ @Test
+ public void testBasicWriter() {
+ Neo4jItemWriter writer = new Neo4jItemWriterBuilder()
+ .neo4jTemplate(this.neo4jTemplate)
+ .neo4jDriver(this.neo4jDriver)
+ .neo4jMappingContext(this.neo4jMappingContext)
+ .build();
+
+ Chunk items = Chunk.of("foo", "bar");
+ writer.write(items);
+
+ verify(this.neo4jTemplate).saveAll(items.getItems());
+ verify(this.neo4jDriver, never()).executableQuery(anyString());
+ }
+
+ @Test
+ public void testBasicDelete() {
+ Neo4jItemWriter writer = new Neo4jItemWriterBuilder()
+ .delete(true)
+ .neo4jMappingContext(this.neo4jMappingContext)
+ .neo4jTemplate(this.neo4jTemplate)
+ .neo4jDriver(neo4jDriver)
+ .build();
+
+ // needs some mocks to create the testable environment
+ Neo4jPersistentEntity> persistentEntity = mock(Neo4jPersistentEntity.class);
+ IdentifierAccessor identifierAccessor = mock(IdentifierAccessor.class);
+ IdDescription idDescription = mock(IdDescription.class);
+ ExecutableQuery executableQuery = mock(ExecutableQuery.class);
+ when(identifierAccessor.getRequiredIdentifier()).thenReturn("someId");
+ when(idDescription.asIdExpression(anyString())).thenReturn(Functions.id(Cypher.anyNode()));
+ when(executableQuery.withParameters(any())).thenReturn(executableQuery);
+ when(persistentEntity.getIdentifierAccessor(any())).thenReturn(identifierAccessor);
+ when(persistentEntity.getPrimaryLabel()).thenReturn("SomeLabel");
+ when(persistentEntity.getIdDescription()).thenReturn(idDescription);
+ when(this.neo4jMappingContext.getNodeDescription(any(Class.class))).thenAnswer(invocationOnMock -> persistentEntity);
+ when(this.neo4jDriver.executableQuery(anyString())).thenReturn(executableQuery);
+
+ Chunk items = Chunk.of("foo", "bar");
+
+ writer.write(items);
+
+ verify(this.neo4jDriver, times(2)).executableQuery(anyString());
+ verify(this.neo4jTemplate, never()).save(items);
+ }
+
+ @Test
+ public void testNoNeo4jDriver() {
+ try {
+ new Neo4jItemWriterBuilder().neo4jTemplate(neo4jTemplate).neo4jMappingContext(neo4jMappingContext).build();
+ fail("Neo4jTemplate was not set but exception was not thrown.");
+ } catch (IllegalArgumentException iae) {
+ assertEquals("neo4jDriver is required.", iae.getMessage());
+ }
+ }
+
+ @Test
+ public void testNoMappingContextFactory() {
+ try {
+ new Neo4jItemWriterBuilder().neo4jTemplate(neo4jTemplate).neo4jDriver(neo4jDriver).build();
+ fail("Neo4jTemplate was not set but exception was not thrown.");
+ } catch (IllegalArgumentException iae) {
+ assertEquals("neo4jMappingContext is required.", iae.getMessage());
+ }
+ }
+
+ @Test
+ public void testNoNeo4jTemplate() {
+ try {
+ new Neo4jItemWriterBuilder().build();
+ fail("Neo4jTemplate was not set but exception was not thrown.");
+ } catch (IllegalArgumentException iae) {
+ assertEquals("neo4jTemplate is required.", iae.getMessage());
+ }
+ }
}