From 7526a2437915c3e56ad4166178a55a511f8edeb9 Mon Sep 17 00:00:00 2001 From: Gerrit Meier Date: Thu, 4 Jan 2024 13:26:55 +0100 Subject: [PATCH 1/6] Update Neo4j extension to use Spring Data Neo4j. --- spring-batch-neo4j/README.md | 101 +++- spring-batch-neo4j/pom.xml | 20 +- .../extensions/neo4j/Neo4jItemReader.java | 150 ++---- .../extensions/neo4j/Neo4jItemWriter.java | 97 ++-- .../neo4j/builder/Neo4jItemReaderBuilder.java | 116 +---- .../neo4j/builder/Neo4jItemWriterBuilder.java | 54 +- .../neo4j/Neo4jItemReaderTests.java | 140 ++--- .../neo4j/Neo4jItemWriterTests.java | 479 +++++++++++++++--- .../builder/Neo4jItemReaderBuilderTests.java | 213 +++----- .../builder/Neo4jItemWriterBuilderTests.java | 127 +++-- 10 files changed, 851 insertions(+), 646 deletions(-) diff --git a/spring-batch-neo4j/README.md b/spring-batch-neo4j/README.md index ec4addc1..d041184a 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,88 @@ 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 + +With a Spring Boot application containing the additional dependencies `spring-boot-starter-neo4j` and `spring-batch-neo4j`, +the following _build.gradle_ dependency definition is the minimal needed. +Please note the exclusion for Spring JDBC from the `spring-boot-starter-batch` to avoid any need for JDBC-based connections. + +```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' +} +``` + +```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..3d9697b3 100644 --- a/spring-batch-neo4j/pom.xml +++ b/spring-batch-neo4j/pom.xml @@ -54,16 +54,16 @@ UTF-8 UTF-8 - 1.8 + 17 - 4.3.3 - 3.2.21 + 5.1.0 + 7.2.1 3.18.1 - 4.13.2 - 3.6.0 + 5.10.1 + 5.8.0 3.8.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..6545032b 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,20 +57,19 @@ * 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 final Log logger = LogFactory.getLog(getClass()); - private SessionFactory sessionFactory; + private Neo4jTemplate neo4jTemplate; - private String startStatement; - private String returnStatement; - private String matchStatement; - private String whereStatement; - private String orderByStatement; + private StatementBuilder.OngoingReadingAndReturn statement; private Class targetType; @@ -86,76 +84,23 @@ 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. + * 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 matchStatement the match fragment of the cypher query + * @param statement the Cypher-DSL statement-in-construction. */ - public void setMatchStatement(String matchStatement) { - this.matchStatement = matchStatement; + public void setStatement(StatementBuilder.OngoingReadingAndReturn statement) { + this.statement = statement; } /** - * An optional where fragment of the cypher query. WHERE is - * prepended to the statement provided and should not - * be included. + * Establish the Neo4jTemplate for the reader. * - * @param whereStatement where fragment of the cypher query + * @param neo4jTemplate the template to use for the reader. */ - 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; + public void setNeo4jTemplate(Neo4jTemplate neo4jTemplate) { + this.neo4jTemplate = neo4jTemplate; } /** @@ -167,28 +112,16 @@ 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(); - + private Statement generateStatement() { + Statement builtStatement = statement + .skip(page * pageSize) + .limit(pageSize) + .build(); if (logger.isDebugEnabled()) { - logger.debug(resultingQuery); + logger.debug(Renderer.getDefaultRenderer().render(builtStatement)); } - return resultingQuery; + return builtStatement; } /** @@ -197,28 +130,15 @@ protected String generateLimitCypherQuery() { * @see InitializingBean#afterPropertiesSet() */ @Override - public void afterPropertiesSet() throws Exception { - Assert.state(sessionFactory != null,"A SessionFactory is required"); + public void afterPropertiesSet() { + Assert.state(neo4jTemplate != null, "A Neo4jTemplate 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"); + Assert.state(statement != null, "A 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(); - } + 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..f3d04992 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,19 +43,21 @@ * 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; + private Neo4jTemplate neo4jTemplate; + private Neo4jMappingContext neo4jMappingContext; + private Driver neo4jDriver; /** * Boolean flag indicating whether the writer should save or delete the item at write @@ -63,12 +70,28 @@ public void setDelete(boolean delete) { } /** - * Establish the session factory that will be used to create {@link Session} instances - * for interacting with Neo4j. - * @param sessionFactory sessionFactory to be used. + * 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 setSessionFactory(SessionFactory sessionFactory) { - this.sessionFactory = sessionFactory; + 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; } /** @@ -77,22 +100,23 @@ public void setSessionFactory(SessionFactory sessionFactory) { * @see InitializingBean#afterPropertiesSet() */ @Override - public void afterPropertiesSet() throws Exception { - Assert.state(this.sessionFactory != null, - "A SessionFactory is required"); + 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(java.util.List) + * @see org.springframework.batch.item.ItemWriter#write(Chunk chunk) */ - @Override - public void write(List items) throws Exception { - if(!CollectionUtils.isEmpty(items)) { - doWrite(items); + @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 @@ -110,18 +134,23 @@ protected void doWrite(List items) { } private void delete(List items) { - Session session = this.sessionFactory.openSession(); - for(T item : items) { - session.delete(item); + // 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) { - Session session = this.sessionFactory.openSession(); - - for (T item : items) { - session.save(item); - } + 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..d9804af6 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,32 +16,27 @@ 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 Neo4jTemplate neo4jTemplate; - private String startStatement; - - private String returnStatement; - - private String matchStatement; - - private String whereStatement; - - private String orderByStatement; + private StatementBuilder.OngoingReadingAndReturn statement; private Class targetType; @@ -113,13 +108,13 @@ public Neo4jItemReaderBuilder currentItemCount(int currentItemCount) { } /** - * Establish the session factory for the reader. - * @param sessionFactory the factory to use for the reader. + * Establish the neo4jTemplate for the reader. + * @param neo4jTemplate the template to use for the reader. * @return this instance for method chaining - * @see Neo4jItemReader#setSessionFactory(SessionFactory) + * @see Neo4jItemReader#setNeo4jTemplate(Neo4jTemplate) */ - public Neo4jItemReaderBuilder sessionFactory(SessionFactory sessionFactory) { - this.sessionFactory = sessionFactory; + public Neo4jItemReaderBuilder neo4jTemplate(Neo4jTemplate neo4jTemplate) { + this.neo4jTemplate = neo4jTemplate; return this; } @@ -151,72 +146,15 @@ public Neo4jItemReaderBuilder parameterValues(Map parameterVa } /** - * 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. + * 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 orderByStatement order by fragment of the cypher query. + * @param statement the cypher query without SKIP or LIMIT * @return this instance for method chaining - * @see Neo4jItemReader#setOrderByStatement(String) + * @see Neo4jItemReader#setStatement(org.neo4j.cypherdsl.core.StatementBuilder.OngoingReadingAndReturn) */ - public Neo4jItemReaderBuilder orderByStatement(String orderByStatement) { - this.orderByStatement = orderByStatement; + public Neo4jItemReaderBuilder statement(StatementBuilder.OngoingReadingAndReturn statement) { + this.statement = statement; return this; } @@ -243,25 +181,19 @@ 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.neo4jTemplate, "neo4jTemplate 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.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.setMatchStatement(this.matchStatement); - reader.setOrderByStatement(this.orderByStatement); reader.setPageSize(this.pageSize); reader.setParameterValues(this.parameterValues); - reader.setSessionFactory(this.sessionFactory); + reader.setNeo4jTemplate(this.neo4jTemplate); reader.setTargetType(this.targetType); - reader.setStartStatement(this.startStatement); - reader.setReturnStatement(this.returnStatement); - reader.setWhereStatement(this.whereStatement); + reader.setStatement(this.statement); reader.setName(this.name); reader.setSaveState(this.saveState); reader.setCurrentItemCount(this.currentItemCount); 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..77b8b54c 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 @@ -16,23 +16,28 @@ 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 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 @@ -49,14 +54,36 @@ public Neo4jItemWriterBuilder delete(boolean delete) { } /** - * Establish the session factory that will be used to create {@link Session} instances + * Establish the session factory that will be used to create {@link Neo4jTemplate} instances * for interacting with Neo4j. - * @param sessionFactory sessionFactory to be used. + * @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; + } + + /** + * 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 - * @see Neo4jItemWriter#setSessionFactory(SessionFactory) */ - public Neo4jItemWriterBuilder sessionFactory(SessionFactory sessionFactory) { - this.sessionFactory = sessionFactory; + public Neo4jItemWriterBuilder neo4jDriver(Driver neo4jDriver) { + this.neo4jDriver = neo4jDriver; + + 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; } @@ -67,10 +94,15 @@ public Neo4jItemWriterBuilder sessionFactory(SessionFactory sessionFactory) { * @return a {@link Neo4jItemWriter} */ public Neo4jItemWriter build() { - Assert.notNull(sessionFactory, "sessionFactory is required."); + 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.setSessionFactory(this.sessionFactory); + 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..16566a8f 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,48 +16,42 @@ 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.mockito.ArgumentMatchers.eq; -import static org.mockito.ArgumentMatchers.isNull; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; public class Neo4jItemReaderTests { - @Rule - public MockitoRule rule = MockitoJUnit.rule().silent(); + private List result; + private Neo4jTemplate neo4jTemplate; - @Mock - private Iterable result; - @Mock - private SessionFactory sessionFactory; - @Mock - private Session session; + @SuppressWarnings("unchecked") + @BeforeEach + void setup() { + neo4jTemplate = mock(Neo4jTemplate.class); + result = mock(List.class); + } - private Neo4jItemReader buildSessionBasedReader() throws Exception { + private Neo4jItemReader buildSessionBasedReader() { Neo4jItemReader reader = new Neo4jItemReader<>(); - reader.setSessionFactory(this.sessionFactory); + reader.setNeo4jTemplate(this.neo4jTemplate); reader.setTargetType(String.class); - reader.setStartStatement("n=node(*)"); - reader.setReturnStatement("*"); - reader.setOrderByStatement("n.age"); + Node n = Cypher.anyNode().named("n"); + reader.setStatement(Cypher.match(n).returning(n)); reader.setPageSize(50); reader.afterPropertiesSet(); @@ -65,7 +59,7 @@ private Neo4jItemReader buildSessionBasedReader() throws Exception { } @Test - public void testAfterPropertiesSet() throws Exception { + public void testAfterPropertiesSet() { Neo4jItemReader reader = new Neo4jItemReader<>(); @@ -73,12 +67,12 @@ public void testAfterPropertiesSet() throws Exception { reader.afterPropertiesSet(); fail("SessionFactory was not set but exception was not thrown."); } catch (IllegalStateException iae) { - assertEquals("A SessionFactory is required", iae.getMessage()); + assertEquals("A Neo4jTemplate is required", iae.getMessage()); } catch (Throwable t) { fail("Wrong exception was thrown:" + t); } - reader.setSessionFactory(this.sessionFactory); + reader.setNeo4jTemplate(this.neo4jTemplate); try { reader.afterPropertiesSet(); @@ -91,47 +85,14 @@ public void testAfterPropertiesSet() throws Exception { 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.setStatement(Cypher.match(Cypher.anyNode()).returning(Cypher.anyNode())); reader.afterPropertiesSet(); reader = new Neo4jItemReader<>(); - reader.setSessionFactory(this.sessionFactory); + reader.setNeo4jTemplate(this.neo4jTemplate); reader.setTargetType(String.class); - reader.setStartStatement("n=node(*)"); - reader.setReturnStatement("n.name, n.phone"); - reader.setOrderByStatement("n.age"); + reader.setStatement(Cypher.match(Cypher.anyNode()).returning(Cypher.anyNode())); reader.afterPropertiesSet(); } @@ -142,61 +103,40 @@ public void testNullResultsWithSession() throws Exception { Neo4jItemReader itemReader = buildSessionBasedReader(); - ArgumentCaptor query = ArgumentCaptor.forClass(String.class); + ArgumentCaptor query = ArgumentCaptor.forClass(Statement.class); - when(this.sessionFactory.openSession()).thenReturn(this.session); - when(this.session.query(eq(String.class), query.capture(), isNull())).thenReturn(null); + when(this.neo4jTemplate.findAll(query.capture(), isNull(), eq(String.class))).thenReturn(List.of()); assertFalse(itemReader.doPageRead().hasNext()); - assertEquals("START n=node(*) RETURN * ORDER BY n.age SKIP 0 LIMIT 50", query.getValue()); + Node node = Cypher.anyNode().named("n"); + assertEquals(Cypher.match(node).returning(node).skip(0).limit(50).build().getCypher(), query.getValue().getCypher()); + } @SuppressWarnings("unchecked") @Test public void testNoResultsWithSession() throws Exception { Neo4jItemReader itemReader = buildSessionBasedReader(); - ArgumentCaptor query = ArgumentCaptor.forClass(String.class); + ArgumentCaptor query = ArgumentCaptor.forClass(Statement.class); - when(this.sessionFactory.openSession()).thenReturn(this.session); - when(this.session.query(eq(String.class), query.capture(), isNull())).thenReturn(result); + when(this.neo4jTemplate.findAll(query.capture(), any(), eq(String.class))).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()); + Node node = Cypher.anyNode().named("n"); + assertEquals(Cypher.match(node).returning(node).skip(0).limit(50).build().getCypher(), query.getValue().getCypher()); } @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(this.neo4jTemplate.findAll(any(Statement.class), isNull(), eq(String.class))).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()); - } } 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..aa9bda25 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,471 @@ 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; - private Neo4jItemWriter writer; + private Neo4jTemplate neo4jTemplate; + private Driver neo4jDriver; + private Neo4jMappingContext neo4jMappingContext; - @Mock - private SessionFactory sessionFactory; - @Mock - private Session session; + @BeforeEach + void setup() { + neo4jTemplate = mock(Neo4jTemplate.class); + neo4jDriver = mock(Driver.class); + neo4jMappingContext = mock(Neo4jMappingContext.class); + } @Test - public void testAfterPropertiesSet() throws Exception{ + public void testAfterPropertiesSet() { writer = new Neo4jItemWriter<>(); try { writer.afterPropertiesSet(); - fail("SessionFactory was not set but exception was not thrown."); + fail("Neo4jTemplate was not set but exception was not thrown."); } catch (IllegalStateException iae) { - assertEquals("A SessionFactory is required", iae.getMessage()); + assertEquals("A Neo4jTemplate is required", iae.getMessage()); } catch (Throwable t) { fail("Wrong exception was thrown."); } - writer.setSessionFactory(this.sessionFactory); + writer.setNeo4jTemplate(this.neo4jTemplate); - writer.afterPropertiesSet(); + 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 = new Neo4jItemWriter<>(); + writer.setNeo4jMappingContext(this.neo4jMappingContext); - writer.setSessionFactory(this.sessionFactory); + 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 testWriteNullSession() throws Exception { - + public void testWriteNoItems() { writer = new Neo4jItemWriter<>(); - writer.setSessionFactory(this.sessionFactory); + writer.setNeo4jTemplate(this.neo4jTemplate); + writer.setNeo4jDriver(this.neo4jDriver); + writer.setNeo4jMappingContext(this.neo4jMappingContext); writer.afterPropertiesSet(); - writer.write(null); + writer.write(Chunk.of()); - verifyNoInteractions(this.session); + verifyNoInteractions(this.neo4jTemplate); } @Test - public void testWriteNullWithSession() throws Exception { + public void testWriteItems() { writer = new Neo4jItemWriter<>(); - writer.setSessionFactory(this.sessionFactory); + writer.setNeo4jTemplate(this.neo4jTemplate); + writer.setNeo4jDriver(this.neo4jDriver); + writer.setNeo4jMappingContext(this.neo4jMappingContext); writer.afterPropertiesSet(); - when(this.sessionFactory.openSession()).thenReturn(this.session); - writer.write(null); + writer.write(Chunk.of(new MyEntity("foo"), new MyEntity("bar"))); - verifyNoInteractions(this.session); + verify(this.neo4jTemplate).saveAll(List.of(new MyEntity("foo"), new MyEntity("bar"))); } @Test - public void testWriteNoItemsWithSession() throws Exception { + 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.setSessionFactory(this.sessionFactory); + writer.setNeo4jTemplate(this.neo4jTemplate); + writer.setNeo4jDriver(this.neo4jDriver); + writer.setNeo4jMappingContext(this.neo4jMappingContext); writer.afterPropertiesSet(); - when(this.sessionFactory.openSession()).thenReturn(this.session); - writer.write(new ArrayList<>()); + writer.setDelete(true); - verifyNoInteractions(this.session); - } + Chunk myEntities = Chunk.of(new MyEntity("id1"), new MyEntity("id2")); + writer.write(myEntities); - @Test - public void testWriteItemsWithSession() throws Exception { - writer = new Neo4jItemWriter<>(); + verify(this.neo4jDriver, new Times(2)).executableQuery("MATCH (MyEntity) WHERE MyEntity.idField = $id DETACH DELETE MyEntity"); + } - writer.setSessionFactory(this.sessionFactory); - writer.afterPropertiesSet(); + private static class MyEntity { + public final String idField; - List items = new ArrayList<>(); - items.add("foo"); - items.add("bar"); + private MyEntity(String idField) { + this.idField = idField; + } - when(this.sessionFactory.openSession()).thenReturn(this.session); - writer.write(items); + @Override + public boolean equals(Object object) { + if (this == object) return true; + if (object == null || getClass() != object.getClass()) return false; + MyEntity myEntity = (MyEntity) object; + return Objects.equals(idField, myEntity.idField); + } - verify(this.session).save("foo"); - verify(this.session).save("bar"); + @Override + public int hashCode() { + return Objects.hash(idField); + } } - @Test - public void testDeleteItemsWithSession() throws Exception { - writer = new Neo4jItemWriter<>(); + 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.getField("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; + } + }); + } - writer.setSessionFactory(this.sessionFactory); - writer.afterPropertiesSet(); + @Override + public Optional getDynamicLabelsProperty() { + return Optional.empty(); + } - List items = new ArrayList<>(); - items.add("foo"); - items.add("bar"); + @Override + public boolean isRelationshipPropertiesEntity() { + return false; + } - writer.setDelete(true); + @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(); + } - when(this.sessionFactory.openSession()).thenReturn(this.session); - writer.write(items); + @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; + } - verify(this.session).delete("foo"); - verify(this.session).delete("bar"); + @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..857e2ed0 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,181 +16,127 @@ 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; + private List result; + private Neo4jTemplate neo4jTemplate; + private StatementBuilder.OngoingReadingAndReturn dummyStatement = Cypher.match(Cypher.anyNode()).returning(Cypher.anyNode()); - @Mock - private SessionFactory sessionFactory; - - @Mock - private Session session; + @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() - .sessionFactory(this.sessionFactory) + .neo4jTemplate(this.neo4jTemplate) .targetType(String.class) - .startStatement("n=node(*)") - .orderByStatement("n.age") + .statement(dummyStatement) .pageSize(50).name("bar") - .matchStatement("n -- m") - .whereStatement("has(n.name)") - .returnStatement("m").build(); + .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)) + when(this.neo4jTemplate.findAll(any(Statement.class), any(), eq(String.class))) .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()); + assertEquals("foo", itemReader.read()); + assertEquals("bar", itemReader.read()); + assertEquals("baz", itemReader.read()); } @Test public void testCurrentSize() throws Exception { Neo4jItemReader itemReader = new Neo4jItemReaderBuilder() - .sessionFactory(this.sessionFactory) + .neo4jTemplate(this.neo4jTemplate) .targetType(String.class) - .startStatement("n=node(*)") - .orderByStatement("n.age") + .statement(dummyStatement) .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)) + when(this.neo4jTemplate.findAll(any(Statement.class), any(), eq(String.class))) .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()); + assertEquals("foo", itemReader.read()); + assertNull(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()); + assertEquals("neo4jTemplate is required.", iae.getMessage()); } } @Test public void testZeroPageSize() { validateExceptionMessage(new Neo4jItemReaderBuilder() - .sessionFactory(this.sessionFactory) + .neo4jTemplate(this.neo4jTemplate) .targetType(String.class) - .startStatement("n=node(*)") - .returnStatement("*") - .orderByStatement("n.age") + .statement(dummyStatement) .pageSize(0) - .name("foo") - .matchStatement("n -- m") - .whereStatement("has(n.name)") - .returnStatement("m"), + .name("foo"), "pageSize must be greater than zero"); } @Test public void testZeroMaxItemCount() { validateExceptionMessage(new Neo4jItemReaderBuilder() - .sessionFactory(this.sessionFactory) + .neo4jTemplate(this.neo4jTemplate) .targetType(String.class) - .startStatement("n=node(*)") - .returnStatement("*") - .orderByStatement("n.age") + .statement(dummyStatement) .pageSize(5) .maxItemCount(0) - .name("foo") - .matchStatement("n -- m") - .whereStatement("has(n.name)") - .returnStatement("m"), + .name("foo"), "maxItemCount must be greater than zero"); } @Test public void testCurrentItemCountGreaterThanMaxItemCount() { validateExceptionMessage(new Neo4jItemReaderBuilder() - .sessionFactory(this.sessionFactory) + .neo4jTemplate(this.neo4jTemplate) .targetType(String.class) - .startStatement("n=node(*)") - .returnStatement("*") - .orderByStatement("n.age") + .statement(dummyStatement) .pageSize(5) .maxItemCount(5) .currentItemCount(6) - .name("foo") - .matchStatement("n -- m") - .whereStatement("has(n.name)") - .returnStatement("m"), + .name("foo"), "maxItemCount must be greater than currentItemCount"); } @@ -198,21 +144,17 @@ public void testCurrentItemCountGreaterThanMaxItemCount() { public void testNullName() { validateExceptionMessage( new Neo4jItemReaderBuilder() - .sessionFactory(this.sessionFactory) + .neo4jTemplate(this.neo4jTemplate) .targetType(String.class) - .startStatement("n=node(*)") - .returnStatement("*") - .orderByStatement("n.age") + .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() - .sessionFactory(this.sessionFactory) + .neo4jTemplate(this.neo4jTemplate) .targetType(String.class) - .startStatement("n=node(*)") - .returnStatement("*") - .orderByStatement("n.age") + .statement(dummyStatement) .saveState(false) .pageSize(50) .build(); @@ -222,59 +164,21 @@ public void testNullName() { public void testNullTargetType() { validateExceptionMessage( new Neo4jItemReaderBuilder() - .sessionFactory(this.sessionFactory) - .startStatement("n=node(*)") - .returnStatement("*") - .orderByStatement("n.age") + .neo4jTemplate(this.neo4jTemplate) + .statement(dummyStatement) .pageSize(50) - .name("bar") - .matchStatement("n -- m") - .whereStatement("has(n.name)") - .returnStatement("m"), + .name("bar"), "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() { + public void testNullStatement() { validateExceptionMessage( new Neo4jItemReaderBuilder() - .sessionFactory(this.sessionFactory) + .neo4jTemplate(this.neo4jTemplate) .targetType(String.class) - .startStatement("n=node(*)") - .returnStatement("*") - .pageSize(50) - .name("bar") - .matchStatement("n -- m") - .whereStatement("has(n.name)") - .returnStatement("m"), - "orderByStatement is required."); + .pageSize(50).name("bar"), + "statement is required."); } private void validateExceptionMessage(Neo4jItemReaderBuilder builder, String message) { @@ -283,8 +187,7 @@ private void validateExceptionMessage(Neo4jItemReaderBuilder builder, String fail("IllegalArgumentException should have been thrown"); } catch (IllegalArgumentException iae) { - assertEquals("IllegalArgumentException message did not match the expected result.", message, - iae.getMessage()); + 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..282dcc8b 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,79 +16,116 @@ 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(); + private Neo4jTemplate neo4jTemplate; + + private Driver neo4jDriver; - @Mock - private SessionFactory sessionFactory; - @Mock - private Session session; + private Neo4jMappingContext neo4jMappingContext; + + @BeforeEach + void setup() { + neo4jDriver = mock(Driver.class); + neo4jTemplate = mock(Neo4jTemplate.class); + neo4jMappingContext = mock(Neo4jMappingContext.class); + } @Test - public void testBasicWriter() throws Exception{ + public void testBasicWriter() { Neo4jItemWriter writer = new Neo4jItemWriterBuilder() - .sessionFactory(this.sessionFactory) + .neo4jTemplate(this.neo4jTemplate) + .neo4jDriver(this.neo4jDriver) + .neo4jMappingContext(this.neo4jMappingContext) .build(); - List items = new ArrayList<>(); - items.add("foo"); - items.add("bar"); - when(this.sessionFactory.openSession()).thenReturn(this.session); + Chunk items = Chunk.of("foo", "bar"); 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"); + verify(this.neo4jTemplate).saveAll(items.getItems()); + verify(this.neo4jDriver, never()).executableQuery(anyString()); } @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"); + 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"); - 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"); + 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 testNoSessionFactory() { + public void testNoNeo4jTemplate() { try { new Neo4jItemWriterBuilder().build(); - fail("SessionFactory was not set but exception was not thrown."); + fail("Neo4jTemplate was not set but exception was not thrown."); } catch (IllegalArgumentException iae) { - assertEquals("sessionFactory is required.", iae.getMessage()); + assertEquals("neo4jTemplate is required.", iae.getMessage()); } } From fd60767db8c8227dec534e70983044ef96bb0fd6 Mon Sep 17 00:00:00 2001 From: Gerrit Meier Date: Thu, 4 Jan 2024 14:55:04 +0100 Subject: [PATCH 2/6] Readme adjustments. --- spring-batch-neo4j/README.md | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/spring-batch-neo4j/README.md b/spring-batch-neo4j/README.md index d041184a..f7b6c82a 100644 --- a/spring-batch-neo4j/README.md +++ b/spring-batch-neo4j/README.md @@ -28,9 +28,12 @@ Neo4jItemWriter writer = new Neo4jItemWriterBuilder() ## Minimal Spring Boot example -With a Spring Boot application containing the additional dependencies `spring-boot-starter-neo4j` and `spring-batch-neo4j`, -the following _build.gradle_ dependency definition is the minimal needed. -Please note the exclusion for Spring JDBC from the `spring-boot-starter-batch` to avoid any need for JDBC-based connections. +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 { @@ -46,6 +49,8 @@ dependencies { } ``` +An example of the usage can be seen in the following example, implementing the `CommandLineRunner` interface. + ```java @SpringBootApplication public class TestSpringBatchApplication implements CommandLineRunner { From 361a9ddf5977c99f5f912d297d2171f140289261 Mon Sep 17 00:00:00 2001 From: Gerrit Meier Date: Thu, 4 Jan 2024 14:58:29 +0100 Subject: [PATCH 3/6] Use Java 17 for GH action build. --- .github/workflows/spring-batch-neo4j.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/spring-batch-neo4j.yml b/.github/workflows/spring-batch-neo4j.yml index 531548ae..af46500d 100644 --- a/.github/workflows/spring-batch-neo4j.yml +++ b/.github/workflows/spring-batch-neo4j.yml @@ -12,10 +12,10 @@ jobs: steps: - name: Checkout source code uses: actions/checkout@v2 - - name: Set up JDK 1.8 + - name: Set up JDK 17 uses: actions/setup-java@v1 with: - java-version: 1.8 + java-version: 17 - name: Build with Maven run: mvn -B package --file pom.xml working-directory: spring-batch-neo4j From f88eec141d436b6a0183454572ac6b4ab1931ee3 Mon Sep 17 00:00:00 2001 From: Gerrit Meier Date: Tue, 20 Aug 2024 08:24:33 +0200 Subject: [PATCH 4/6] Review: code changes --- spring-batch-neo4j/pom.xml | 8 +++--- .../extensions/neo4j/Neo4jItemWriter.java | 1 - .../neo4j/builder/Neo4jItemWriterBuilder.java | 9 ++---- .../neo4j/Neo4jItemReaderTests.java | 28 ++++++++----------- .../neo4j/Neo4jItemWriterTests.java | 22 ++------------- 5 files changed, 20 insertions(+), 48 deletions(-) diff --git a/spring-batch-neo4j/pom.xml b/spring-batch-neo4j/pom.xml index 3d9697b3..38d9801f 100644 --- a/spring-batch-neo4j/pom.xml +++ b/spring-batch-neo4j/pom.xml @@ -57,16 +57,16 @@ 17 - 5.1.0 + 5.1.2 7.2.1 3.18.1 - 5.10.1 - 5.8.0 + 5.11.0 + 5.12.0 - 3.8.1 + 3.13.0 3.2.0 3.2.1
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 f3d04992..5252402b 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 @@ -49,7 +49,6 @@ * @author Glenn Renfro * @author Mahmoud Ben Hassine * @author Gerrit Meier - * */ public class Neo4jItemWriter implements ItemWriter, InitializingBean { 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 77b8b54c..d1b51d70 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 @@ -49,7 +49,6 @@ public class Neo4jItemWriterBuilder { */ public Neo4jItemWriterBuilder delete(boolean delete) { this.delete = delete; - return this; } @@ -62,7 +61,6 @@ public Neo4jItemWriterBuilder delete(boolean delete) { */ public Neo4jItemWriterBuilder neo4jTemplate(Neo4jTemplate neo4jTemplate) { this.neo4jTemplate = neo4jTemplate; - return this; } @@ -73,7 +71,6 @@ public Neo4jItemWriterBuilder neo4jTemplate(Neo4jTemplate neo4jTemplate) { */ public Neo4jItemWriterBuilder neo4jDriver(Driver neo4jDriver) { this.neo4jDriver = neo4jDriver; - return this; } @@ -84,7 +81,6 @@ public Neo4jItemWriterBuilder neo4jDriver(Driver neo4jDriver) { */ public Neo4jItemWriterBuilder neo4jMappingContext(Neo4jMappingContext neo4jMappingContext) { this.neo4jMappingContext = neo4jMappingContext; - return this; } @@ -102,7 +98,6 @@ public Neo4jItemWriter build() { 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 16566a8f..1a712210 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 @@ -25,24 +25,25 @@ import org.springframework.data.neo4j.core.Neo4jTemplate; import java.util.Arrays; -import java.util.Collections; import java.util.List; -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.ArgumentMatchers.*; +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 { - private List result; private Neo4jTemplate neo4jTemplate; - @SuppressWarnings("unchecked") @BeforeEach void setup() { neo4jTemplate = mock(Neo4jTemplate.class); - result = mock(List.class); } private Neo4jItemReader buildSessionBasedReader() { @@ -97,9 +98,8 @@ public void testAfterPropertiesSet() { reader.afterPropertiesSet(); } - @SuppressWarnings("unchecked") @Test - public void testNullResultsWithSession() throws Exception { + public void testNullResultsWithSession() { Neo4jItemReader itemReader = buildSessionBasedReader(); @@ -113,28 +113,24 @@ public void testNullResultsWithSession() throws Exception { } - @SuppressWarnings("unchecked") @Test - public void testNoResultsWithSession() throws Exception { + public void testNoResultsWithSession() { Neo4jItemReader itemReader = buildSessionBasedReader(); ArgumentCaptor query = ArgumentCaptor.forClass(Statement.class); - when(this.neo4jTemplate.findAll(query.capture(), any(), eq(String.class))).thenReturn(result); - when(result.iterator()).thenReturn(Collections.emptyIterator()); + 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()); } - @SuppressWarnings("serial") @Test - public void testResultsWithMatchAndWhereWithSession() throws Exception { + public void testResultsWithMatchAndWhereWithSession() { Neo4jItemReader itemReader = buildSessionBasedReader(); itemReader.afterPropertiesSet(); - when(this.neo4jTemplate.findAll(any(Statement.class), isNull(), eq(String.class))).thenReturn(result); - when(result.iterator()).thenReturn(Arrays.asList("foo", "bar", "baz").iterator()); + 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 aa9bda25..e3a3662d 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 @@ -165,25 +165,7 @@ public T execute(Collector recordCollector, ResultFinish verify(this.neo4jDriver, new Times(2)).executableQuery("MATCH (MyEntity) WHERE MyEntity.idField = $id DETACH DELETE MyEntity"); } - private static class MyEntity { - public final String idField; - - private MyEntity(String idField) { - this.idField = idField; - } - - @Override - public boolean equals(Object object) { - if (this == object) return true; - if (object == null || getClass() != object.getClass()) return false; - MyEntity myEntity = (MyEntity) object; - return Objects.equals(idField, myEntity.idField); - } - - @Override - public int hashCode() { - return Objects.hash(idField); - } + private record MyEntity(String idField) { } private static class TestEntity extends BasicPersistentEntity @@ -245,7 +227,7 @@ public Method getWither() { @Override public Field getField() { try { - return MyEntity.class.getField("idField"); + return MyEntity.class.getDeclaredField("idField"); } catch (NoSuchFieldException e) { throw new RuntimeException(e); } From 74dd0722b2059bfd46ae3166fd7fb40c0748c4d4 Mon Sep 17 00:00:00 2001 From: Gerrit Meier Date: Tue, 20 Aug 2024 08:25:22 +0200 Subject: [PATCH 5/6] Review: reformat --- .../extensions/neo4j/Neo4jItemReader.java | 153 ++-- .../extensions/neo4j/Neo4jItemWriter.java | 203 ++--- .../neo4j/builder/Neo4jItemReaderBuilder.java | 336 +++---- .../neo4j/builder/Neo4jItemWriterBuilder.java | 127 +-- .../neo4j/Neo4jItemReaderTests.java | 138 +-- .../neo4j/Neo4jItemWriterTests.java | 838 +++++++++--------- .../builder/Neo4jItemReaderBuilderTests.java | 300 ++++--- .../builder/Neo4jItemWriterBuilderTests.java | 176 ++-- 8 files changed, 1136 insertions(+), 1135 deletions(-) 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 6545032b..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 @@ -58,87 +58,86 @@ *

* * @param type of entity to load - * * @author Michael Minella * @author Mahmoud Ben Hassine * @author Gerrit Meier */ public class Neo4jItemReader extends AbstractPaginatedDataItemReader implements InitializingBean { - 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(); - } + 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 5252402b..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 @@ -44,7 +44,6 @@ *

* * @param type of the entity to write - * * @author Michael Minella * @author Glenn Renfro * @author Mahmoud Ben Hassine @@ -52,104 +51,106 @@ */ public class Neo4jItemWriter implements ItemWriter, InitializingBean { - 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); - } + 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 d9804af6..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 @@ -27,179 +27,179 @@ * 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 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; - } + 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 d1b51d70..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 @@ -26,78 +26,81 @@ * 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 Neo4jTemplate neo4jTemplate; - private Driver neo4jDriver; - private Neo4jMappingContext neo4jMappingContext; + 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; - return this; - } + /** + * 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; + } - /** - * 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 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; + } - /** - * 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; - } + /** + * 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; + } - /** - * 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; - } + /** + * 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(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; - } + /** + * 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 1a712210..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 @@ -39,100 +39,100 @@ public class Neo4jItemReaderTests { - private Neo4jTemplate neo4jTemplate; + private Neo4jTemplate neo4jTemplate; - @BeforeEach - void setup() { - neo4jTemplate = mock(Neo4jTemplate.class); - } + @BeforeEach + void setup() { + neo4jTemplate = mock(Neo4jTemplate.class); + } - private Neo4jItemReader buildSessionBasedReader() { - Neo4jItemReader reader = new Neo4jItemReader<>(); + 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(); + 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; - } + return reader; + } - @Test - public void testAfterPropertiesSet() { + @Test + public void testAfterPropertiesSet() { - Neo4jItemReader reader = new Neo4jItemReader<>(); + 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); - } + 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); + 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); - } + 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.setTargetType(String.class); - reader.setStatement(Cypher.match(Cypher.anyNode()).returning(Cypher.anyNode())); + reader.setStatement(Cypher.match(Cypher.anyNode()).returning(Cypher.anyNode())); - reader.afterPropertiesSet(); + reader.afterPropertiesSet(); - reader = new Neo4jItemReader<>(); - reader.setNeo4jTemplate(this.neo4jTemplate); - reader.setTargetType(String.class); - reader.setStatement(Cypher.match(Cypher.anyNode()).returning(Cypher.anyNode())); + reader = new Neo4jItemReader<>(); + reader.setNeo4jTemplate(this.neo4jTemplate); + reader.setTargetType(String.class); + reader.setStatement(Cypher.match(Cypher.anyNode()).returning(Cypher.anyNode())); - reader.afterPropertiesSet(); - } + reader.afterPropertiesSet(); + } - @Test - public void testNullResultsWithSession() { + @Test + public void testNullResultsWithSession() { - Neo4jItemReader itemReader = buildSessionBasedReader(); + Neo4jItemReader itemReader = buildSessionBasedReader(); - ArgumentCaptor query = ArgumentCaptor.forClass(Statement.class); + ArgumentCaptor query = ArgumentCaptor.forClass(Statement.class); - when(this.neo4jTemplate.findAll(query.capture(), isNull(), eq(String.class))).thenReturn(List.of()); + 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()); + 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); + @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()); + 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()); - } + 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(); + @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")); + when(this.neo4jTemplate.findAll(any(Statement.class), isNull(), eq(String.class))).thenReturn(Arrays.asList("foo", "bar", "baz")); - assertTrue(itemReader.doPageRead().hasNext()); - } + 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 e3a3662d..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 @@ -46,423 +46,423 @@ public class Neo4jItemWriterTests { - 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; - } - } + 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 857e2ed0..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 @@ -39,155 +39,153 @@ */ public class Neo4jItemReaderBuilderTests { - 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()); - } - } + 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 282dcc8b..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 @@ -40,93 +40,93 @@ */ public class Neo4jItemWriterBuilderTests { - 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()); - } - } + 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()); + } + } } From 925c8cc42bb038dfa9b5f30a80d1d9638cc28213 Mon Sep 17 00:00:00 2001 From: Gerrit Meier Date: Tue, 20 Aug 2024 08:31:13 +0200 Subject: [PATCH 6/6] Review: update GitHub actions. --- .github/workflows/spring-batch-neo4j.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/spring-batch-neo4j.yml b/.github/workflows/spring-batch-neo4j.yml index af46500d..20218f90 100644 --- a/.github/workflows/spring-batch-neo4j.yml +++ b/.github/workflows/spring-batch-neo4j.yml @@ -11,10 +11,11 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout source code - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Set up JDK 17 - uses: actions/setup-java@v1 + uses: actions/setup-java@v4 with: + distribution: 'temurin' java-version: 17 - name: Build with Maven run: mvn -B package --file pom.xml