Update Neo4j extension to use Spring Data Neo4j. (#125)

Move away from the Neo4j OGM usage because Spring Data Neo4j
fits better in the ecosystem, spring-batch is settled in.

Previously there was no visible difference to users because Spring Data Neo4j
was based on Neo4j OGM which isn't the case anymore.
This commit is contained in:
Gerrit Meier
2024-08-20 17:50:41 +02:00
committed by GitHub
parent 42093bc813
commit 41cbb93142
11 changed files with 1242 additions and 1058 deletions

View File

@@ -11,11 +11,12 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout source code
uses: actions/checkout@v2
- name: Set up JDK 1.8
uses: actions/setup-java@v1
uses: actions/checkout@v4
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
java-version: 1.8
distribution: 'temurin'
java-version: 17
- name: Build with Maven
run: mvn -B package --file pom.xml
working-directory: spring-batch-neo4j

View File

@@ -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<String> itemReader = new Neo4jItemReaderBuilder<String>()
.sessionFactory(sessionFactory)
.name("itemReader")
.targetType(String.class)
.startStatement("n=node(*)")
.orderByStatement("n.age")
.matchStatement("n -- m")
.whereStatement("has(n.name)")
.returnStatement("m")
Neo4jItemReader<User> reader = new Neo4jItemReaderBuilder<User>()
.neo4jTemplate(neo4jTemplate)
.name("userReader")
.statement(Cypher.match(userNode).returning(userNode))
.targetType(User.class)
.pageSize(50)
.build();
```
@@ -24,8 +19,93 @@ Neo4jItemReader<String> itemReader = new Neo4jItemReaderBuilder<String>()
The `Neo4jItemWriter` can be configured as follows:
```java
SessionFactory sessionFactory = ...
Neo4jItemWriter<String> writer = new Neo4jItemWriterBuilder<String>()
.sessionFactory(sessionFactory)
Neo4jItemWriter<User> writer = new Neo4jItemWriterBuilder<User>()
.neo4jTemplate(neo4jTemplate)
.neo4jDriver(driver)
.neo4jMappingContext(mappingContext)
.build();
```
## Minimal Spring Boot example
Additional to the already existing dependencies in a new Spring Boot application,
`spring-boot-starter-data-neo4j`, `spring-batch-neo4j` and the `spring-boot-starter-batch` are needed
but `spring-jdbc` and `spring-boot-starter-jdbc` must be explicitly excluded.
The exclusions are mandatory to avoid any need for JDBC-based connections, like JDBC URI etc.
See the following _build.gradle_ dependency definition for a minimal example.
```groovy
dependencies {
implementation ('org.springframework.boot:spring-boot-starter-batch') {
exclude group: 'org.springframework', module: 'spring-jdbc'
exclude group: 'org.springframework.boot', module: 'spring-boot-starter-jdbc'
}
// current development version 0.2.0-SNAPSHOT
implementation 'org.springframework.batch.extensions:spring-batch-neo4j'
implementation 'org.springframework.boot:spring-boot-starter-data-neo4j'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.batch:spring-batch-test'
}
```
An example of the usage can be seen in the following example, implementing the `CommandLineRunner` interface.
```java
@SpringBootApplication
public class TestSpringBatchApplication implements CommandLineRunner {
// those dependencies are created by Spring Boot's
// spring-data-neo4j autoconfiguration
@Autowired
private Driver driver;
@Autowired
private Neo4jMappingContext mappingContext;
@Autowired
private Neo4jTemplate neo4jTemplate;
public static void main(String[] args) {
SpringApplication.run(TestSpringBatchApplication.class, args);
}
@Override
public void run(String... args) {
// writing
Neo4jItemWriter<User> writer = new Neo4jItemWriterBuilder<User>()
.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<User> reader = new Neo4jItemReaderBuilder<User>()
.neo4jTemplate(neo4jTemplate)
.name("userReader")
.statement(Cypher.match(userNode).returning(userNode))
.targetType(User.class)
.build();
List<User> 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;
}
}
}
```

View File

@@ -54,19 +54,19 @@
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
<java.version>17</java.version>
<!-- Production dependencies-->
<spring.batch.version>4.3.3</spring.batch.version>
<neo4j-ogm-core.version>3.2.21</neo4j-ogm-core.version>
<spring.batch.version>5.1.2</spring.batch.version>
<spring-data-neo4j.version>7.2.1</spring-data-neo4j.version>
<!-- Test Dependencies -->
<assertj.version>3.18.1</assertj.version>
<junit.version>4.13.2</junit.version>
<mockito.version>3.6.0</mockito.version>
<junit.version>5.11.0</junit.version>
<mockito.version>5.12.0</mockito.version>
<!-- Maven plugins -->
<maven-compiler-plugin.version>3.8.1</maven-compiler-plugin.version>
<maven-compiler-plugin.version>3.13.0</maven-compiler-plugin.version>
<maven-javadoc-plugin.version>3.2.0</maven-javadoc-plugin.version>
<maven-source-plugin.version>3.2.1</maven-source-plugin.version>
</properties>
@@ -83,15 +83,15 @@
<version>${spring.batch.version}</version>
</dependency>
<dependency>
<groupId>org.neo4j</groupId>
<artifactId>neo4j-ogm-core</artifactId>
<version>${neo4j-ogm-core.version}</version>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-neo4j</artifactId>
<version>${spring-data-neo4j.version}</version>
</dependency>
<!-- Test Dependencies -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>

View File

@@ -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;
/**
* <p>
@@ -38,7 +37,7 @@ import org.springframework.util.StringUtils;
* </p>
*
* <p>
* 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 @@ import org.springframework.util.StringUtils;
* </p>
*
* <p>
* 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.
* </p>
@@ -58,167 +57,87 @@ import org.springframework.util.StringUtils;
* environment (no restart available).
* </p>
*
* @param <T> type of entity to load
* @author Michael Minella
* @author Mahmoud Ben Hassine
* @author Gerrit Meier
*/
public class Neo4jItemReader<T> extends AbstractPaginatedDataItemReader<T> 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<T> targetType;
private Class<T> targetType;
private Map<String, Object> parameterValues;
private Map<String, Object> 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<String, Object> parameterValues) {
this.parameterValues = 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<String, Object> parameterValues) {
this.parameterValues = parameterValues;
}
protected final Map<String, Object> getParameterValues() {
return this.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;
}
/**
* The start segment of the cypher query. START is prepended
* to the statement provided and should <em>not</em> be
* included.
*
* @param startStatement the start fragment of the cypher query.
*/
public void setStartStatement(String startStatement) {
this.startStatement = startStatement;
}
/**
* Establish the Neo4jTemplate for the reader.
*
* @param neo4jTemplate the template to use for the reader.
*/
public void setNeo4jTemplate(Neo4jTemplate neo4jTemplate) {
this.neo4jTemplate = neo4jTemplate;
}
/**
* The return statement of the cypher query. RETURN is prepended
* to the statement provided and should <em>not</em> be
* included
*
* @param returnStatement the return fragment of the cypher query.
*/
public void setReturnStatement(String returnStatement) {
this.returnStatement = returnStatement;
}
/**
* The object type to be returned from each call to {@link #read()}
*
* @param targetType the type of object to return.
*/
public void setTargetType(Class<T> targetType) {
this.targetType = targetType;
}
/**
* An optional match fragment of the cypher query. MATCH is
* prepended to the statement provided and should <em>not</em>
* be included.
*
* @param matchStatement the match fragment of the cypher query
*/
public void setMatchStatement(String matchStatement) {
this.matchStatement = matchStatement;
}
private Statement generateStatement() {
Statement builtStatement = statement
.skip(page * pageSize)
.limit(pageSize)
.build();
if (logger.isDebugEnabled()) {
logger.debug(Renderer.getDefaultRenderer().render(builtStatement));
}
/**
* An optional where fragment of the cypher query. WHERE is
* prepended to the statement provided and should <em>not</em>
* be included.
*
* @param whereStatement where fragment of the cypher query
*/
public void setWhereStatement(String whereStatement) {
this.whereStatement = whereStatement;
}
return builtStatement;
}
/**
* 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 <em>not</em> be included.
*
* @param orderByStatement order by fragment of the cypher query.
*/
public void setOrderByStatement(String orderByStatement) {
this.orderByStatement = orderByStatement;
}
/**
* 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");
}
protected SessionFactory getSessionFactory() {
return sessionFactory;
}
/**
* Establish the session factory for the reader.
* @param sessionFactory the factory to use for the reader.
*/
public void setSessionFactory(SessionFactory sessionFactory) {
this.sessionFactory = sessionFactory;
}
/**
* The object type to be returned from each call to {@link #read()}
*
* @param targetType the type of object to return.
*/
public void setTargetType(Class<T> targetType) {
this.targetType = targetType;
}
protected final Class<T> getTargetType() {
return this.targetType;
}
protected String generateLimitCypherQuery() {
StringBuilder query = new StringBuilder(128);
query.append("START ").append(startStatement);
query.append(matchStatement != null ? " MATCH " + matchStatement : "");
query.append(whereStatement != null ? " WHERE " + whereStatement : "");
query.append(" RETURN ").append(returnStatement);
query.append(" ORDER BY ").append(orderByStatement);
query.append(" SKIP " + (pageSize * page));
query.append(" LIMIT " + pageSize);
String resultingQuery = query.toString();
if (logger.isDebugEnabled()) {
logger.debug(resultingQuery);
}
return resultingQuery;
}
/**
* Checks mandatory properties
*
* @see InitializingBean#afterPropertiesSet()
*/
@Override
public void afterPropertiesSet() throws Exception {
Assert.state(sessionFactory != null,"A SessionFactory is required");
Assert.state(targetType != null, "The type to be returned is required");
Assert.state(StringUtils.hasText(startStatement), "A START statement is required");
Assert.state(StringUtils.hasText(returnStatement), "A RETURN statement is required");
Assert.state(StringUtils.hasText(orderByStatement), "A ORDER BY statement is required");
}
@SuppressWarnings("unchecked")
@Override
protected Iterator<T> doPageRead() {
Session session = getSessionFactory().openSession();
Iterable<T> queryResults = session.query(getTargetType(),
generateLimitCypherQuery(),
getParameterValues());
if(queryResults != null) {
return queryResults.iterator();
}
else {
return new ArrayList<T>().iterator();
}
}
@SuppressWarnings("unchecked")
@Override
protected Iterator<T> doPageRead() {
return neo4jTemplate.findAll(generateStatement(), parameterValues, targetType).iterator();
}
}

View File

@@ -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;
/**
* <p>
@@ -38,90 +43,114 @@ import org.springframework.util.CollectionUtils;
* behavior) so it can be used in multiple concurrent transactions.
* </p>
*
* @param <T> type of the entity to write
* @author Michael Minella
* @author Glenn Renfro
* @author Mahmoud Ben Hassine
*
* @author Gerrit Meier
*/
public class Neo4jItemWriter<T> implements ItemWriter<T>, InitializingBean {
protected static final Log logger = LogFactory
.getLog(Neo4jItemWriter.class);
private boolean delete = false;
private boolean delete = false;
private Neo4jTemplate neo4jTemplate;
private Neo4jMappingContext neo4jMappingContext;
private Driver neo4jDriver;
private SessionFactory sessionFactory;
/**
* Boolean flag indicating whether the writer should save or delete the item at write
* time.
*
* @param delete true if write should delete item, false if item should be saved.
* Default is false.
*/
public void setDelete(boolean delete) {
this.delete = delete;
}
/**
* 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;
}
/**
* Establish the session factory that will be used to create {@link Session} instances
* for interacting with Neo4j.
* @param sessionFactory sessionFactory to be used.
*/
public void setSessionFactory(SessionFactory sessionFactory) {
this.sessionFactory = sessionFactory;
}
/**
* 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;
}
/**
* Checks mandatory properties
*
* @see InitializingBean#afterPropertiesSet()
*/
@Override
public void afterPropertiesSet() throws Exception {
Assert.state(this.sessionFactory != null,
"A SessionFactory is required");
}
/**
* Neo4jMappingContext needed for determine the id type of the entity instances.
*
* @param neo4jMappingContext initialized mapping context
*/
public void setNeo4jMappingContext(Neo4jMappingContext neo4jMappingContext) {
this.neo4jMappingContext = neo4jMappingContext;
}
/**
* Write all items to the data store.
*
* @see org.springframework.batch.item.ItemWriter#write(java.util.List)
*/
@Override
public void write(List<? extends T> items) throws Exception {
if(!CollectionUtils.isEmpty(items)) {
doWrite(items);
}
}
/**
* 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");
}
/**
* Performs the actual write using the template. This can be overridden by
* a subclass if necessary.
*
* @param items the list of items to be persisted.
*/
protected void doWrite(List<? extends T> items) {
if(delete) {
delete(items);
}
else {
save(items);
}
}
/**
* Write all items to the data store.
*
* @see org.springframework.batch.item.ItemWriter#write(Chunk chunk)
*/
@Override
public void write(@NonNull Chunk<? extends T> chunk) {
if (!chunk.isEmpty()) {
doWrite(chunk.getItems());
}
}
private void delete(List<? extends T> items) {
Session session = this.sessionFactory.openSession();
/**
* Performs the actual write using the template. This can be overridden by
* a subclass if necessary.
*
* @param items the list of items to be persisted.
*/
protected void doWrite(List<? extends T> items) {
if (delete) {
delete(items);
} else {
save(items);
}
}
for(T item : items) {
session.delete(item);
}
}
private void delete(List<? extends T> items) {
for (T item : items) {
// Figure out id field individually because different
// id strategies could have been taken for classes within a
// business model hierarchy.
Neo4jPersistentEntity<?> nodeDescription = (Neo4jPersistentEntity<?>) this.neo4jMappingContext.getNodeDescription(item.getClass());
Object identifier = nodeDescription.getIdentifierAccessor(item).getRequiredIdentifier();
Node named = Cypher.anyNode().named(nodeDescription.getPrimaryLabel());
Statement statement = Cypher.match(named)
.where(nodeDescription.getIdDescription().asIdExpression(nodeDescription.getPrimaryLabel()).eq(Cypher.parameter("id")))
.detachDelete(named).build();
private void save(List<? extends T> items) {
Session session = this.sessionFactory.openSession();
String renderedStatement = Renderer.getDefaultRenderer().render(statement);
this.neo4jDriver.executableQuery(renderedStatement).withParameters(Map.of("id", identifier)).execute();
}
}
for (T item : items) {
session.save(item);
}
}
private void save(List<? extends T> items) {
this.neo4jTemplate.saveAll(items);
}
}

View File

@@ -16,258 +16,190 @@
package org.springframework.batch.extensions.neo4j.builder;
import java.util.Map;
import org.neo4j.ogm.session.SessionFactory;
import org.neo4j.cypherdsl.core.StatementBuilder;
import org.springframework.batch.extensions.neo4j.Neo4jItemReader;
import org.springframework.data.neo4j.core.Neo4jTemplate;
import org.springframework.util.Assert;
import java.util.Map;
/**
* A builder for the {@link Neo4jItemReader}.
*
* @param <T> type of the entity to read
* @author Glenn Renfro
* @author Gerrit Meier
* @see Neo4jItemReader
*/
public class Neo4jItemReaderBuilder<T> {
private SessionFactory sessionFactory;
private Neo4jTemplate neo4jTemplate;
private String startStatement;
private StatementBuilder.OngoingReadingAndReturn statement;
private String returnStatement;
private Class<T> targetType;
private String matchStatement;
private Map<String, Object> parameterValues;
private String whereStatement;
private int pageSize = 10;
private String orderByStatement;
private boolean saveState = true;
private Class<T> targetType;
private String name;
private Map<String, Object> parameterValues;
private int maxItemCount = Integer.MAX_VALUE;
private int pageSize = 10;
private int currentItemCount;
private boolean saveState = true;
/**
* 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<T> saveState(boolean saveState) {
this.saveState = saveState;
private String name;
return this;
}
private int maxItemCount = Integer.MAX_VALUE;
/**
* 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<T> name(String name) {
this.name = name;
private int currentItemCount;
return this;
}
/**
* 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<T> saveState(boolean saveState) {
this.saveState = saveState;
/**
* 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<T> maxItemCount(int maxItemCount) {
this.maxItemCount = maxItemCount;
return this;
}
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<T> name(String name) {
this.name = name;
/**
* 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<T> currentItemCount(int currentItemCount) {
this.currentItemCount = currentItemCount;
return this;
}
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<T> maxItemCount(int maxItemCount) {
this.maxItemCount = maxItemCount;
/**
* 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<T> neo4jTemplate(Neo4jTemplate neo4jTemplate) {
this.neo4jTemplate = neo4jTemplate;
return this;
}
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<T> currentItemCount(int currentItemCount) {
this.currentItemCount = currentItemCount;
/**
* 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<T> pageSize(int pageSize) {
this.pageSize = pageSize;
return this;
}
return this;
}
/**
* Establish the session factory for the reader.
* @param sessionFactory the factory to use for the reader.
* @return this instance for method chaining
* @see Neo4jItemReader#setSessionFactory(SessionFactory)
*/
public Neo4jItemReaderBuilder<T> sessionFactory(SessionFactory sessionFactory) {
this.sessionFactory = sessionFactory;
/**
* 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<T> parameterValues(Map<String, Object> parameterValues) {
this.parameterValues = parameterValues;
return this;
}
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<T> pageSize(int pageSize) {
this.pageSize = pageSize;
/**
* 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<T> statement(StatementBuilder.OngoingReadingAndReturn statement) {
this.statement = statement;
return this;
}
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<T> parameterValues(Map<String, Object> parameterValues) {
this.parameterValues = parameterValues;
/**
* 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<T> targetType(Class<T> targetType) {
this.targetType = targetType;
return this;
}
return this;
}
/**
* The start segment of the cypher query. START is prepended to the statement provided
* and should <em>not</em> be included.
*
* @param startStatement the start fragment of the cypher query.
* @return this instance for method chaining
* @see Neo4jItemReader#setStartStatement(String)
*/
public Neo4jItemReaderBuilder<T> startStatement(String startStatement) {
this.startStatement = startStatement;
/**
* Returns a fully constructed {@link Neo4jItemReader}.
*
* @return a new {@link Neo4jItemReader}
*/
public Neo4jItemReader<T> 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");
return this;
}
Neo4jItemReader<T> 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);
/**
* The return statement of the cypher query. RETURN is prepended to the statement
* provided and should <em>not</em> be included
*
* @param returnStatement the return fragment of the cypher query.
* @return this instance for method chaining
* @see Neo4jItemReader#setReturnStatement(String)
*/
public Neo4jItemReaderBuilder<T> 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 <em>not</em> be included.
*
* @param matchStatement the match fragment of the cypher query
* @return this instance for method chaining
* @see Neo4jItemReader#setMatchStatement(String)
*/
public Neo4jItemReaderBuilder<T> 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 <em>not</em> be included.
*
* @param whereStatement where fragment of the cypher query
* @return this instance for method chaining
* @see Neo4jItemReader#setWhereStatement(String)
*/
public Neo4jItemReaderBuilder<T> 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 <em>not</em> be included.
*
* @param orderByStatement order by fragment of the cypher query.
* @return this instance for method chaining
* @see Neo4jItemReader#setOrderByStatement(String)
*/
public Neo4jItemReaderBuilder<T> orderByStatement(String orderByStatement) {
this.orderByStatement = orderByStatement;
return this;
}
/**
* The object type to be returned from each call to {@link Neo4jItemReader#read()}
*
* @param targetType the type of object to return.
* @return this instance for method chaining
* @see Neo4jItemReader#setTargetType(Class)
*/
public Neo4jItemReaderBuilder<T> targetType(Class<T> targetType) {
this.targetType = targetType;
return this;
}
/**
* Returns a fully constructed {@link Neo4jItemReader}.
*
* @return a new {@link Neo4jItemReader}
*/
public Neo4jItemReader<T> build() {
if (this.saveState) {
Assert.hasText(this.name, "A name is required when saveState is set to true");
}
Assert.notNull(this.sessionFactory, "sessionFactory is required.");
Assert.notNull(this.targetType, "targetType is required.");
Assert.hasText(this.startStatement, "startStatement is required.");
Assert.hasText(this.returnStatement, "returnStatement is required.");
Assert.hasText(this.orderByStatement, "orderByStatement is required.");
Assert.isTrue(this.pageSize > 0, "pageSize must be greater than zero");
Assert.isTrue(this.maxItemCount > 0, "maxItemCount must be greater than zero");
Assert.isTrue(this.maxItemCount > this.currentItemCount , "maxItemCount must be greater than currentItemCount");
Neo4jItemReader<T> reader = new Neo4jItemReader<>();
reader.setMatchStatement(this.matchStatement);
reader.setOrderByStatement(this.orderByStatement);
reader.setPageSize(this.pageSize);
reader.setParameterValues(this.parameterValues);
reader.setSessionFactory(this.sessionFactory);
reader.setTargetType(this.targetType);
reader.setStartStatement(this.startStatement);
reader.setReturnStatement(this.returnStatement);
reader.setWhereStatement(this.whereStatement);
reader.setName(this.name);
reader.setSaveState(this.saveState);
reader.setCurrentItemCount(this.currentItemCount);
reader.setMaxItemCount(this.maxItemCount);
return reader;
}
return reader;
}
}

View File

@@ -1,10 +1,10 @@
/*
* Copyright 2017-2021 the original author or authors.
*
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
@@ -16,61 +16,91 @@
package org.springframework.batch.extensions.neo4j.builder;
import org.neo4j.ogm.session.Session;
import org.neo4j.ogm.session.SessionFactory;
import org.neo4j.driver.Driver;
import org.springframework.batch.extensions.neo4j.Neo4jItemWriter;
import org.springframework.data.neo4j.core.Neo4jTemplate;
import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext;
import org.springframework.util.Assert;
/**
* A builder implementation for the {@link Neo4jItemWriter}
*
* @param <T> type of the entity to write
* @author Glenn Renfro
* @author Gerrit Meier
* @see Neo4jItemWriter
*/
public class Neo4jItemWriterBuilder<T> {
private boolean delete = false;
private boolean delete = false;
private SessionFactory sessionFactory;
private Neo4jTemplate neo4jTemplate;
private Driver neo4jDriver;
private Neo4jMappingContext neo4jMappingContext;
/**
* Boolean flag indicating whether the writer should save or delete the item at write
* time.
* @param delete true if write should delete item, false if item should be saved.
* Default is false.
* @return The current instance of the builder
* @see Neo4jItemWriter#setDelete(boolean)
*/
public Neo4jItemWriterBuilder<T> delete(boolean delete) {
this.delete = delete;
/**
* Boolean flag indicating whether the writer should save or delete the item at write
* time.
*
* @param delete true if write should delete item, false if item should be saved.
* Default is false.
* @return The current instance of the builder
* @see Neo4jItemWriter#setDelete(boolean)
*/
public Neo4jItemWriterBuilder<T> delete(boolean delete) {
this.delete = delete;
return this;
}
return this;
}
/**
* Establish the session factory that will be used to create {@link Neo4jTemplate} instances
* for interacting with Neo4j.
*
* @param neo4jTemplate neo4jTemplate to be used.
* @return The current instance of the builder
* @see Neo4jItemWriter#setNeo4jTemplate(Neo4jTemplate)
*/
public Neo4jItemWriterBuilder<T> neo4jTemplate(Neo4jTemplate neo4jTemplate) {
this.neo4jTemplate = neo4jTemplate;
return this;
}
/**
* Establish the session factory that will be used to create {@link Session} instances
* for interacting with Neo4j.
* @param sessionFactory sessionFactory to be used.
* @return The current instance of the builder
* @see Neo4jItemWriter#setSessionFactory(SessionFactory)
*/
public Neo4jItemWriterBuilder<T> sessionFactory(SessionFactory sessionFactory) {
this.sessionFactory = sessionFactory;
/**
* Set the preconfigured Neo4j driver to be used within the built writer instance.
*
* @param neo4jDriver preconfigured Neo4j driver instance
* @return The current instance of the builder
*/
public Neo4jItemWriterBuilder<T> neo4jDriver(Driver neo4jDriver) {
this.neo4jDriver = neo4jDriver;
return this;
}
return this;
}
/**
* Set the Neo4jMappingContext to be used within the built writer instance.
*
* @param neo4jMappingContext initialized Neo4jMappingContext instance
* @return The current instance of the builder
*/
public Neo4jItemWriterBuilder<T> 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<T> build() {
Assert.notNull(sessionFactory, "sessionFactory is required.");
Neo4jItemWriter<T> writer = new Neo4jItemWriter<>();
writer.setDelete(this.delete);
writer.setSessionFactory(this.sessionFactory);
return writer;
}
/**
* Validates and builds a {@link org.springframework.batch.extensions.neo4j.Neo4jItemWriter}.
*
* @return a {@link Neo4jItemWriter}
*/
public Neo4jItemWriter<T> build() {
Assert.notNull(neo4jTemplate, "neo4jTemplate is required.");
Assert.notNull(neo4jDriver, "neo4jDriver is required.");
Assert.notNull(neo4jMappingContext, "neo4jMappingContext is required.");
Neo4jItemWriter<T> writer = new Neo4jItemWriter<>();
writer.setDelete(this.delete);
writer.setNeo4jTemplate(this.neo4jTemplate);
writer.setNeo4jDriver(this.neo4jDriver);
writer.setNeo4jMappingContext(this.neo4jMappingContext);
return writer;
}
}

View File

@@ -16,187 +16,123 @@
package org.springframework.batch.extensions.neo4j;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import org.junit.Rule;
import org.junit.Test;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.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 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 static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import java.util.Arrays;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.isNull;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class Neo4jItemReaderTests {
@Rule
public MockitoRule rule = MockitoJUnit.rule().silent();
private Neo4jTemplate neo4jTemplate;
@Mock
private Iterable<String> result;
@Mock
private SessionFactory sessionFactory;
@Mock
private Session session;
@BeforeEach
void setup() {
neo4jTemplate = mock(Neo4jTemplate.class);
}
private Neo4jItemReader<String> buildSessionBasedReader() throws Exception {
Neo4jItemReader<String> reader = new Neo4jItemReader<>();
private Neo4jItemReader<String> buildSessionBasedReader() {
Neo4jItemReader<String> reader = new Neo4jItemReader<>();
reader.setSessionFactory(this.sessionFactory);
reader.setTargetType(String.class);
reader.setStartStatement("n=node(*)");
reader.setReturnStatement("*");
reader.setOrderByStatement("n.age");
reader.setPageSize(50);
reader.afterPropertiesSet();
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() throws Exception {
@Test
public void testAfterPropertiesSet() {
Neo4jItemReader<String> reader = new Neo4jItemReader<>();
Neo4jItemReader<String> reader = new Neo4jItemReader<>();
try {
reader.afterPropertiesSet();
fail("SessionFactory was not set but exception was not thrown.");
} catch (IllegalStateException iae) {
assertEquals("A SessionFactory is required", iae.getMessage());
} catch (Throwable t) {
fail("Wrong exception was thrown:" + t);
}
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.setSessionFactory(this.sessionFactory);
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);
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.setStatement(Cypher.match(Cypher.anyNode()).returning(Cypher.anyNode()));
reader.setStartStatement("n=node(*)");
reader.afterPropertiesSet();
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 = new Neo4jItemReader<>();
reader.setNeo4jTemplate(this.neo4jTemplate);
reader.setTargetType(String.class);
reader.setStatement(Cypher.match(Cypher.anyNode()).returning(Cypher.anyNode()));
reader.setReturnStatement("n.name, n.phone");
reader.afterPropertiesSet();
}
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);
}
@Test
public void testNullResultsWithSession() {
reader.setOrderByStatement("n.age");
Neo4jItemReader<String> itemReader = buildSessionBasedReader();
reader.afterPropertiesSet();
ArgumentCaptor<Statement> query = ArgumentCaptor.forClass(Statement.class);
reader = new Neo4jItemReader<>();
reader.setSessionFactory(this.sessionFactory);
reader.setTargetType(String.class);
reader.setStartStatement("n=node(*)");
reader.setReturnStatement("n.name, n.phone");
reader.setOrderByStatement("n.age");
when(this.neo4jTemplate.findAll(query.capture(), isNull(), eq(String.class))).thenReturn(List.of());
reader.afterPropertiesSet();
}
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("unchecked")
@Test
public void testNullResultsWithSession() throws Exception {
}
Neo4jItemReader<String> itemReader = buildSessionBasedReader();
@Test
public void testNoResultsWithSession() {
Neo4jItemReader<String> itemReader = buildSessionBasedReader();
ArgumentCaptor<Statement> query = ArgumentCaptor.forClass(Statement.class);
ArgumentCaptor<String> query = ArgumentCaptor.forClass(String.class);
when(this.neo4jTemplate.findAll(query.capture(), any(), eq(String.class))).thenReturn(List.of());
when(this.sessionFactory.openSession()).thenReturn(this.session);
when(this.session.query(eq(String.class), query.capture(), isNull())).thenReturn(null);
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());
assertEquals("START n=node(*) RETURN * ORDER BY n.age SKIP 0 LIMIT 50", query.getValue());
}
@Test
public void testResultsWithMatchAndWhereWithSession() {
Neo4jItemReader<String> itemReader = buildSessionBasedReader();
itemReader.afterPropertiesSet();
@SuppressWarnings("unchecked")
@Test
public void testNoResultsWithSession() throws Exception {
Neo4jItemReader<String> itemReader = buildSessionBasedReader();
ArgumentCaptor<String> query = ArgumentCaptor.forClass(String.class);
when(this.neo4jTemplate.findAll(any(Statement.class), isNull(), eq(String.class))).thenReturn(Arrays.asList("foo", "bar", "baz"));
when(this.sessionFactory.openSession()).thenReturn(this.session);
when(this.session.query(eq(String.class), query.capture(), isNull())).thenReturn(result);
when(result.iterator()).thenReturn(Collections.emptyIterator());
assertTrue(itemReader.doPageRead().hasNext());
}
assertFalse(itemReader.doPageRead().hasNext());
assertEquals("START n=node(*) RETURN * ORDER BY n.age SKIP 0 LIMIT 50", query.getValue());
}
@SuppressWarnings("serial")
@Test
public void testResultsWithMatchAndWhereWithSession() throws Exception {
Neo4jItemReader<String> itemReader = buildSessionBasedReader();
itemReader.setMatchStatement("n -- m");
itemReader.setWhereStatement("has(n.name)");
itemReader.setReturnStatement("m");
itemReader.afterPropertiesSet();
when(this.sessionFactory.openSession()).thenReturn(this.session);
when(this.session.query(String.class, "START n=node(*) MATCH n -- m WHERE has(n.name) RETURN m ORDER BY n.age SKIP 0 LIMIT 50", null)).thenReturn(result);
when(result.iterator()).thenReturn(Arrays.asList("foo", "bar", "baz").iterator());
assertTrue(itemReader.doPageRead().hasNext());
}
@SuppressWarnings("serial")
@Test
public void testResultsWithMatchAndWhereWithParametersWithSession() throws Exception {
Neo4jItemReader<String> itemReader = buildSessionBasedReader();
Map<String, Object> 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());
}
}

View File

@@ -16,134 +16,453 @@
package org.springframework.batch.extensions.neo4j;
import java.util.ArrayList;
import java.util.List;
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 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 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.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 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<MyEntity> writer;
private Neo4jItemWriter<String> 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{
@Test
public void testAfterPropertiesSet() {
writer = new Neo4jItemWriter<>();
writer = new Neo4jItemWriter<>();
try {
writer.afterPropertiesSet();
fail("SessionFactory was not set but exception was not thrown.");
} catch (IllegalStateException iae) {
assertEquals("A SessionFactory is required", iae.getMessage());
} catch (Throwable t) {
fail("Wrong exception was thrown.");
}
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.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.afterPropertiesSet();
}
writer.setNeo4jDriver(this.neo4jDriver);
@Test
public void testWriteNullSession() throws Exception {
writer.afterPropertiesSet();
}
writer = new Neo4jItemWriter<>();
@Test
public void testWriteNoItems() {
writer = new Neo4jItemWriter<>();
writer.setSessionFactory(this.sessionFactory);
writer.afterPropertiesSet();
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 {
writer = new Neo4jItemWriter<>();
@Test
public void testWriteItems() {
writer = new Neo4jItemWriter<>();
writer.setSessionFactory(this.sessionFactory);
writer.afterPropertiesSet();
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 {
writer = new Neo4jItemWriter<>();
@Test
public void testDeleteItems() {
TypeInformation<MyEntity> typeInformation = TypeInformation.of(MyEntity.class);
NodeDescription<MyEntity> entity = new TestEntity<>(typeInformation);
when(neo4jMappingContext.getNodeDescription(MyEntity.class)).thenAnswer(invocationOnMock -> entity);
when(neo4jDriver.executableQuery(anyString())).thenReturn(new ExecutableQuery() {
@Override
public ExecutableQuery withParameters(Map<String, Object> parameters) {
return this;
}
writer.setSessionFactory(this.sessionFactory);
writer.afterPropertiesSet();
@Override
public ExecutableQuery withConfig(QueryConfig config) {
return null;
}
when(this.sessionFactory.openSession()).thenReturn(this.session);
writer.write(new ArrayList<>());
@Override
public <A, R, T> T execute(Collector<Record, A, R> recordCollector, ResultFinisher<R, T> resultFinisher) {
return null;
}
});
verifyNoInteractions(this.session);
}
writer = new Neo4jItemWriter<>();
@Test
public void testWriteItemsWithSession() throws Exception {
writer = new Neo4jItemWriter<>();
writer.setNeo4jTemplate(this.neo4jTemplate);
writer.setNeo4jDriver(this.neo4jDriver);
writer.setNeo4jMappingContext(this.neo4jMappingContext);
writer.afterPropertiesSet();
writer.setSessionFactory(this.sessionFactory);
writer.afterPropertiesSet();
writer.setDelete(true);
List<String> items = new ArrayList<>();
items.add("foo");
items.add("bar");
Chunk<MyEntity> myEntities = Chunk.of(new MyEntity("id1"), new MyEntity("id2"));
writer.write(myEntities);
when(this.sessionFactory.openSession()).thenReturn(this.session);
writer.write(items);
verify(this.neo4jDriver, new Times(2)).executableQuery("MATCH (MyEntity) WHERE MyEntity.idField = $id DETACH DELETE MyEntity");
}
verify(this.session).save("foo");
verify(this.session).save("bar");
}
private record MyEntity(String idField) {
}
@Test
public void testDeleteItemsWithSession() throws Exception {
writer = new Neo4jItemWriter<>();
private static class TestEntity<T> extends BasicPersistentEntity<T, Neo4jPersistentProperty>
implements Neo4jPersistentEntity<T> {
writer.setSessionFactory(this.sessionFactory);
writer.afterPropertiesSet();
public TestEntity(TypeInformation<T> information) {
super(information);
addPersistentProperty(new Neo4jPersistentProperty() {
@Override
public Neo4jPersistentPropertyConverter<?> getOptionalConverter() {
return null;
}
List<String> items = new ArrayList<>();
items.add("foo");
items.add("bar");
@Override
public boolean isEntityWithRelationshipProperties() {
return false;
}
writer.setDelete(true);
@Override
public PersistentEntity<?, Neo4jPersistentProperty> getOwner() {
return null;
}
when(this.sessionFactory.openSession()).thenReturn(this.session);
writer.write(items);
@Override
public String getName() {
return "idField";
}
verify(this.session).delete("foo");
verify(this.session).delete("bar");
}
@Override
public Class<?> getType() {
return String.class;
}
@Override
public TypeInformation<?> getTypeInformation() {
return TypeInformation.of(String.class);
}
@Override
public Iterable<? extends TypeInformation<?>> getPersistentEntityTypeInformation() {
return null;
}
@Override
public Method getGetter() {
return null;
}
@Override
public Method getSetter() {
return null;
}
@Override
public Method getWither() {
return null;
}
@Override
public Field getField() {
try {
return MyEntity.class.getDeclaredField("idField");
} catch (NoSuchFieldException e) {
throw new RuntimeException(e);
}
}
@Override
public String getSpelExpression() {
return null;
}
@Override
public Association<Neo4jPersistentProperty> 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 extends Annotation> A findAnnotation(Class<A> annotationType) {
return null;
}
@Override
public <A extends Annotation> A findPropertyOrOwnerAnnotation(Class<A> annotationType) {
return null;
}
@Override
public boolean isAnnotationPresent(Class<? extends Annotation> annotationType) {
return false;
}
@Override
public boolean usePropertyAccess() {
return false;
}
@Override
public Class<?> getAssociationTargetType() {
return null;
}
@Override
public TypeInformation<?> getAssociationTargetTypeInformation() {
return null;
}
@Override
public String getFieldName() {
return null;
}
@Override
public String getPropertyName() {
return null;
}
@Override
public boolean isInternalIdProperty() {
return false;
}
@Override
public boolean isRelationship() {
return false;
}
@Override
public boolean isComposite() {
return false;
}
});
}
@Override
public Optional<Neo4jPersistentProperty> 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<String> getAdditionalLabels() {
return null;
}
@Override
public Class<T> getUnderlyingClass() {
return null;
}
@Override
public IdDescription getIdDescription() {
return IdDescription.forAssignedIds(Cypher.name("thing"), "idField");
}
@Override
public Collection<GraphPropertyDescription> getGraphProperties() {
return null;
}
@Override
public Collection<GraphPropertyDescription> getGraphPropertiesInHierarchy() {
return null;
}
@Override
public Optional<GraphPropertyDescription> getGraphProperty(String fieldName) {
return Optional.empty();
}
@Override
public Collection<RelationshipDescription> getRelationships() {
return null;
}
@Override
public Collection<RelationshipDescription> getRelationshipsInHierarchy(Predicate<PropertyFilter.RelaxedPropertyPath> propertyPredicate) {
return null;
}
@Override
public void addChildNodeDescription(NodeDescription<?> child) {
}
@Override
public Collection<NodeDescription<?>> getChildNodeDescriptionsInHierarchy() {
return null;
}
@Override
public void setParentNodeDescription(NodeDescription<?> parent) {
}
@Override
public NodeDescription<?> getParentNodeDescription() {
return null;
}
@Override
public boolean containsPossibleCircles(Predicate<PropertyFilter.RelaxedPropertyPath> includeField) {
return false;
}
@Override
public boolean describesInterface() {
return false;
}
}
}

View File

@@ -16,275 +16,176 @@
package org.springframework.batch.extensions.neo4j.builder;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import org.junit.Rule;
import org.junit.Test;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
import org.neo4j.ogm.session.Session;
import org.neo4j.ogm.session.SessionFactory;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.neo4j.cypherdsl.core.Cypher;
import org.neo4j.cypherdsl.core.Statement;
import org.neo4j.cypherdsl.core.StatementBuilder;
import org.springframework.batch.extensions.neo4j.Neo4jItemReader;
import org.springframework.data.neo4j.core.Neo4jTemplate;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.fail;
import java.util.Arrays;
import java.util.List;
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();
private List<String> result;
private Neo4jTemplate neo4jTemplate;
private StatementBuilder.OngoingReadingAndReturn dummyStatement = Cypher.match(Cypher.anyNode()).returning(Cypher.anyNode());
@Mock
private Iterable<String> result;
@SuppressWarnings("unchecked")
@BeforeEach
void setup() {
result = mock(List.class);
neo4jTemplate = mock(Neo4jTemplate.class);
}
@Mock
private SessionFactory sessionFactory;
@Test
public void testFullyQualifiedItemReader() throws Exception {
dummyStatement = Cypher.match(Cypher.anyNode()).returning(Cypher.anyNode());
Neo4jItemReader<String> itemReader = new Neo4jItemReaderBuilder<String>()
.neo4jTemplate(this.neo4jTemplate)
.targetType(String.class)
.statement(dummyStatement)
.pageSize(50).name("bar")
.build();
@Mock
private Session session;
when(this.neo4jTemplate.findAll(any(Statement.class), any(), eq(String.class)))
.thenReturn(result);
when(result.iterator()).thenReturn(Arrays.asList("foo", "bar", "baz").iterator());
@Test
public void testFullyQualifiedItemReader() throws Exception {
Neo4jItemReader<String> itemReader = new Neo4jItemReaderBuilder<String>()
.sessionFactory(this.sessionFactory)
.targetType(String.class)
.startStatement("n=node(*)")
.orderByStatement("n.age")
.pageSize(50).name("bar")
.matchStatement("n -- m")
.whereStatement("has(n.name)")
.returnStatement("m").build();
assertEquals("foo", itemReader.read());
assertEquals("bar", itemReader.read());
assertEquals("baz", itemReader.read());
}
when(this.sessionFactory.openSession()).thenReturn(this.session);
when(this.session.query(String.class,
"START n=node(*) MATCH n -- m WHERE has(n.name) RETURN m ORDER BY n.age SKIP 0 LIMIT 50", null))
.thenReturn(result);
when(result.iterator()).thenReturn(Arrays.asList("foo", "bar", "baz").iterator());
@Test
public void testCurrentSize() throws Exception {
Neo4jItemReader<String> itemReader = new Neo4jItemReaderBuilder<String>()
.neo4jTemplate(this.neo4jTemplate)
.targetType(String.class)
.statement(dummyStatement)
.pageSize(50).name("bar")
.currentItemCount(0)
.maxItemCount(1)
.build();
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());
}
when(this.neo4jTemplate.findAll(any(Statement.class), any(), eq(String.class)))
.thenReturn(result);
when(result.iterator()).thenReturn(Arrays.asList("foo", "bar", "baz").iterator());
@Test
public void testCurrentSize() throws Exception {
Neo4jItemReader<String> itemReader = new Neo4jItemReaderBuilder<String>()
.sessionFactory(this.sessionFactory)
.targetType(String.class)
.startStatement("n=node(*)")
.orderByStatement("n.age")
.pageSize(50).name("bar")
.returnStatement("m")
.currentItemCount(0)
.maxItemCount(1)
.build();
assertEquals("foo", itemReader.read());
assertNull(itemReader.read());
}
when(this.sessionFactory.openSession()).thenReturn(this.session);
when(this.session.query(String.class, "START n=node(*) RETURN m ORDER BY n.age SKIP 0 LIMIT 50", null))
.thenReturn(result);
when(result.iterator()).thenReturn(Arrays.asList("foo", "bar", "baz").iterator());
assertEquals("The expected value was not returned by reader.", "foo", itemReader.read());
assertNull("The expected value was not should be null.", itemReader.read());
}
@Test
public void testNoSessionFactory() {
try {
new Neo4jItemReaderBuilder<String>()
.targetType(String.class)
.pageSize(50)
.name("bar").build();
@Test
public void testResultsWithMatchAndWhereWithParametersWithSession() throws Exception {
Map<String, Object> params = new HashMap<>();
params.put("foo", "bar");
Neo4jItemReader<String> itemReader = new Neo4jItemReaderBuilder<String>()
.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();
fail("IllegalArgumentException should have been thrown");
} catch (IllegalArgumentException iae) {
assertEquals("neo4jTemplate is required.", iae.getMessage());
}
}
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());
@Test
public void testZeroPageSize() {
validateExceptionMessage(new Neo4jItemReaderBuilder<String>()
.neo4jTemplate(this.neo4jTemplate)
.targetType(String.class)
.statement(dummyStatement)
.pageSize(0)
.name("foo"),
"pageSize must be greater than zero");
}
assertEquals("The expected value was not returned by reader.", "foo", itemReader.read());
}
@Test
public void testZeroMaxItemCount() {
validateExceptionMessage(new Neo4jItemReaderBuilder<String>()
.neo4jTemplate(this.neo4jTemplate)
.targetType(String.class)
.statement(dummyStatement)
.pageSize(5)
.maxItemCount(0)
.name("foo"),
"maxItemCount must be greater than zero");
}
@Test
public void testNoSessionFactory() {
try {
new Neo4jItemReaderBuilder<String>()
.targetType(String.class)
.startStatement("n=node(*)")
.returnStatement("*")
.orderByStatement("n.age")
.pageSize(50)
.name("bar").build();
@Test
public void testCurrentItemCountGreaterThanMaxItemCount() {
validateExceptionMessage(new Neo4jItemReaderBuilder<String>()
.neo4jTemplate(this.neo4jTemplate)
.targetType(String.class)
.statement(dummyStatement)
.pageSize(5)
.maxItemCount(5)
.currentItemCount(6)
.name("foo"),
"maxItemCount must be greater than currentItemCount");
}
fail("IllegalArgumentException should have been thrown");
}
catch (IllegalArgumentException iae) {
assertEquals("IllegalArgumentException message did not match the expected result.",
"sessionFactory is required.", iae.getMessage());
}
}
@Test
public void testNullName() {
validateExceptionMessage(
new Neo4jItemReaderBuilder<String>()
.neo4jTemplate(this.neo4jTemplate)
.targetType(String.class)
.statement(dummyStatement)
.pageSize(50),
"A name is required when saveState is set to true");
@Test
public void testZeroPageSize() {
validateExceptionMessage(new Neo4jItemReaderBuilder<String>()
.sessionFactory(this.sessionFactory)
.targetType(String.class)
.startStatement("n=node(*)")
.returnStatement("*")
.orderByStatement("n.age")
.pageSize(0)
.name("foo")
.matchStatement("n -- m")
.whereStatement("has(n.name)")
.returnStatement("m"),
"pageSize must be greater than zero");
}
// tests that name is not required if saveState is set to false.
new Neo4jItemReaderBuilder<String>()
.neo4jTemplate(this.neo4jTemplate)
.targetType(String.class)
.statement(dummyStatement)
.saveState(false)
.pageSize(50)
.build();
}
@Test
public void testZeroMaxItemCount() {
validateExceptionMessage(new Neo4jItemReaderBuilder<String>()
.sessionFactory(this.sessionFactory)
.targetType(String.class)
.startStatement("n=node(*)")
.returnStatement("*")
.orderByStatement("n.age")
.pageSize(5)
.maxItemCount(0)
.name("foo")
.matchStatement("n -- m")
.whereStatement("has(n.name)")
.returnStatement("m"),
"maxItemCount must be greater than zero");
}
@Test
public void testNullTargetType() {
validateExceptionMessage(
new Neo4jItemReaderBuilder<String>()
.neo4jTemplate(this.neo4jTemplate)
.statement(dummyStatement)
.pageSize(50)
.name("bar"),
"targetType is required.");
}
@Test
public void testCurrentItemCountGreaterThanMaxItemCount() {
validateExceptionMessage(new Neo4jItemReaderBuilder<String>()
.sessionFactory(this.sessionFactory)
.targetType(String.class)
.startStatement("n=node(*)")
.returnStatement("*")
.orderByStatement("n.age")
.pageSize(5)
.maxItemCount(5)
.currentItemCount(6)
.name("foo")
.matchStatement("n -- m")
.whereStatement("has(n.name)")
.returnStatement("m"),
"maxItemCount must be greater than currentItemCount");
}
@Test
public void testNullStatement() {
validateExceptionMessage(
new Neo4jItemReaderBuilder<String>()
.neo4jTemplate(this.neo4jTemplate)
.targetType(String.class)
.pageSize(50).name("bar"),
"statement is required.");
}
@Test
public void testNullName() {
validateExceptionMessage(
new Neo4jItemReaderBuilder<String>()
.sessionFactory(this.sessionFactory)
.targetType(String.class)
.startStatement("n=node(*)")
.returnStatement("*")
.orderByStatement("n.age")
.pageSize(50),
"A name is required when saveState is set to true");
// tests that name is not required if saveState is set to false.
new Neo4jItemReaderBuilder<String>()
.sessionFactory(this.sessionFactory)
.targetType(String.class)
.startStatement("n=node(*)")
.returnStatement("*")
.orderByStatement("n.age")
.saveState(false)
.pageSize(50)
.build();
}
@Test
public void testNullTargetType() {
validateExceptionMessage(
new Neo4jItemReaderBuilder<String>()
.sessionFactory(this.sessionFactory)
.startStatement("n=node(*)")
.returnStatement("*")
.orderByStatement("n.age")
.pageSize(50)
.name("bar")
.matchStatement("n -- m")
.whereStatement("has(n.name)")
.returnStatement("m"),
"targetType is required.");
}
@Test
public void testNullStartStatement() {
validateExceptionMessage(
new Neo4jItemReaderBuilder<String>()
.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<String>()
.sessionFactory(this.sessionFactory)
.targetType(String.class)
.startStatement("n=node(*)")
.orderByStatement("n.age")
.pageSize(50).name("bar")
.matchStatement("n -- m")
.whereStatement("has(n.name)"), "returnStatement is required.");
}
@Test
public void testNullOrderByStatement() {
validateExceptionMessage(
new Neo4jItemReaderBuilder<String>()
.sessionFactory(this.sessionFactory)
.targetType(String.class)
.startStatement("n=node(*)")
.returnStatement("*")
.pageSize(50)
.name("bar")
.matchStatement("n -- m")
.whereStatement("has(n.name)")
.returnStatement("m"),
"orderByStatement is required.");
}
private void validateExceptionMessage(Neo4jItemReaderBuilder<?> builder, String message) {
try {
builder.build();
fail("IllegalArgumentException should have been thrown");
}
catch (IllegalArgumentException iae) {
assertEquals("IllegalArgumentException message did not match the expected result.", message,
iae.getMessage());
}
}
private void validateExceptionMessage(Neo4jItemReaderBuilder<?> builder, String message) {
try {
builder.build();
fail("IllegalArgumentException should have been thrown");
} catch (IllegalArgumentException iae) {
assertEquals(message, iae.getMessage());
}
}
}

View File

@@ -16,80 +16,117 @@
package org.springframework.batch.extensions.neo4j.builder;
import java.util.ArrayList;
import java.util.List;
import org.junit.Rule;
import org.junit.Test;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
import org.neo4j.ogm.session.Session;
import org.neo4j.ogm.session.SessionFactory;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.neo4j.cypherdsl.core.Cypher;
import org.neo4j.cypherdsl.core.Functions;
import org.neo4j.driver.Driver;
import org.neo4j.driver.ExecutableQuery;
import org.springframework.batch.extensions.neo4j.Neo4jItemWriter;
import org.springframework.batch.item.Chunk;
import org.springframework.data.mapping.IdentifierAccessor;
import org.springframework.data.neo4j.core.Neo4jTemplate;
import org.springframework.data.neo4j.core.mapping.IdDescription;
import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext;
import org.springframework.data.neo4j.core.mapping.Neo4jPersistentEntity;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.fail;
import static org.mockito.Mockito.*;
/**
* @author Glenn Renfro
* @author Gerrit Meier
*/
public class Neo4jItemWriterBuilderTests {
@Rule
public MockitoRule rule = MockitoJUnit.rule().silent();
private Neo4jTemplate neo4jTemplate;
@Mock
private SessionFactory sessionFactory;
@Mock
private Session session;
private Driver neo4jDriver;
@Test
public void testBasicWriter() throws Exception{
Neo4jItemWriter<String> writer = new Neo4jItemWriterBuilder<String>()
.sessionFactory(this.sessionFactory)
.build();
List<String> items = new ArrayList<>();
items.add("foo");
items.add("bar");
private Neo4jMappingContext neo4jMappingContext;
when(this.sessionFactory.openSession()).thenReturn(this.session);
writer.write(items);
@BeforeEach
void setup() {
neo4jDriver = mock(Driver.class);
neo4jTemplate = mock(Neo4jTemplate.class);
neo4jMappingContext = mock(Neo4jMappingContext.class);
}
verify(this.session).save("foo");
verify(this.session).save("bar");
verify(this.session, never()).delete("foo");
verify(this.session, never()).delete("bar");
}
@Test
public void testBasicWriter() {
Neo4jItemWriter<String> writer = new Neo4jItemWriterBuilder<String>()
.neo4jTemplate(this.neo4jTemplate)
.neo4jDriver(this.neo4jDriver)
.neo4jMappingContext(this.neo4jMappingContext)
.build();
@Test
public void testBasicDelete() throws Exception{
Neo4jItemWriter<String> writer = new Neo4jItemWriterBuilder<String>().delete(true).sessionFactory(this.sessionFactory).build();
List<String> items = new ArrayList<>();
items.add("foo");
items.add("bar");
Chunk<String> items = Chunk.of("foo", "bar");
writer.write(items);
when(this.sessionFactory.openSession()).thenReturn(this.session);
writer.write(items);
verify(this.neo4jTemplate).saveAll(items.getItems());
verify(this.neo4jDriver, never()).executableQuery(anyString());
}
verify(this.session).delete("foo");
verify(this.session).delete("bar");
verify(this.session, never()).save("foo");
verify(this.session, never()).save("bar");
}
@Test
public void testBasicDelete() {
Neo4jItemWriter<String> writer = new Neo4jItemWriterBuilder<String>()
.delete(true)
.neo4jMappingContext(this.neo4jMappingContext)
.neo4jTemplate(this.neo4jTemplate)
.neo4jDriver(neo4jDriver)
.build();
@Test
public void testNoSessionFactory() {
try {
new Neo4jItemWriterBuilder<String>().build();
fail("SessionFactory was not set but exception was not thrown.");
} catch (IllegalArgumentException iae) {
assertEquals("sessionFactory is required.", iae.getMessage());
}
}
// 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<String> 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<String>().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<String>().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<String>().build();
fail("Neo4jTemplate was not set but exception was not thrown.");
} catch (IllegalArgumentException iae) {
assertEquals("neo4jTemplate is required.", iae.getMessage());
}
}
}