diff --git a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/data/builder/MongoItemReaderBuilder.java b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/data/builder/MongoItemReaderBuilder.java new file mode 100644 index 000000000..68ff79fed --- /dev/null +++ b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/data/builder/MongoItemReaderBuilder.java @@ -0,0 +1,211 @@ +/* + * 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.List; +import java.util.Map; + +import org.springframework.batch.item.builder.AbstractItemCountingItemStreamItemReaderBuilder; +import org.springframework.batch.item.data.MongoItemReader; +import org.springframework.data.domain.Sort; +import org.springframework.data.mongodb.core.MongoOperations; +import org.springframework.util.Assert; + +/** + * A builder implementation for the {@link MongoItemReader} + * + * @author Glenn Renfro + * @since 4.0 + * @see MongoItemReader + */ +public class MongoItemReaderBuilder + extends AbstractItemCountingItemStreamItemReaderBuilder> { + private MongoOperations template; + + private String query; + + private Class targetType; + + Map sorts; + + private String hint; + + private String fields; + + private String collection; + + private List parameterValues; + + protected int pageSize = 10; + + /** + * Used to perform operations against the MongoDB instance. Also handles the mapping + * of documents to objects. + * + * @param template the MongoOperations instance to use + * @see MongoOperations + * @return The current instance of the builder + * @see MongoItemReader#setTemplate(MongoOperations) + */ + public MongoItemReaderBuilder template(MongoOperations template) { + this.template = template; + + return this; + } + + /** + * A JSON formatted MongoDB query. Parameterization of the provided query is allowed + * via ?<index> placeholders where the <index> indicates the index of the + * parameterValue to substitute. + * + * @param query JSON formatted Mongo query + * @return The current instance of the builder + * @see MongoItemReader#setQuery(String) + */ + public MongoItemReaderBuilder query(String query) { + this.query = query; + + return this; + } + + /** + * The type of object to be returned for each {@link MongoItemReader#read()} call. + * + * @param targetType the type of object to return + * @return The current instance of the builder + * @see MongoItemReader#setTargetType(Class) + */ + public MongoItemReaderBuilder targetType(Class targetType) { + this.targetType = targetType; + + return this; + } + + /** + * {@link List} of values to be substituted in for each of the parameters in the + * query. + * + * @param parameterValues values + * @return The current instance of the builder + * @see MongoItemReader#setParameterValues(List) + */ + public MongoItemReaderBuilder parameterValues(List parameterValues) { + this.parameterValues = parameterValues; + + return this; + } + + /** + * JSON defining the fields to be returned from the matching documents by MongoDB. + * + * @param fields JSON string that identifies the fields to sort by. + * @return The current instance of the builder + * @see MongoItemReader#setFields(String) + */ + public MongoItemReaderBuilder fields(String fields) { + this.fields = fields; + + return this; + } + + /** + * {@link Map} of property + * names/{@link org.springframework.data.domain.Sort.Direction} values to sort the + * input by. + * + * @param sorts map of properties and direction to sort each. + * @return The current instance of the builder + * @see MongoItemReader#setSort(Map) + */ + public MongoItemReaderBuilder sorts(Map sorts) { + this.sorts = sorts; + + return this; + } + + /** + * Establish an optional collection that can be queried. + * + * @param collection Mongo collection to be queried. + * @return The current instance of the builder + * @see MongoItemReader#setCollection(String) + */ + public MongoItemReaderBuilder collection(String collection) { + this.collection = collection; + + return this; + } + + /** + * JSON String telling MongoDB what index to use. + * + * @param hint string indicating what index to use. + * @return The current instance of the builder + * @see MongoItemReader#setHint(String) + */ + public MongoItemReaderBuilder hint(String hint) { + this.hint = hint; + + 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 MongoItemReader#setPageSize(int) + */ + public MongoItemReaderBuilder pageSize(int pageSize) { + this.pageSize = pageSize; + + return this; + } + + /** + * Validates and builds a {@link MongoItemReader}. + * + * @return a {@link MongoItemReader} + */ + public MongoItemReader build() { + Assert.notNull(this.template, "template is required."); + if (this.saveState) { + Assert.hasText(this.name, "A name is required when saveState is set to true"); + } + Assert.notNull(this.targetType, "targetType is required."); + Assert.notNull(this.query, "query is required."); + Assert.notNull(this.sorts, "sorts map is required."); + + MongoItemReader reader = new MongoItemReader<>(); + reader.setTemplate(this.template); + reader.setTargetType(this.targetType); + reader.setQuery(this.query); + reader.setSort(this.sorts); + reader.setHint(this.hint); + reader.setFields(this.fields); + reader.setCollection(this.collection); + reader.setParameterValues(this.parameterValues); + + reader.setPageSize(this.pageSize); + 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/test/java/org/springframework/batch/item/data/builder/MongoItemReaderBuilderTests.java b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/data/builder/MongoItemReaderBuilderTests.java new file mode 100644 index 000000000..89372b248 --- /dev/null +++ b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/data/builder/MongoItemReaderBuilderTests.java @@ -0,0 +1,166 @@ +/* + * 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.ArrayList; +import java.util.HashMap; +import java.util.Map; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import org.springframework.batch.item.data.MongoItemReader; +import org.springframework.data.domain.Sort; +import org.springframework.data.mongodb.core.MongoOperations; +import org.springframework.data.mongodb.core.query.Query; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.fail; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.when; + +/** + * @author Glenn Renfro + */ +public class MongoItemReaderBuilderTests { + @Mock + private MongoOperations template; + + private Map sortOptions; + + private ArgumentCaptor queryContainer; + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + this.sortOptions = new HashMap<>(); + this.sortOptions.put("name", Sort.Direction.DESC); + this.queryContainer = ArgumentCaptor.forClass(Query.class); + } + + @Test + public void testBasic() throws Exception { + MongoItemReader reader = getBasicBuilder().build(); + + when(template.find(this.queryContainer.capture(), eq(String.class))).thenReturn(new ArrayList<>()); + + assertNull("reader should not return result", reader.read()); + + Query query = this.queryContainer.getValue(); + assertEquals(50, query.getLimit()); + assertEquals(0, query.getSkip()); + assertEquals("{ }", query.getQueryObject().toJson()); + assertEquals("{ \"name\" : -1 }", query.getSortObject().toJson()); + } + + @Test + public void testFields() throws Exception { + MongoItemReader reader = getBasicBuilder().fields("{name : 1, age : 1, _id: 0}").build(); + + when(this.template.find(this.queryContainer.capture(), eq(String.class))).thenReturn(new ArrayList<>()); + + assertNull("reader should not return result", reader.read()); + + Query query = this.queryContainer.getValue(); + assertEquals(1, query.getFieldsObject().get("name")); + assertEquals(1, query.getFieldsObject().get("age")); + assertEquals(0, query.getFieldsObject().get("_id")); + } + + @Test + public void testHint() throws Exception { + MongoItemReader reader = getBasicBuilder().hint("{ $natural : 1}").build(); + + when(this.template.find(this.queryContainer.capture(), eq(String.class))).thenReturn(new ArrayList<>()); + + assertNull("reader should not return result", reader.read()); + + Query query = this.queryContainer.getValue(); + assertEquals("{ $natural : 1}", query.getHint()); + } + + @Test + public void testCollection() throws Exception { + MongoItemReader reader = getBasicBuilder().parameterValues(new ArrayList() { + { + add("foo"); + } + }).query("{ name : ?0 }").collection("collection").build(); + + ArgumentCaptor collectionContainer = ArgumentCaptor.forClass(String.class); + + when(this.template.find(this.queryContainer.capture(), eq(String.class), collectionContainer.capture())) + .thenReturn(new ArrayList<>()); + + assertNull("reader should not return result", reader.read()); + + Query query = this.queryContainer.getValue(); + assertEquals("{ \"name\" : \"foo\" }", query.getQueryObject().toJson()); + assertEquals("{ \"name\" : -1 }", query.getSortObject().toJson()); + assertEquals("collection", collectionContainer.getValue()); + } + + @Test + public void testNullTemplate() { + validateExceptionMessage(new MongoItemReaderBuilder().targetType(String.class).query("{ }") + .sorts(this.sortOptions).name("mongoReaderTest").pageSize(50), "template is required."); + } + + @Test + public void testNullTargetType() { + validateExceptionMessage(new MongoItemReaderBuilder().template(this.template).query("{ }") + .sorts(this.sortOptions).name("mongoReaderTest").pageSize(50), "targetType is required."); + } + + @Test + public void testNullQuery() { + validateExceptionMessage(new MongoItemReaderBuilder().template(this.template).targetType(String.class) + .sorts(this.sortOptions).name("mongoReaderTest").pageSize(50), "query is required."); + } + + @Test + public void testNullSorts() { + validateExceptionMessage(new MongoItemReaderBuilder().template(this.template).targetType(String.class) + .query("{ }").name("mongoReaderTest").pageSize(50), "sorts map is required."); + } + + @Test + public void testNullName() { + validateExceptionMessage(new MongoItemReaderBuilder().template(this.template).targetType(String.class) + .query("{ }").sorts(this.sortOptions).pageSize(50), "A name is required when saveState is set to true"); + } + + private void validateExceptionMessage(MongoItemReaderBuilder 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 MongoItemReaderBuilder getBasicBuilder() { + return new MongoItemReaderBuilder().template(this.template).targetType(String.class).query("{ }") + .sorts(this.sortOptions).name("mongoReaderTest").pageSize(50); + } +}