diff --git a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/data/AbstractNeo4jItemReader.java b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/data/AbstractNeo4jItemReader.java index ae0e86a06..595706afc 100644 --- a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/data/AbstractNeo4jItemReader.java +++ b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/data/AbstractNeo4jItemReader.java @@ -147,6 +147,10 @@ public abstract class AbstractNeo4jItemReader extends 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; } diff --git a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/data/AbstractPaginatedDataItemReader.java b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/data/AbstractPaginatedDataItemReader.java index 1b0f374c7..a5fc71116 100644 --- a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/data/AbstractPaginatedDataItemReader.java +++ b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/data/AbstractPaginatedDataItemReader.java @@ -18,6 +18,7 @@ package org.springframework.batch.item.data; import org.springframework.batch.item.ItemReader; import org.springframework.batch.item.ItemStreamReader; import org.springframework.batch.item.support.AbstractItemCountingItemStreamItemReader; +import org.springframework.util.Assert; import java.util.Iterator; @@ -27,6 +28,7 @@ import java.util.Iterator; * semantics required for restartability based on those facilities. * * @author Michael Minella + * @author Glenn Renfro * @since 2.2 * @param Type of item to be read */ @@ -44,9 +46,10 @@ AbstractItemCountingItemStreamItemReader { /** * The number of items to be read with each page. * - * @param pageSize the number of items + * @param pageSize the number of items. pageSize must be greater than zero. */ public void setPageSize(int pageSize) { + Assert.isTrue(pageSize > 0, "pageSize must be greater than zero"); this.pageSize = pageSize; } diff --git a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/data/builder/Neo4jItemReaderBuilder.java b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/data/builder/Neo4jItemReaderBuilder.java new file mode 100644 index 000000000..ea5822d85 --- /dev/null +++ b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/data/builder/Neo4jItemReaderBuilder.java @@ -0,0 +1,213 @@ +/* + * Copyright 2017 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 + * + * http://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.item.data.builder; + +import java.util.Map; + +import org.neo4j.ogm.session.SessionFactory; + +import org.springframework.batch.item.builder.AbstractItemCountingItemStreamItemReaderBuilder; +import org.springframework.batch.item.data.Neo4jItemReader; +import org.springframework.util.Assert; + +/** + * A builder for the {@link Neo4jItemReader}. + * + * @author Glenn Renfro + * @since 4.0 + * @see Neo4jItemReader + */ +public class Neo4jItemReaderBuilder + extends AbstractItemCountingItemStreamItemReaderBuilder> { + + 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; + + /** + * 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-infrastructure/src/main/java/org/springframework/batch/item/support/AbstractItemCountingItemStreamItemReader.java b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/support/AbstractItemCountingItemStreamItemReader.java index d06cec7da..f63d38c1a 100644 --- a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/support/AbstractItemCountingItemStreamItemReader.java +++ b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/support/AbstractItemCountingItemStreamItemReader.java @@ -32,6 +32,7 @@ import org.springframework.util.Assert; * Subclasses are inherently not thread-safe. * * @author Robert Kasanicky + * @author Glenn Renfro */ public abstract class AbstractItemCountingItemStreamItemReader extends AbstractItemStreamItemReader { @@ -119,9 +120,10 @@ public abstract class AbstractItemCountingItemStreamItemReader extends Abstra * * @see #setName(String) * - * @param count the value of the maximum item count + * @param count the value of the maximum item count. count must be greater than zero. */ public void setMaxItemCount(int count) { + Assert.isTrue(count > 0, "count must be greater than zero"); this.maxItemCount = count; } diff --git a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/data/builder/Neo4jItemReaderBuilderTests.java b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/data/builder/Neo4jItemReaderBuilderTests.java new file mode 100644 index 000000000..bc9568c3d --- /dev/null +++ b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/data/builder/Neo4jItemReaderBuilderTests.java @@ -0,0 +1,291 @@ +/* + * Copyright 2017 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 + * + * http://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.item.data.builder; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +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 { + + @Mock + private Iterable result; + + @Mock + private SessionFactory sessionFactory; + + @Mock + private Session session; + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + } + + @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()); + } + } +}