From 4012f8d25178dcef3cfe21d218b80cf52c1ba8d2 Mon Sep 17 00:00:00 2001 From: Michael Minella Date: Mon, 6 Feb 2017 16:06:48 -0600 Subject: [PATCH] Added a builder for the StaxEventItemReader This commit adds a fluent builder for the StaxEventItemReader. Resolves BATCH-2576 --- .../builder/StaxEventItemReaderBuilder.java | 208 +++++++++++++++++ .../xml/StaxEventItemReaderCommonTests.java | 8 +- .../StaxEventItemReaderBuilderTests.java | 212 ++++++++++++++++++ 3 files changed, 422 insertions(+), 6 deletions(-) create mode 100644 spring-batch-infrastructure/src/main/java/org/springframework/batch/item/xml/builder/StaxEventItemReaderBuilder.java create mode 100644 spring-batch-infrastructure/src/test/java/org/springframework/batch/item/xml/builder/StaxEventItemReaderBuilderTests.java diff --git a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/xml/builder/StaxEventItemReaderBuilder.java b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/xml/builder/StaxEventItemReaderBuilder.java new file mode 100644 index 000000000..e795c5897 --- /dev/null +++ b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/xml/builder/StaxEventItemReaderBuilder.java @@ -0,0 +1,208 @@ +/* + * 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.xml.builder; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.springframework.batch.item.xml.StaxEventItemReader; +import org.springframework.core.io.Resource; +import org.springframework.oxm.Unmarshaller; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +/** + * A fluent builder for the {@link StaxEventItemReader} + * + * @author Michael Minella + * @since 4.0 + */ +public class StaxEventItemReaderBuilder { + + private boolean strict = true; + + private Resource resource; + + private Unmarshaller unmarshaller; + + private List fragmentRootElements = new ArrayList<>(); + + private int currentItemCount = 0; + + private int maxItemCount = Integer.MAX_VALUE; + + private boolean saveState = true; + + private String name; + + /** + * The name used to calculate the key within the + * {@link org.springframework.batch.item.ExecutionContext}. Required if + * {@link StaxEventItemReaderBuilder#saveState(boolean)} is set to true. + * + * @param name name of the reader instance + * @return The current instance of the builder. + * @see StaxEventItemReader#setName(String) + */ + public StaxEventItemReaderBuilder name(String name) { + this.name = name; + + return this; + } + + /** + * The {@link Resource} to be used as input. + * + * @param resource the input to the reader. + * @return The current instance of the builder. + * @see StaxEventItemReader#setResource(Resource) + */ + public StaxEventItemReaderBuilder resource(Resource resource) { + this.resource = resource; + + return this; + } + + /** + * An implementation of the {@link Unmarshaller} from Spring's OXM module. + * + * @param unmarshaller component responsible for unmarshalling XML chunks + * @return The current instance of the builder. + * @see StaxEventItemReader#setUnmarshaller + */ + public StaxEventItemReaderBuilder unmarshaller(Unmarshaller unmarshaller) { + this.unmarshaller = unmarshaller; + + return this; + } + + /** + * Adds the list of fragments to be used as the root of each chunk to the configuration. + * + * @param fragmentRootElements the XML root elements to be used to identify XML chunks. + * @return The current instance of the builder. + * @see StaxEventItemReader#setFragmentRootElementNames(String[]) + */ + public StaxEventItemReaderBuilder addFragmentRootElements(String... fragmentRootElements) { + this.fragmentRootElements.addAll(Arrays.asList(fragmentRootElements)); + + return this; + } + + /** + * Adds the list of fragments to be used as the root of each chunk to the configuration. + * + * @param fragmentRootElements the XML root elements to be used to identify XML chunks. + * @return The current instance of the builder. + * @see StaxEventItemReader#setFragmentRootElementNames(String[]) + */ + public StaxEventItemReaderBuilder addFragmentRootElements(List fragmentRootElements) { + this.fragmentRootElements.addAll(fragmentRootElements); + + return this; + } + + /** + * The starting point for reading (offset number of items). This value is overriden + * on restart if saveState is set to true. + * + * @param currentItemCount item number to begin at + * @return The current instance of the builder. + * @see StaxEventItemReader#setCurrentItemCount(int) + */ + public StaxEventItemReaderBuilder currentItemCount(int currentItemCount) { + this.currentItemCount = currentItemCount; + + return this; + } + + /** + * The maximum number of items to read. + * + * @param maxItemCount max number of items to be read + * @return The current instance of the builder. + * @see StaxEventItemReader#setMaxItemCount(int) + */ + public StaxEventItemReaderBuilder maxItemCount(int maxItemCount) { + this.maxItemCount = maxItemCount; + + return this; + } + + /** + * Indicates that the state of the reader should be saved in the + * {@link org.springframework.batch.item.ExecutionContext} for restart. True by + * default. + * + * @param saveState indicates the state of the reader should be saved + * @return The current instance of the builder. + * @see StaxEventItemReader#setSaveState(boolean) + */ + public StaxEventItemReaderBuilder saveState(boolean saveState) { + this.saveState = saveState; + + return this; + } + + /** + * Setting this value to true indicates that it is an error if the input does not + * exist and an exception will be thrown. Defaults to true. + * + * @param strict indicates the input file must exist + * @return The current instance of the builder + * @see StaxEventItemReader#setStrict(boolean) + */ + public StaxEventItemReaderBuilder strict(boolean strict) { + this.strict = strict; + + return this; + } + + /** + * Validates the configuration and builds a new {@link StaxEventItemReader} + * + * @return a new instance of the {@link StaxEventItemReader} + */ + public StaxEventItemReader build() { + Assert.notNull(this.resource, "A resource is required."); + + StaxEventItemReader reader = new StaxEventItemReader<>(); + + if(this.saveState) { + Assert.state(StringUtils.hasText(this.name), + "A name is required when saveState is set to true."); + } + else { + reader.setName(this.name); + } + + Assert.notEmpty(this.fragmentRootElements, + "At least one fragment root element is required"); + + reader.setSaveState(this.saveState); + reader.setResource(this.resource); + reader.setFragmentRootElementNames( + this.fragmentRootElements.toArray(new String[this.fragmentRootElements.size()])); + + reader.setStrict(this.strict); + reader.setUnmarshaller(this.unmarshaller); + reader.setCurrentItemCount(this.currentItemCount); + reader.setMaxItemCount(this.maxItemCount); + + return reader; + } +} diff --git a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/xml/StaxEventItemReaderCommonTests.java b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/xml/StaxEventItemReaderCommonTests.java index 6f8670c2d..d4d51e2fc 100644 --- a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/xml/StaxEventItemReaderCommonTests.java +++ b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/xml/StaxEventItemReaderCommonTests.java @@ -15,17 +15,12 @@ */ package org.springframework.batch.item.xml; -import static org.junit.Assert.assertTrue; - import java.io.IOException; - import javax.xml.stream.XMLEventReader; import javax.xml.stream.events.Attribute; import javax.xml.stream.events.StartElement; import javax.xml.transform.Source; -import org.junit.runners.JUnit4; -import org.junit.runner.RunWith; import org.springframework.batch.item.AbstractItemStreamItemReaderTests; import org.springframework.batch.item.ExecutionContext; import org.springframework.batch.item.ItemReader; @@ -34,7 +29,8 @@ import org.springframework.core.io.ByteArrayResource; import org.springframework.oxm.Unmarshaller; import org.springframework.oxm.XmlMappingException; -@RunWith(JUnit4.class) +import static org.junit.Assert.assertTrue; + public class StaxEventItemReaderCommonTests extends AbstractItemStreamItemReaderTests { private final static String FOOS = " "; diff --git a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/xml/builder/StaxEventItemReaderBuilderTests.java b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/xml/builder/StaxEventItemReaderBuilderTests.java new file mode 100644 index 000000000..ff7d16db8 --- /dev/null +++ b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/xml/builder/StaxEventItemReaderBuilderTests.java @@ -0,0 +1,212 @@ +/* + * 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.xml.builder; + +import javax.xml.bind.annotation.XmlRootElement; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import org.springframework.batch.item.ExecutionContext; +import org.springframework.batch.item.ItemStreamException; +import org.springframework.batch.item.xml.StaxEventItemReader; +import org.springframework.core.io.ByteArrayResource; +import org.springframework.core.io.Resource; +import org.springframework.oxm.jaxb.Jaxb2Marshaller; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.fail; + +/** + * @author Michael Minella + */ +public class StaxEventItemReaderBuilderTests { + + private static final String SIMPLE_XML = "1" + + "twothree4" + + "fivesix7" + + "eightnine"; + + @Mock + private Resource resource; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + } + + @Test + public void testValidation() { + try { + new StaxEventItemReaderBuilder().build(); + fail("Validation of the missing resource failed"); + } + catch (IllegalArgumentException ignore) { + } + + try { + new StaxEventItemReaderBuilder() + .resource(this.resource) + .build(); + fail("saveState == true should require a name"); + } + catch (IllegalStateException iae) { + assertEquals("A name is required when saveState is set to true.", + iae.getMessage()); + } + + try { + new StaxEventItemReaderBuilder() + .resource(this.resource) + .saveState(false) + .build(); + fail("No root tags have been configured"); + } + catch (IllegalArgumentException iae) { + assertEquals("At least one fragment root element is required", iae.getMessage()); + } + } + + @Test + public void testConfiguration() throws Exception { + Jaxb2Marshaller unmarshaller = new Jaxb2Marshaller(); + unmarshaller.setClassesToBeBound(Foo.class); + + StaxEventItemReader reader = new StaxEventItemReaderBuilder() + .name("fooReader") + .resource(getResource(SIMPLE_XML)) + .addFragmentRootElements("foo") + .currentItemCount(1) + .maxItemCount(2) + .unmarshaller(unmarshaller) + .build(); + + reader.afterPropertiesSet(); + + ExecutionContext executionContext = new ExecutionContext(); + reader.open(executionContext); + Foo item = reader.read(); + assertNull(reader.read()); + reader.update(executionContext); + + reader.close(); + + assertEquals(4, item.getFirst()); + assertEquals("five", item.getSecond()); + assertEquals("six", item.getThird()); + assertEquals(2, executionContext.size()); + } + + @Test(expected = ItemStreamException.class) + public void testStrict() throws Exception { + Jaxb2Marshaller unmarshaller = new Jaxb2Marshaller(); + unmarshaller.setClassesToBeBound(Foo.class); + + StaxEventItemReader reader = new StaxEventItemReaderBuilder() + .name("fooReader") + .resource(this.resource) + .addFragmentRootElements("foo") + .unmarshaller(unmarshaller) + .build(); + + reader.afterPropertiesSet(); + + ExecutionContext executionContext = new ExecutionContext(); + reader.open(executionContext); + } + + @Test + public void testSaveState() throws Exception { + Jaxb2Marshaller unmarshaller = new Jaxb2Marshaller(); + unmarshaller.setClassesToBeBound(Foo.class); + + StaxEventItemReader reader = new StaxEventItemReaderBuilder() + .name("fooReader") + .resource(getResource(SIMPLE_XML)) + .addFragmentRootElements("foo") + .unmarshaller(unmarshaller) + .saveState(false) + .build(); + + reader.afterPropertiesSet(); + + ExecutionContext executionContext = new ExecutionContext(); + reader.open(executionContext); + Foo item = reader.read(); + item = reader.read(); + item = reader.read(); + assertNull(reader.read()); + reader.update(executionContext); + + reader.close(); + + assertEquals(7, item.getFirst()); + assertEquals("eight", item.getSecond()); + assertEquals("nine", item.getThird()); + assertEquals(0, executionContext.size()); + } + + private Resource getResource(String contents) { + return new ByteArrayResource(contents.getBytes()); + } + + @XmlRootElement(name="foo") + public static class Foo { + private int first; + private String second; + private String third; + + public Foo() {} + + public Foo(int first, String second, String third) { + this.first = first; + this.second = second; + this.third = third; + } + + public int getFirst() { + return first; + } + + public void setFirst(int first) { + this.first = first; + } + + public String getSecond() { + return second; + } + + public void setSecond(String second) { + this.second = second; + } + + public String getThird() { + return third; + } + + public void setThird(String third) { + this.third = third; + } + + @Override + public String toString() { + return String.format("{%s, %s, %s}", this.first, this.second, this.third); + } + } +}