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 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 items) { - if(delete) { - delete(items); - } - else { - save(items); - } - } - - private void delete(List items) { - Session session = this.sessionFactory.openSession(); - - for(T item : items) { - session.delete(item); - } - } - - private void save(List 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 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 items) { + if (delete) { + delete(items); + } else { + save(items); + } + } + + private void delete(List 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 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 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> 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 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()); + } + } }