diff --git a/.github/workflows/spring-batch-neo4j.yml b/.github/workflows/spring-batch-neo4j.yml new file mode 100644 index 0000000..531548a --- /dev/null +++ b/.github/workflows/spring-batch-neo4j.yml @@ -0,0 +1,21 @@ +name: Spring Batch Neo4j + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout source code + uses: actions/checkout@v2 + - name: Set up JDK 1.8 + uses: actions/setup-java@v1 + with: + java-version: 1.8 + - name: Build with Maven + run: mvn -B package --file pom.xml + working-directory: spring-batch-neo4j diff --git a/README.md b/README.md index 2a94d46..e1ae833 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,7 @@ This project is part of the [Spring organization][] on GitHub. | spring-batch-excel | Support for [Microsoft Excel] | [@mdeinum](https://github.com/mdeinum) | 0.1.0 | [![Spring Batch Excel](https://github.com/spring-projects/spring-batch-extensions/actions/workflows/spring-batch-excel.yml/badge.svg)](https://github.com/spring-projects/spring-batch-extensions/actions/workflows/spring-batch-excel.yml) | | spring-batch-elasticsearch | Support for [Elasticsearch] | TBA | 0.1.0-SNAPSHOT | [![Spring Batch Elasticsearch](https://github.com/spring-projects/spring-batch-extensions/actions/workflows/spring-batch-elasticsearch.yml/badge.svg)](https://github.com/spring-projects/spring-batch-extensions/actions/workflows/spring-batch-elasticsearch.yml) | | spring-batch-bigquery | Support for [Google BigQuery] | [@dgray16](https://github.com/dgray16) | 0.1.0-SNAPSHOT | [![Spring Batch BigQuery](https://github.com/spring-projects/spring-batch-extensions/actions/workflows/spring-batch-bigquery.yml/badge.svg)](https://github.com/spring-projects/spring-batch-extensions/actions/workflows/spring-batch-bigquery.yml) | +| spring-batch-neo4j | Support for [Neo4j] | [@michael-simons](https://github.com/michael-simons) | 0.1.0-SNAPSHOT | [![Spring Batch Neo4j](https://github.com/spring-projects/spring-batch-extensions/actions/workflows/spring-batch-neo4j.yml/badge.svg)](https://github.com/spring-projects/spring-batch-extensions/actions/workflows/spring-batch-neo4j.yml) | ## Getting support @@ -87,6 +88,8 @@ Follow the Spring Batch team members and contributors on Twitter: * [@michaelminella](https://twitter.com/michaelminella) - Michael Minella * [@b_e_n_a_s](https://twitter.com/b_e_n_a_s) - Mahmoud Ben Hassine * [@mdeinum](https://twitter.com/mdeinum) - Marten Deinum +* [@rotnroll666](https://twitter.com/rotnroll666) - Michael Simons +* [@meistermeier](https://twitter.com/meistermeier) - Gerrit Meier ## License @@ -100,6 +103,7 @@ noted differently for individual extension Modules, but this should be the rare [Microsoft Excel]: https://www.microsoft.com/en-us/microsoft-365/excel [Elasticsearch]: https://www.elastic.co [Google BigQuery]: https://cloud.google.com/bigquery +[Neo4j]: https://neo4j.com [spring-batch tag]: https://stackoverflow.com/questions/tagged/spring-batch [Spring Batch]: https://github.com/spring-projects/spring-batch [Spring Boot]: https://github.com/spring-projects/spring-boot diff --git a/spring-batch-neo4j/README.md b/spring-batch-neo4j/README.md new file mode 100644 index 0000000..ec4addc --- /dev/null +++ b/spring-batch-neo4j/README.md @@ -0,0 +1,31 @@ +# spring-batch-neo4j + +This extension contains an `ItemReader` and `ItemWriter` implementations for [Neo4j](https://neo4j.com). + +# Usage example + +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") + .pageSize(50) + .build(); +``` + +The `Neo4jItemWriter` can be configured as follows: + +```java +SessionFactory sessionFactory = ... +Neo4jItemWriter writer = new Neo4jItemWriterBuilder() + .sessionFactory(sessionFactory) + .build(); +``` \ No newline at end of file diff --git a/spring-batch-neo4j/pom.xml b/spring-batch-neo4j/pom.xml new file mode 100644 index 0000000..dc2433d --- /dev/null +++ b/spring-batch-neo4j/pom.xml @@ -0,0 +1,157 @@ + + + + + 4.0.0 + + org.springframework.batch.extensions + spring-batch-neo4j + 0.1.0-SNAPSHOT + Spring Batch Neo4j + Spring Batch extension for Neo4j + https://github.com/spring-projects/spring-batch-extensions/tree/main/spring-batch-neo4j + + + + Apache 2.0 + https://www.apache.org/licenses/LICENSE-2.0.txt + repo + + + + + https://github.com/spring-projects/spring-batch-extensions + git://github.com/spring-projects/spring-batch-extensions.git + git@github.com:spring-projects/spring-batch-extensions.git + + + + + michael-simons + Michael Simons + https://github.com/michael-simons + + + meistermeier + Gerrit Meier + https://github.com/meistermeier + + + + + UTF-8 + UTF-8 + 1.8 + + + 4.3.3 + 3.2.21 + + + 3.18.1 + 4.13.2 + 3.6.0 + + + 3.8.1 + 3.2.0 + 3.2.1 + + + + + org.springframework.batch + spring-batch-core + ${spring.batch.version} + + + org.springframework.batch + spring-batch-infrastructure + ${spring.batch.version} + + + org.neo4j + neo4j-ogm-core + ${neo4j-ogm-core.version} + + + + + junit + junit + ${junit.version} + test + + + org.mockito + mockito-core + ${mockito.version} + test + + + org.assertj + assertj-core + ${assertj.version} + test + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + ${maven-compiler-plugin.version} + + ${java.version} + ${java.version} + + -Xlint:all,deprecation + + + + + org.apache.maven.plugins + maven-javadoc-plugin + ${maven-javadoc-plugin.version} + + + attach-javadocs + + jar + + + + + + org.apache.maven.plugins + maven-source-plugin + ${maven-source-plugin.version} + + + attach-sources + + jar + + + + + + + + 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 new file mode 100644 index 0000000..179af22 --- /dev/null +++ b/spring-batch-neo4j/src/main/java/org/springframework/batch/extensions/neo4j/Neo4jItemReader.java @@ -0,0 +1,224 @@ +/* + * Copyright 2012-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 + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +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.springframework.batch.item.ItemReader; +import org.springframework.batch.item.data.AbstractPaginatedDataItemReader; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +/** + *

+ * Restartable {@link ItemReader} that reads objects from the graph database Neo4j + * via a paging technique. + *

+ * + *

+ * It executes cypher queries built from the statement fragments 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 + * will begin again at the same number item it left off at. + *

+ * + *

+ * Performance is dependent on your Neo4J configuration (embedded or remote) 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. + *

+ * + *

+ * This implementation is thread-safe between calls to + * {@link #open(org.springframework.batch.item.ExecutionContext)}, however you + * should set saveState=false if used in a multi-threaded + * environment (no restart available). + *

+ * + * @author Michael Minella + * @author Mahmoud Ben Hassine + */ +public class Neo4jItemReader extends AbstractPaginatedDataItemReader implements InitializingBean { + + protected Log logger = LogFactory.getLog(getClass()); + + private SessionFactory sessionFactory; + + private String startStatement; + private String returnStatement; + private String matchStatement; + private String whereStatement; + private String orderByStatement; + + private Class targetType; + + private Map parameterValues; + + /** + * Optional parameters to be used in the cypher query. + * + * @param parameterValues the parameter values to be used in the cypher query + */ + public void setParameterValues(Map parameterValues) { + this.parameterValues = parameterValues; + } + + protected final Map getParameterValues() { + return this.parameterValues; + } + + /** + * The start segment of the cypher query. START is prepended + * to the statement provided and should not be + * included. + * + * @param startStatement the start fragment of the cypher query. + */ + public void setStartStatement(String startStatement) { + this.startStatement = startStatement; + } + + /** + * The return statement of the cypher query. RETURN is prepended + * to the statement provided and should not be + * included + * + * @param returnStatement the return fragment of the cypher query. + */ + public void setReturnStatement(String returnStatement) { + this.returnStatement = returnStatement; + } + + /** + * An optional match fragment of the cypher query. MATCH is + * prepended to the statement provided and should not + * be included. + * + * @param matchStatement the match fragment of the cypher query + */ + public void setMatchStatement(String matchStatement) { + this.matchStatement = matchStatement; + } + + /** + * An optional where fragment of the cypher query. WHERE is + * prepended to the statement provided and should not + * be included. + * + * @param whereStatement where fragment of the cypher query + */ + public void setWhereStatement(String whereStatement) { + this.whereStatement = whereStatement; + } + + /** + * A list of properties to order the results by. This is + * required so that subsequent page requests pull back the + * segment of results correctly. ORDER BY is prepended to + * the statement provided and should not be included. + * + * @param orderByStatement order by fragment of the cypher query. + */ + public void setOrderByStatement(String orderByStatement) { + this.orderByStatement = orderByStatement; + } + + protected SessionFactory getSessionFactory() { + return sessionFactory; + } + + /** + * Establish the session factory for the reader. + * @param sessionFactory the factory to use for the reader. + */ + public void setSessionFactory(SessionFactory sessionFactory) { + this.sessionFactory = sessionFactory; + } + + /** + * The object type to be returned from each call to {@link #read()} + * + * @param targetType the type of object to return. + */ + public void setTargetType(Class targetType) { + this.targetType = targetType; + } + + protected final Class getTargetType() { + return this.targetType; + } + + protected String generateLimitCypherQuery() { + StringBuilder query = new StringBuilder(128); + + query.append("START ").append(startStatement); + query.append(matchStatement != null ? " MATCH " + matchStatement : ""); + query.append(whereStatement != null ? " WHERE " + whereStatement : ""); + query.append(" RETURN ").append(returnStatement); + query.append(" ORDER BY ").append(orderByStatement); + query.append(" SKIP " + (pageSize * page)); + query.append(" LIMIT " + pageSize); + + String resultingQuery = query.toString(); + + if (logger.isDebugEnabled()) { + logger.debug(resultingQuery); + } + + return resultingQuery; + } + + /** + * Checks mandatory properties + * + * @see InitializingBean#afterPropertiesSet() + */ + @Override + public void afterPropertiesSet() throws Exception { + Assert.state(sessionFactory != null,"A SessionFactory is required"); + Assert.state(targetType != null, "The type to be returned is required"); + Assert.state(StringUtils.hasText(startStatement), "A START statement is required"); + Assert.state(StringUtils.hasText(returnStatement), "A RETURN statement is required"); + Assert.state(StringUtils.hasText(orderByStatement), "A ORDER BY statement is required"); + } + + @SuppressWarnings("unchecked") + @Override + protected Iterator doPageRead() { + Session session = getSessionFactory().openSession(); + + Iterable queryResults = session.query(getTargetType(), + generateLimitCypherQuery(), + getParameterValues()); + + if(queryResults != null) { + return queryResults.iterator(); + } + else { + return new ArrayList().iterator(); + } + } +} 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 new file mode 100644 index 0000000..d7e339e --- /dev/null +++ b/spring-batch-neo4j/src/main/java/org/springframework/batch/extensions/neo4j/Neo4jItemWriter.java @@ -0,0 +1,127 @@ +/* + * Copyright 2012-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 + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +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.springframework.batch.item.ItemWriter; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.util.Assert; +import org.springframework.util.CollectionUtils; + +/** + *

+ * A {@link ItemWriter} implementation that writes to a Neo4j database. + *

+ * + *

+ * This writer is thread-safe once all properties are set (normal singleton + * behavior) so it can be used in multiple concurrent transactions. + *

+ * + * @author Michael Minella + * @author Glenn Renfro + * @author Mahmoud Ben Hassine + * + */ +public class Neo4jItemWriter implements ItemWriter, InitializingBean { + + protected static final Log logger = LogFactory + .getLog(Neo4jItemWriter.class); + + private boolean delete = false; + + private SessionFactory sessionFactory; + + /** + * Boolean flag indicating whether the writer should save or delete the item at write + * time. + * @param delete true if write should delete item, false if item should be saved. + * Default is false. + */ + public void setDelete(boolean delete) { + this.delete = delete; + } + + /** + * Establish the session factory that will be used to create {@link Session} instances + * for interacting with Neo4j. + * @param sessionFactory sessionFactory to be used. + */ + public void setSessionFactory(SessionFactory sessionFactory) { + this.sessionFactory = sessionFactory; + } + + /** + * Checks mandatory properties + * + * @see InitializingBean#afterPropertiesSet() + */ + @Override + public void afterPropertiesSet() throws Exception { + Assert.state(this.sessionFactory != null, + "A SessionFactory is required"); + } + + /** + * Write all items to the data store. + * + * @see org.springframework.batch.item.ItemWriter#write(java.util.List) + */ + @Override + public void write(List items) throws Exception { + if(!CollectionUtils.isEmpty(items)) { + doWrite(items); + } + } + + /** + * Performs the actual write using the template. This can be overridden by + * a subclass if necessary. + * + * @param items the list of items to be persisted. + */ + protected void doWrite(List items) { + if(delete) { + delete(items); + } + else { + save(items); + } + } + + private void delete(List items) { + Session session = this.sessionFactory.openSession(); + + for(T item : items) { + session.delete(item); + } + } + + private void save(List items) { + Session session = this.sessionFactory.openSession(); + + for (T item : items) { + session.save(item); + } + } +} 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 new file mode 100644 index 0000000..32df8ac --- /dev/null +++ b/spring-batch-neo4j/src/main/java/org/springframework/batch/extensions/neo4j/builder/Neo4jItemReaderBuilder.java @@ -0,0 +1,273 @@ +/* + * 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 + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.batch.extensions.neo4j.builder; + +import java.util.Map; + +import org.neo4j.ogm.session.SessionFactory; + +import org.springframework.batch.item.data.Neo4jItemReader; +import org.springframework.util.Assert; + +/** + * A builder for the {@link Neo4jItemReader}. + * + * @author Glenn Renfro + * @see Neo4jItemReader + */ +public class Neo4jItemReaderBuilder { + + private SessionFactory sessionFactory; + + private String startStatement; + + private String returnStatement; + + private String matchStatement; + + private String whereStatement; + + private String orderByStatement; + + private Class targetType; + + private Map parameterValues; + + private int pageSize = 10; + + private boolean saveState = true; + + private String name; + + private int maxItemCount = Integer.MAX_VALUE; + + private int currentItemCount; + + /** + * Configure if the state of the {@link org.springframework.batch.item.ItemStreamSupport} + * should be persisted within the {@link org.springframework.batch.item.ExecutionContext} + * for restart purposes. + * + * @param saveState defaults to true + * @return The current instance of the builder. + */ + public Neo4jItemReaderBuilder saveState(boolean saveState) { + this.saveState = saveState; + + return this; + } + + /** + * The name used to calculate the key within the + * {@link org.springframework.batch.item.ExecutionContext}. Required if + * {@link #saveState(boolean)} is set to true. + * + * @param name name of the reader instance + * @return The current instance of the builder. + * @see org.springframework.batch.item.ItemStreamSupport#setName(String) + */ + public Neo4jItemReaderBuilder name(String name) { + this.name = name; + + return this; + } + + /** + * Configure the max number of items to be read. + * + * @param maxItemCount the max items to be read + * @return The current instance of the builder. + * @see org.springframework.batch.item.support.AbstractItemCountingItemStreamItemReader#setMaxItemCount(int) + */ + public Neo4jItemReaderBuilder maxItemCount(int maxItemCount) { + this.maxItemCount = maxItemCount; + + return this; + } + + /** + * Index for the current item. Used on restarts to indicate where to start from. + * + * @param currentItemCount current index + * @return this instance for method chaining + * @see org.springframework.batch.item.support.AbstractItemCountingItemStreamItemReader#setCurrentItemCount(int) + */ + public Neo4jItemReaderBuilder currentItemCount(int currentItemCount) { + this.currentItemCount = currentItemCount; + + return this; + } + + /** + * Establish the session factory for the reader. + * @param sessionFactory the factory to use for the reader. + * @return this instance for method chaining + * @see Neo4jItemReader#setSessionFactory(SessionFactory) + */ + public Neo4jItemReaderBuilder sessionFactory(SessionFactory sessionFactory) { + this.sessionFactory = sessionFactory; + + return this; + } + + /** + * The number of items to be read with each page. + * + * @param pageSize the number of items + * @return this instance for method chaining + * @see Neo4jItemReader#setPageSize(int) + */ + public Neo4jItemReaderBuilder pageSize(int pageSize) { + this.pageSize = pageSize; + + return this; + } + + /** + * Optional parameters to be used in the cypher query. + * + * @param parameterValues the parameter values to be used in the cypher query + * @return this instance for method chaining + * @see Neo4jItemReader#setParameterValues(Map) + */ + public Neo4jItemReaderBuilder parameterValues(Map parameterValues) { + this.parameterValues = parameterValues; + + return this; + } + + /** + * The start segment of the cypher query. START is prepended to the statement provided + * and should not be included. + * + * @param startStatement the start fragment of the cypher query. + * @return this instance for method chaining + * @see Neo4jItemReader#setStartStatement(String) + */ + public Neo4jItemReaderBuilder startStatement(String startStatement) { + this.startStatement = startStatement; + + return this; + } + + /** + * The return statement of the cypher query. RETURN is prepended to the statement + * provided and should not be included + * + * @param returnStatement the return fragment of the cypher query. + * @return this instance for method chaining + * @see Neo4jItemReader#setReturnStatement(String) + */ + public Neo4jItemReaderBuilder returnStatement(String returnStatement) { + this.returnStatement = returnStatement; + + return this; + } + + /** + * An optional match fragment of the cypher query. MATCH is prepended to the statement + * provided and should not be included. + * + * @param matchStatement the match fragment of the cypher query + * @return this instance for method chaining + * @see Neo4jItemReader#setMatchStatement(String) + */ + public Neo4jItemReaderBuilder matchStatement(String matchStatement) { + this.matchStatement = matchStatement; + + return this; + } + + /** + * An optional where fragment of the cypher query. WHERE is prepended to the statement + * provided and should not be included. + * + * @param whereStatement where fragment of the cypher query + * @return this instance for method chaining + * @see Neo4jItemReader#setWhereStatement(String) + */ + public Neo4jItemReaderBuilder whereStatement(String whereStatement) { + this.whereStatement = whereStatement; + + return this; + } + + /** + * A list of properties to order the results by. This is required so that subsequent + * page requests pull back the segment of results correctly. ORDER BY is prepended to + * the statement provided and should not be included. + * + * @param orderByStatement order by fragment of the cypher query. + * @return this instance for method chaining + * @see Neo4jItemReader#setOrderByStatement(String) + */ + public Neo4jItemReaderBuilder orderByStatement(String orderByStatement) { + this.orderByStatement = orderByStatement; + + return this; + } + + /** + * The object type to be returned from each call to {@link Neo4jItemReader#read()} + * + * @param targetType the type of object to return. + * @return this instance for method chaining + * @see Neo4jItemReader#setTargetType(Class) + */ + public Neo4jItemReaderBuilder targetType(Class targetType) { + this.targetType = targetType; + + return this; + } + + /** + * Returns a fully constructed {@link Neo4jItemReader}. + * + * @return a new {@link Neo4jItemReader} + */ + public Neo4jItemReader build() { + if (this.saveState) { + Assert.hasText(this.name, "A name is required when saveState is set to true"); + } + Assert.notNull(this.sessionFactory, "sessionFactory is required."); + Assert.notNull(this.targetType, "targetType is required."); + Assert.hasText(this.startStatement, "startStatement is required."); + Assert.hasText(this.returnStatement, "returnStatement is required."); + Assert.hasText(this.orderByStatement, "orderByStatement is required."); + Assert.isTrue(this.pageSize > 0, "pageSize must be greater than zero"); + Assert.isTrue(this.maxItemCount > 0, "maxItemCount must be greater than zero"); + Assert.isTrue(this.maxItemCount > this.currentItemCount , "maxItemCount must be greater than currentItemCount"); + + Neo4jItemReader reader = new Neo4jItemReader<>(); + reader.setMatchStatement(this.matchStatement); + reader.setOrderByStatement(this.orderByStatement); + reader.setPageSize(this.pageSize); + reader.setParameterValues(this.parameterValues); + reader.setSessionFactory(this.sessionFactory); + reader.setTargetType(this.targetType); + reader.setStartStatement(this.startStatement); + reader.setReturnStatement(this.returnStatement); + reader.setWhereStatement(this.whereStatement); + reader.setName(this.name); + reader.setSaveState(this.saveState); + reader.setCurrentItemCount(this.currentItemCount); + reader.setMaxItemCount(this.maxItemCount); + + return reader; + } + +} 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 new file mode 100644 index 0000000..f90f515 --- /dev/null +++ b/spring-batch-neo4j/src/main/java/org/springframework/batch/extensions/neo4j/builder/Neo4jItemWriterBuilder.java @@ -0,0 +1,76 @@ +/* + * 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 + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.batch.extensions.neo4j.builder; + +import org.neo4j.ogm.session.Session; +import org.neo4j.ogm.session.SessionFactory; + +import org.springframework.batch.item.data.Neo4jItemWriter; +import org.springframework.util.Assert; + +/** + * A builder implementation for the {@link Neo4jItemWriter} + * + * @author Glenn Renfro + * @see Neo4jItemWriter + */ +public class Neo4jItemWriterBuilder { + + private boolean delete = false; + + private SessionFactory sessionFactory; + + /** + * Boolean flag indicating whether the writer should save or delete the item at write + * time. + * @param delete true if write should delete item, false if item should be saved. + * Default is false. + * @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 Session} instances + * for interacting with Neo4j. + * @param sessionFactory sessionFactory to be used. + * @return The current instance of the builder + * @see Neo4jItemWriter#setSessionFactory(SessionFactory) + */ + public Neo4jItemWriterBuilder sessionFactory(SessionFactory sessionFactory) { + this.sessionFactory = sessionFactory; + + return this; + } + + /** + * Validates and builds a {@link org.springframework.batch.item.data.Neo4jItemWriter}. + * + * @return a {@link Neo4jItemWriter} + */ + public Neo4jItemWriter build() { + Assert.notNull(sessionFactory, "sessionFactory is required."); + Neo4jItemWriter writer = new Neo4jItemWriter<>(); + writer.setDelete(this.delete); + writer.setSessionFactory(this.sessionFactory); + return writer; + } +} 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 new file mode 100644 index 0000000..825ac8d --- /dev/null +++ b/spring-batch-neo4j/src/test/java/org/springframework/batch/extensions/neo4j/Neo4jItemReaderTests.java @@ -0,0 +1,202 @@ +/* + * Copyright 2013-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 + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +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.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.mockito.Mockito.when; + +public class Neo4jItemReaderTests { + + @Rule + public MockitoRule rule = MockitoJUnit.rule().silent(); + + @Mock + private Iterable result; + @Mock + private SessionFactory sessionFactory; + @Mock + private Session session; + + private Neo4jItemReader buildSessionBasedReader() throws Exception { + Neo4jItemReader reader = new Neo4jItemReader<>(); + + reader.setSessionFactory(this.sessionFactory); + reader.setTargetType(String.class); + reader.setStartStatement("n=node(*)"); + reader.setReturnStatement("*"); + reader.setOrderByStatement("n.age"); + reader.setPageSize(50); + reader.afterPropertiesSet(); + + return reader; + } + + @Test + public void testAfterPropertiesSet() throws Exception { + + Neo4jItemReader reader = new Neo4jItemReader<>(); + + try { + reader.afterPropertiesSet(); + fail("SessionFactory was not set but exception was not thrown."); + } catch (IllegalStateException iae) { + assertEquals("A SessionFactory is required", iae.getMessage()); + } catch (Throwable t) { + fail("Wrong exception was thrown:" + t); + } + + reader.setSessionFactory(this.sessionFactory); + + try { + reader.afterPropertiesSet(); + fail("Target Type was not set but exception was not thrown."); + } catch (IllegalStateException iae) { + assertEquals("The type to be returned is required", iae.getMessage()); + } catch (Throwable t) { + fail("Wrong exception was thrown:" + t); + } + + reader.setTargetType(String.class); + + try { + reader.afterPropertiesSet(); + fail("START was not set but exception was not thrown."); + } catch (IllegalStateException iae) { + assertEquals("A START statement is required", iae.getMessage()); + } catch (Throwable t) { + fail("Wrong exception was thrown:" + t); + } + + reader.setStartStatement("n=node(*)"); + + try { + reader.afterPropertiesSet(); + fail("RETURN was not set but exception was not thrown."); + } catch (IllegalStateException iae) { + assertEquals("A RETURN statement is required", iae.getMessage()); + } catch (Throwable t) { + fail("Wrong exception was thrown:" + t); + } + + reader.setReturnStatement("n.name, n.phone"); + + try { + reader.afterPropertiesSet(); + fail("ORDER BY was not set but exception was not thrown."); + } catch (IllegalStateException iae) { + assertEquals("A ORDER BY statement is required", iae.getMessage()); + } catch (Throwable t) { + fail("Wrong exception was thrown:" + t); + } + + reader.setOrderByStatement("n.age"); + + reader.afterPropertiesSet(); + + reader = new Neo4jItemReader<>(); + reader.setSessionFactory(this.sessionFactory); + reader.setTargetType(String.class); + reader.setStartStatement("n=node(*)"); + reader.setReturnStatement("n.name, n.phone"); + reader.setOrderByStatement("n.age"); + + reader.afterPropertiesSet(); + } + + @SuppressWarnings("unchecked") + @Test + public void testNullResultsWithSession() throws Exception { + + Neo4jItemReader itemReader = buildSessionBasedReader(); + + ArgumentCaptor query = ArgumentCaptor.forClass(String.class); + + when(this.sessionFactory.openSession()).thenReturn(this.session); + when(this.session.query(eq(String.class), query.capture(), isNull())).thenReturn(null); + + assertFalse(itemReader.doPageRead().hasNext()); + assertEquals("START n=node(*) RETURN * ORDER BY n.age SKIP 0 LIMIT 50", query.getValue()); + } + + @SuppressWarnings("unchecked") + @Test + public void testNoResultsWithSession() throws Exception { + Neo4jItemReader itemReader = buildSessionBasedReader(); + ArgumentCaptor query = ArgumentCaptor.forClass(String.class); + + when(this.sessionFactory.openSession()).thenReturn(this.session); + when(this.session.query(eq(String.class), query.capture(), isNull())).thenReturn(result); + when(result.iterator()).thenReturn(Collections.emptyIterator()); + + assertFalse(itemReader.doPageRead().hasNext()); + assertEquals("START n=node(*) RETURN * ORDER BY n.age SKIP 0 LIMIT 50", query.getValue()); + } + + @SuppressWarnings("serial") + @Test + public void testResultsWithMatchAndWhereWithSession() throws Exception { + Neo4jItemReader itemReader = buildSessionBasedReader(); + itemReader.setMatchStatement("n -- m"); + itemReader.setWhereStatement("has(n.name)"); + itemReader.setReturnStatement("m"); + itemReader.afterPropertiesSet(); + + when(this.sessionFactory.openSession()).thenReturn(this.session); + when(this.session.query(String.class, "START n=node(*) MATCH n -- m WHERE has(n.name) RETURN m ORDER BY n.age SKIP 0 LIMIT 50", null)).thenReturn(result); + when(result.iterator()).thenReturn(Arrays.asList("foo", "bar", "baz").iterator()); + + assertTrue(itemReader.doPageRead().hasNext()); + } + + @SuppressWarnings("serial") + @Test + public void testResultsWithMatchAndWhereWithParametersWithSession() throws Exception { + Neo4jItemReader itemReader = buildSessionBasedReader(); + Map params = new HashMap<>(); + params.put("foo", "bar"); + itemReader.setParameterValues(params); + itemReader.setMatchStatement("n -- m"); + itemReader.setWhereStatement("has(n.name)"); + itemReader.setReturnStatement("m"); + itemReader.afterPropertiesSet(); + + when(this.sessionFactory.openSession()).thenReturn(this.session); + when(this.session.query(String.class, "START n=node(*) MATCH n -- m WHERE has(n.name) RETURN m ORDER BY n.age SKIP 0 LIMIT 50", params)).thenReturn(result); + when(result.iterator()).thenReturn(Arrays.asList("foo", "bar", "baz").iterator()); + + assertTrue(itemReader.doPageRead().hasNext()); + } +} 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 new file mode 100644 index 0000000..b4eb651 --- /dev/null +++ b/spring-batch-neo4j/src/test/java/org/springframework/batch/extensions/neo4j/Neo4jItemWriterTests.java @@ -0,0 +1,149 @@ +/* + * Copyright 2013-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 + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +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; + +public class Neo4jItemWriterTests { + + @Rule + public MockitoRule rule = MockitoJUnit.rule().silent(); + + private Neo4jItemWriter writer; + + @Mock + private SessionFactory sessionFactory; + @Mock + private Session session; + + @Test + public void testAfterPropertiesSet() throws Exception{ + + writer = new Neo4jItemWriter<>(); + + try { + writer.afterPropertiesSet(); + fail("SessionFactory was not set but exception was not thrown."); + } catch (IllegalStateException iae) { + assertEquals("A SessionFactory is required", iae.getMessage()); + } catch (Throwable t) { + fail("Wrong exception was thrown."); + } + + writer.setSessionFactory(this.sessionFactory); + + writer.afterPropertiesSet(); + + writer = new Neo4jItemWriter<>(); + + writer.setSessionFactory(this.sessionFactory); + + writer.afterPropertiesSet(); + } + + @Test + public void testWriteNullSession() throws Exception { + + writer = new Neo4jItemWriter<>(); + + writer.setSessionFactory(this.sessionFactory); + writer.afterPropertiesSet(); + + writer.write(null); + + verifyNoInteractions(this.session); + } + + @Test + public void testWriteNullWithSession() throws Exception { + writer = new Neo4jItemWriter<>(); + + writer.setSessionFactory(this.sessionFactory); + writer.afterPropertiesSet(); + + when(this.sessionFactory.openSession()).thenReturn(this.session); + writer.write(null); + + verifyNoInteractions(this.session); + } + + @Test + public void testWriteNoItemsWithSession() throws Exception { + writer = new Neo4jItemWriter<>(); + + writer.setSessionFactory(this.sessionFactory); + writer.afterPropertiesSet(); + + when(this.sessionFactory.openSession()).thenReturn(this.session); + writer.write(new ArrayList<>()); + + verifyNoInteractions(this.session); + } + + @Test + public void testWriteItemsWithSession() throws Exception { + writer = new Neo4jItemWriter<>(); + + writer.setSessionFactory(this.sessionFactory); + writer.afterPropertiesSet(); + + List items = new ArrayList<>(); + items.add("foo"); + items.add("bar"); + + when(this.sessionFactory.openSession()).thenReturn(this.session); + writer.write(items); + + verify(this.session).save("foo"); + verify(this.session).save("bar"); + } + + @Test + public void testDeleteItemsWithSession() throws Exception { + writer = new Neo4jItemWriter<>(); + + writer.setSessionFactory(this.sessionFactory); + writer.afterPropertiesSet(); + + List items = new ArrayList<>(); + items.add("foo"); + items.add("bar"); + + writer.setDelete(true); + + when(this.sessionFactory.openSession()).thenReturn(this.session); + writer.write(items); + + verify(this.session).delete("foo"); + verify(this.session).delete("bar"); + } +} 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 new file mode 100644 index 0000000..9cbb28f --- /dev/null +++ b/spring-batch-neo4j/src/test/java/org/springframework/batch/extensions/neo4j/builder/Neo4jItemReaderBuilderTests.java @@ -0,0 +1,290 @@ +/* + * 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 + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +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.springframework.batch.item.data.Neo4jItemReader; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.fail; +import static org.mockito.Mockito.when; + +/** + * @author Glenn Renfro + */ +public class Neo4jItemReaderBuilderTests { + + @Rule + public MockitoRule rule = MockitoJUnit.rule().silent(); + + @Mock + private Iterable result; + + @Mock + private SessionFactory sessionFactory; + + @Mock + private Session session; + + @Test + public void testFullyQualifiedItemReader() throws Exception { + Neo4jItemReader itemReader = new Neo4jItemReaderBuilder() + .sessionFactory(this.sessionFactory) + .targetType(String.class) + .startStatement("n=node(*)") + .orderByStatement("n.age") + .pageSize(50).name("bar") + .matchStatement("n -- m") + .whereStatement("has(n.name)") + .returnStatement("m").build(); + + when(this.sessionFactory.openSession()).thenReturn(this.session); + when(this.session.query(String.class, + "START n=node(*) MATCH n -- m WHERE has(n.name) RETURN m ORDER BY n.age SKIP 0 LIMIT 50", null)) + .thenReturn(result); + when(result.iterator()).thenReturn(Arrays.asList("foo", "bar", "baz").iterator()); + + assertEquals("The expected value was not returned by reader.", "foo", itemReader.read()); + assertEquals("The expected value was not returned by reader.", "bar", itemReader.read()); + assertEquals("The expected value was not returned by reader.", "baz", itemReader.read()); + } + + @Test + public void testCurrentSize() throws Exception { + Neo4jItemReader itemReader = new Neo4jItemReaderBuilder() + .sessionFactory(this.sessionFactory) + .targetType(String.class) + .startStatement("n=node(*)") + .orderByStatement("n.age") + .pageSize(50).name("bar") + .returnStatement("m") + .currentItemCount(0) + .maxItemCount(1) + .build(); + + when(this.sessionFactory.openSession()).thenReturn(this.session); + when(this.session.query(String.class, "START n=node(*) RETURN m ORDER BY n.age SKIP 0 LIMIT 50", null)) + .thenReturn(result); + when(result.iterator()).thenReturn(Arrays.asList("foo", "bar", "baz").iterator()); + + assertEquals("The expected value was not returned by reader.", "foo", itemReader.read()); + assertNull("The expected value was not should be null.", itemReader.read()); + } + + @Test + public void testResultsWithMatchAndWhereWithParametersWithSession() throws Exception { + Map params = new HashMap<>(); + params.put("foo", "bar"); + Neo4jItemReader itemReader = new Neo4jItemReaderBuilder() + .sessionFactory(this.sessionFactory) + .targetType(String.class) + .startStatement("n=node(*)") + .returnStatement("*") + .orderByStatement("n.age") + .pageSize(50) + .name("foo") + .parameterValues(params) + .matchStatement("n -- m") + .whereStatement("has(n.name)") + .returnStatement("m") + .build(); + + when(this.sessionFactory.openSession()).thenReturn(this.session); + when(this.session.query(String.class, + "START n=node(*) MATCH n -- m WHERE has(n.name) RETURN m ORDER BY n.age SKIP 0 LIMIT 50", params)) + .thenReturn(result); + when(result.iterator()).thenReturn(Arrays.asList("foo", "bar", "baz").iterator()); + + assertEquals("The expected value was not returned by reader.", "foo", itemReader.read()); + } + + @Test + public void testNoSessionFactory() { + try { + new Neo4jItemReaderBuilder() + .targetType(String.class) + .startStatement("n=node(*)") + .returnStatement("*") + .orderByStatement("n.age") + .pageSize(50) + .name("bar").build(); + + fail("IllegalArgumentException should have been thrown"); + } + catch (IllegalArgumentException iae) { + assertEquals("IllegalArgumentException message did not match the expected result.", + "sessionFactory is required.", iae.getMessage()); + } + } + + @Test + public void testZeroPageSize() { + validateExceptionMessage(new Neo4jItemReaderBuilder() + .sessionFactory(this.sessionFactory) + .targetType(String.class) + .startStatement("n=node(*)") + .returnStatement("*") + .orderByStatement("n.age") + .pageSize(0) + .name("foo") + .matchStatement("n -- m") + .whereStatement("has(n.name)") + .returnStatement("m"), + "pageSize must be greater than zero"); + } + + @Test + public void testZeroMaxItemCount() { + validateExceptionMessage(new Neo4jItemReaderBuilder() + .sessionFactory(this.sessionFactory) + .targetType(String.class) + .startStatement("n=node(*)") + .returnStatement("*") + .orderByStatement("n.age") + .pageSize(5) + .maxItemCount(0) + .name("foo") + .matchStatement("n -- m") + .whereStatement("has(n.name)") + .returnStatement("m"), + "maxItemCount must be greater than zero"); + } + + @Test + public void testCurrentItemCountGreaterThanMaxItemCount() { + validateExceptionMessage(new Neo4jItemReaderBuilder() + .sessionFactory(this.sessionFactory) + .targetType(String.class) + .startStatement("n=node(*)") + .returnStatement("*") + .orderByStatement("n.age") + .pageSize(5) + .maxItemCount(5) + .currentItemCount(6) + .name("foo") + .matchStatement("n -- m") + .whereStatement("has(n.name)") + .returnStatement("m"), + "maxItemCount must be greater than currentItemCount"); + } + + @Test + public void testNullName() { + validateExceptionMessage( + new Neo4jItemReaderBuilder() + .sessionFactory(this.sessionFactory) + .targetType(String.class) + .startStatement("n=node(*)") + .returnStatement("*") + .orderByStatement("n.age") + .pageSize(50), + "A name is required when saveState is set to true"); + + // tests that name is not required if saveState is set to false. + new Neo4jItemReaderBuilder() + .sessionFactory(this.sessionFactory) + .targetType(String.class) + .startStatement("n=node(*)") + .returnStatement("*") + .orderByStatement("n.age") + .saveState(false) + .pageSize(50) + .build(); + } + + @Test + public void testNullTargetType() { + validateExceptionMessage( + new Neo4jItemReaderBuilder() + .sessionFactory(this.sessionFactory) + .startStatement("n=node(*)") + .returnStatement("*") + .orderByStatement("n.age") + .pageSize(50) + .name("bar") + .matchStatement("n -- m") + .whereStatement("has(n.name)") + .returnStatement("m"), + "targetType is required."); + } + + @Test + public void testNullStartStatement() { + validateExceptionMessage( + new Neo4jItemReaderBuilder() + .sessionFactory(this.sessionFactory) + .targetType(String.class) + .returnStatement("*") + .orderByStatement("n.age") + .pageSize(50).name("bar") + .matchStatement("n -- m") + .whereStatement("has(n.name)") + .returnStatement("m"), + "startStatement is required."); + } + + @Test + public void testNullReturnStatement() { + validateExceptionMessage(new Neo4jItemReaderBuilder() + .sessionFactory(this.sessionFactory) + .targetType(String.class) + .startStatement("n=node(*)") + .orderByStatement("n.age") + .pageSize(50).name("bar") + .matchStatement("n -- m") + .whereStatement("has(n.name)"), "returnStatement is required."); + } + + @Test + public void testNullOrderByStatement() { + validateExceptionMessage( + new Neo4jItemReaderBuilder() + .sessionFactory(this.sessionFactory) + .targetType(String.class) + .startStatement("n=node(*)") + .returnStatement("*") + .pageSize(50) + .name("bar") + .matchStatement("n -- m") + .whereStatement("has(n.name)") + .returnStatement("m"), + "orderByStatement is required."); + } + + private void validateExceptionMessage(Neo4jItemReaderBuilder builder, String message) { + try { + builder.build(); + fail("IllegalArgumentException should have been thrown"); + } + catch (IllegalArgumentException iae) { + assertEquals("IllegalArgumentException message did not match the expected result.", message, + iae.getMessage()); + } + } +} 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 new file mode 100644 index 0000000..15f9d40 --- /dev/null +++ b/spring-batch-neo4j/src/test/java/org/springframework/batch/extensions/neo4j/builder/Neo4jItemWriterBuilderTests.java @@ -0,0 +1,95 @@ +/* + * 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 + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +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.springframework.batch.item.data.Neo4jItemWriter; + +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; + +/** + * @author Glenn Renfro + */ +public class Neo4jItemWriterBuilderTests { + + @Rule + public MockitoRule rule = MockitoJUnit.rule().silent(); + + @Mock + private SessionFactory sessionFactory; + @Mock + private Session session; + + @Test + public void testBasicWriter() throws Exception{ + Neo4jItemWriter writer = new Neo4jItemWriterBuilder() + .sessionFactory(this.sessionFactory) + .build(); + List items = new ArrayList<>(); + items.add("foo"); + items.add("bar"); + + when(this.sessionFactory.openSession()).thenReturn(this.session); + writer.write(items); + + verify(this.session).save("foo"); + verify(this.session).save("bar"); + verify(this.session, never()).delete("foo"); + verify(this.session, never()).delete("bar"); + } + + @Test + public void testBasicDelete() throws Exception{ + Neo4jItemWriter writer = new Neo4jItemWriterBuilder().delete(true).sessionFactory(this.sessionFactory).build(); + List items = new ArrayList<>(); + items.add("foo"); + items.add("bar"); + + when(this.sessionFactory.openSession()).thenReturn(this.session); + writer.write(items); + + verify(this.session).delete("foo"); + verify(this.session).delete("bar"); + verify(this.session, never()).save("foo"); + verify(this.session, never()).save("bar"); + } + + @Test + public void testNoSessionFactory() { + try { + new Neo4jItemWriterBuilder().build(); + fail("SessionFactory was not set but exception was not thrown."); + } catch (IllegalArgumentException iae) { + assertEquals("sessionFactory is required.", iae.getMessage()); + } + } + +}