diff --git a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/xml/StaxEventItemWriter.java b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/xml/StaxEventItemWriter.java index f1d5a7f93..522de3719 100644 --- a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/xml/StaxEventItemWriter.java +++ b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/xml/StaxEventItemWriter.java @@ -80,13 +80,13 @@ ResourceAwareItemWriterItemStream, InitializingBean { private static final Log log = LogFactory.getLog(StaxEventItemWriter.class); // default encoding - private static final String DEFAULT_ENCODING = "UTF-8"; + public static final String DEFAULT_ENCODING = "UTF-8"; // default encoding - private static final String DEFAULT_XML_VERSION = "1.0"; + public static final String DEFAULT_XML_VERSION = "1.0"; // default root tag name - private static final String DEFAULT_ROOT_TAG_NAME = "root"; + public static final String DEFAULT_ROOT_TAG_NAME = "root"; // restart data property name private static final String RESTART_DATA_NAME = "position"; diff --git a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/xml/builder/StaxEventItemWriterBuilder.java b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/xml/builder/StaxEventItemWriterBuilder.java new file mode 100644 index 000000000..dc48646a6 --- /dev/null +++ b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/xml/builder/StaxEventItemWriterBuilder.java @@ -0,0 +1,285 @@ +/* + * 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.Map; + +import org.springframework.batch.item.xml.StaxEventItemWriter; +import org.springframework.batch.item.xml.StaxWriterCallback; +import org.springframework.core.io.Resource; +import org.springframework.oxm.Marshaller; +import org.springframework.util.Assert; + +/** + * A builder for the {@link StaxEventItemWriter}. + * + * @author Michael Minella + * @since 4.0 + * @see StaxEventItemWriter + */ +public class StaxEventItemWriterBuilder { + + private Resource resource; + + private Marshaller marshaller; + + private StaxWriterCallback headerCallback; + + private StaxWriterCallback footerCallback; + + private boolean transactional = true; + + private boolean forceSync = false; + + private boolean shouldDeleteIfEmpty = false; + + private String encoding = StaxEventItemWriter.DEFAULT_ENCODING; + + private String version = StaxEventItemWriter.DEFAULT_XML_VERSION; + + private String rootTagName = StaxEventItemWriter.DEFAULT_ROOT_TAG_NAME; + + private Map rootElementAttributes; + + private boolean overwriteOutput = true; + + 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 StaxEventItemWriterBuilder#saveState(boolean)} is set to true. + * + * @param name name of the reader instance + * @return The current instance of the builder. + * @see StaxEventItemWriter#setName(String) + */ + public StaxEventItemWriterBuilder name(String name) { + this.name = name; + + return this; + } + + /** + * The {@link Resource} to be used as output. + * + * @param resource the output from the writer + * @return the current instance of the builder. + * @see StaxEventItemWriter#setResource(Resource) + */ + public StaxEventItemWriterBuilder resource(Resource resource) { + this.resource = resource; + + return this; + } + + /** + * The {@link Marshaller} implementation responsible for the serialization of the + * items to XML. This field is required. + * + * @param marshaller the component used to generate XML + * @return the current instance of the builder. + * @see StaxEventItemWriter#setMarshaller(Marshaller) + */ + public StaxEventItemWriterBuilder marshaller(Marshaller marshaller) { + this.marshaller = marshaller; + + return this; + } + + /** + * A {@link StaxWriterCallback} to provide any header elements + * + * @param headerCallback a {@link StaxWriterCallback} + * @return the current instance of the builder. + * @see StaxEventItemWriter#setHeaderCallback(StaxWriterCallback) + */ + public StaxEventItemWriterBuilder headerCallback(StaxWriterCallback headerCallback) { + this.headerCallback = headerCallback; + + return this; + } + + /** + * A {@link StaxWriterCallback} to provide any footer elements + * + * @param footerCallback a {@link StaxWriterCallback} + * @return the current instance of the builder. + * @see StaxEventItemWriter#setFooterCallback(StaxWriterCallback) + */ + public StaxEventItemWriterBuilder footerCallback(StaxWriterCallback footerCallback) { + this.footerCallback = footerCallback; + + return this; + } + + /** + * The resulting writer is participating in a transaction and writes should be delayed + * as late as possible. + * + * @param transactional indicates that the writer is transactional. Defaults to false. + * @return the current instance of the builder + * @see StaxEventItemWriter#setTransactional(boolean) + */ + public StaxEventItemWriterBuilder transactional(boolean transactional) { + this.transactional = transactional; + + return this; + } + + /** + * Flag to indicate that changes should be force-synced to disk on flush. + * + * @param forceSync indicates if force sync should occur. Defaults to false. + * @return the current instance of the builder + * @see StaxEventItemWriter#setForceSync(boolean) + */ + public StaxEventItemWriterBuilder forceSync(boolean forceSync) { + this.forceSync = forceSync; + + return this; + } + + /** + * Flag to indicate that the output file should be deleted if no results were written + * to it. Defaults to false. + * + * @param shouldDelete indicator + * @return the current instance of the builder + * @see StaxEventItemWriter#setShouldDeleteIfEmpty(boolean) + */ + public StaxEventItemWriterBuilder shouldDeleteIfEmpty(boolean shouldDelete) { + this.shouldDeleteIfEmpty = shouldDelete; + + return this; + } + + /** + * Encoding for the file. Defaults to UTF-8. + * + * @param encoding String encoding algorithm + * @return the current instance of the builder + * @see StaxEventItemWriter#setEncoding(String) + */ + public StaxEventItemWriterBuilder encoding(String encoding) { + this.encoding = encoding; + + return this; + } + + /** + * Version of XML to be generated. Must be supported by the {@link Marshaller} + * provided. + * + * @param version XML version + * @return the current instance of the builder + * @see StaxEventItemWriter#version + */ + public StaxEventItemWriterBuilder version(String version) { + this.version = version; + + return this; + } + + /** + * The name of the root tag for the output document. + * + * @param rootTagName tag name + * @return the current instance of the builder + * @see StaxEventItemWriter#setRootTagName(String) + */ + public StaxEventItemWriterBuilder rootTagName(String rootTagName) { + this.rootTagName = rootTagName; + + return this; + } + + /** + * A Map of attributes to be included in the document's root element. + * + * @param rootElementAttributes map fo attributes + * @return the current instance of the builder. + * @see StaxEventItemWriter#setRootElementAttributes(Map) + */ + public StaxEventItemWriterBuilder rootElementAttributes(Map rootElementAttributes) { + this.rootElementAttributes = rootElementAttributes; + + return this; + } + + /** + * Indicates if an existing file should be overwritten if found. Defaults to true. + * + * @param overwriteOutput indicator + * @return the current instance of the builder. + * @see StaxEventItemWriter#setOverwriteOutput(boolean) + */ + public StaxEventItemWriterBuilder overwriteOutput(boolean overwriteOutput) { + this.overwriteOutput = overwriteOutput; + + return this; + } + + /** + * Indicates if the state of the writer should be saved in the + * {@link org.springframework.batch.item.ExecutionContext}. Setting this to false + * will impact restartability. Defaults to true. + * + * @param saveState indicator + * @return the current instance of the builder + * @see StaxEventItemWriter#setSaveState(boolean) + */ + public StaxEventItemWriterBuilder saveState(boolean saveState) { + this.saveState = saveState; + + return this; + } + + /** + * Returns a configured {@link StaxEventItemWriter} + * + * @return a StaxEventItemWriter + */ + public StaxEventItemWriter build() { + Assert.notNull(this.marshaller, "A marshaller is required"); + + if(this.saveState) { + Assert.notNull(this.name, "A name is required"); + } + + StaxEventItemWriter writer = new StaxEventItemWriter<>(); + + writer.setEncoding(this.encoding); + writer.setFooterCallback(this.footerCallback); + writer.setForceSync(this.forceSync); + writer.setHeaderCallback(this.headerCallback); + writer.setMarshaller(this.marshaller); + writer.setOverwriteOutput(this.overwriteOutput); + writer.setResource(this.resource); + writer.setRootElementAttributes(this.rootElementAttributes); + writer.setRootTagName(this.rootTagName); + writer.setSaveState(this.saveState); + writer.setShouldDeleteIfEmpty(this.shouldDeleteIfEmpty); + writer.setTransactional(this.transactional); + writer.setVersion(this.version); + writer.setName(this.name); + + return writer; + } + + } diff --git a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/xml/builder/StaxEventItemWriterBuilderTests.java b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/xml/builder/StaxEventItemWriterBuilderTests.java new file mode 100644 index 000000000..0d5b2cbad --- /dev/null +++ b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/xml/builder/StaxEventItemWriterBuilderTests.java @@ -0,0 +1,271 @@ +/* + * 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.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import javax.xml.bind.annotation.XmlRootElement; +import javax.xml.stream.XMLEventFactory; +import javax.xml.stream.XMLStreamException; + +import org.apache.commons.io.FileUtils; +import org.junit.Before; +import org.junit.Test; + +import org.springframework.batch.item.ExecutionContext; +import org.springframework.batch.item.ItemStreamException; +import org.springframework.batch.item.xml.StaxEventItemWriter; +import org.springframework.batch.support.transaction.TransactionAwareBufferedWriter; +import org.springframework.core.io.FileSystemResource; +import org.springframework.core.io.Resource; +import org.springframework.oxm.Marshaller; +import org.springframework.oxm.jaxb.Jaxb2Marshaller; +import org.springframework.test.util.ReflectionTestUtils; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +/** + * @author Michael Minella + */ +public class StaxEventItemWriterBuilderTests { + + private Resource resource; + + private List items; + + private Marshaller marshaller; + + private static final String FULL_OUTPUT = "" + + "\uFEFF" + + "1twothree\uFEFF" + + "4" + + "fivesix\uFEFF" + + "7" + + "eightnine\uFEFF\uFEFF" + + ""; + + private static final String SIMPLE_OUTPUT = "" + + "1twothree" + + "4" + + "fivesix" + + "7" + + "eightnine"; + + @Before + public void setUp() throws IOException { + File directory = new File("build/data"); + directory.mkdirs(); + this.resource = new FileSystemResource( + File.createTempFile("StaxEventItemWriterBuilderTests", ".xml", directory)); + + this.items = new ArrayList<>(3); + this.items.add(new Foo(1, "two", "three")); + this.items.add(new Foo(4, "five", "six")); + this.items.add(new Foo(7, "eight", "nine")); + + marshaller = new Jaxb2Marshaller(); + ((Jaxb2Marshaller) marshaller).setClassesToBeBound(Foo.class); + } + + @Test(expected = ItemStreamException.class) + public void testOverwriteOutput() throws Exception { + StaxEventItemWriter staxEventItemWriter = new StaxEventItemWriterBuilder() + .name("fooWriter") + .marshaller(marshaller) + .resource(this.resource) + .overwriteOutput(false) + .build(); + + staxEventItemWriter.afterPropertiesSet(); + + ExecutionContext executionContext = new ExecutionContext(); + staxEventItemWriter.open(executionContext); + + staxEventItemWriter.write(this.items); + + staxEventItemWriter.update(executionContext); + staxEventItemWriter.close(); + + File output = this.resource.getFile(); + + assertTrue(output.exists()); + + executionContext = new ExecutionContext(); + staxEventItemWriter.open(executionContext); + } + + @Test + public void testDeleteIfEmpty() throws Exception { + ExecutionContext executionContext = new ExecutionContext(); + + StaxEventItemWriter staxEventItemWriter = new StaxEventItemWriterBuilder() + .name("fooWriter") + .resource(this.resource) + .marshaller(this.marshaller) + .shouldDeleteIfEmpty(true) + .build(); + + staxEventItemWriter.afterPropertiesSet(); + staxEventItemWriter.open(executionContext); + staxEventItemWriter.write(Collections.emptyList()); + staxEventItemWriter.update(executionContext); + staxEventItemWriter.close(); + + File file = this.resource.getFile(); + + assertFalse(file.exists()); + } + + @Test + public void testTransactional() { + + StaxEventItemWriter staxEventItemWriter = new StaxEventItemWriterBuilder() + .name("fooWriter") + .resource(this.resource) + .marshaller(this.marshaller) + .transactional(true) + .forceSync(true) + .build(); + + ExecutionContext executionContext = new ExecutionContext(); + + staxEventItemWriter.open(executionContext); + + Object writer = ReflectionTestUtils.getField(staxEventItemWriter, "bufferedWriter"); + + assertTrue(writer instanceof TransactionAwareBufferedWriter); + + assertTrue((Boolean) ReflectionTestUtils.getField(writer, "forceSync")); + } + + @Test + public void testConfiguration() throws Exception { + Map rootElementAttributes = new HashMap<>(); + rootElementAttributes.put("baz", "quix"); + + StaxEventItemWriter staxEventItemWriter = new StaxEventItemWriterBuilder() + .name("fooWriter") + .marshaller(marshaller) + .encoding("UTF-16") + .footerCallback(writer -> { + XMLEventFactory factory = XMLEventFactory.newInstance(); + try { + writer.add(factory.createEndElement("ns", + "http://www.springframework.org/test", + "group")); + } + catch (XMLStreamException e) { + throw new RuntimeException(e); + } + }) + .headerCallback(writer -> { + XMLEventFactory factory = XMLEventFactory.newInstance(); + try { + writer.add(factory.createStartElement("ns", + "http://www.springframework.org/test", + "group")); + } + catch (XMLStreamException e) { + throw new RuntimeException(e); + } + }) + .resource(this.resource) + .rootTagName("foobarred") + .rootElementAttributes(rootElementAttributes) + .saveState(false) + .version("1.1") + .build(); + + staxEventItemWriter.afterPropertiesSet(); + + ExecutionContext executionContext = new ExecutionContext(); + staxEventItemWriter.open(executionContext); + + staxEventItemWriter.write(this.items); + + staxEventItemWriter.update(executionContext); + staxEventItemWriter.close(); + + assertEquals(FULL_OUTPUT, getOutputFileContent("UTF-16")); + assertEquals(0, executionContext.size()); + } + + @Test(expected = IllegalArgumentException.class) + public void testMissingMarshallerValidation() { + new StaxEventItemWriterBuilder() + .name("fooWriter") + .build(); + } + + @Test(expected = IllegalArgumentException.class) + public void testMissingNameValidation() { + new StaxEventItemWriterBuilder() + .marshaller(new Jaxb2Marshaller()) + .build(); + } + + private String getOutputFileContent(String encoding) throws IOException { + String value = FileUtils.readFileToString(resource.getFile(), encoding); + value = value.replace("", ""); + return value; + } + + @XmlRootElement(name="item", namespace="http://www.springframework.org/test") + 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; + } + } +}