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 7405c0daf1..374cb3435e 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 @@ -1,5 +1,5 @@ /* - * Copyright 2006-2020 the original author or authors. + * Copyright 2006-2022 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. @@ -52,7 +52,6 @@ import org.springframework.batch.support.transaction.TransactionAwareBufferedWriter; import org.springframework.beans.factory.InitializingBean; import org.springframework.core.io.Resource; -import org.springframework.dao.DataAccessResourceFailureException; import org.springframework.oxm.Marshaller; import org.springframework.oxm.XmlMappingException; import org.springframework.util.Assert; @@ -64,12 +63,12 @@ /** * An implementation of {@link ItemWriter} which uses StAX and * {@link Marshaller} for serializing object to XML. - * + * * This item writer also provides restart, statistics and transaction features * by implementing corresponding interfaces. - * + * * The implementation is not thread-safe. - * + * * @author Peter Zozom * @author Robert Kasanicky * @author Michael Minella @@ -98,7 +97,7 @@ public class StaxEventItemWriter extends AbstractItemStreamItemWriter impl // unclosed header callback elements property name private static final String UNCLOSED_HEADER_CALLBACK_ELEMENTS_NAME = "unclosedHeaderCallbackElements"; - + // restart data property name private static final String WRITE_STATISTICS_NAME = "record.count"; @@ -159,11 +158,11 @@ public class StaxEventItemWriter extends AbstractItemStreamItemWriter impl private boolean forceSync; private boolean shouldDeleteIfEmpty = false; - + private boolean restarted = false; private boolean initialized = false; - + // List holding the QName of elements that were opened in the header callback, but not closed private List unclosedHeaderCallbackElements = Collections.emptyList(); @@ -173,7 +172,7 @@ public StaxEventItemWriter() { /** * Set output file. - * + * * @param resource the output file */ @Override @@ -183,7 +182,7 @@ public void setResource(Resource resource) { /** * Set Object to XML marshaller. - * + * * @param marshaller the Object to XML marshaller */ public void setMarshaller(Marshaller marshaller) { @@ -212,7 +211,7 @@ public void setFooterCallback(StaxWriterCallback footerCallback) { /** * Flag to indicate that writes should be deferred to the end of a * transaction if present. Defaults to true. - * + * * @param transactional the flag to set */ public void setTransactional(boolean transactional) { @@ -225,7 +224,7 @@ public void setTransactional(boolean transactional) { * be lost if the OS crashes in between a write and a cache flush. Setting * to true may result in slower performance for usage patterns involving * many frequent writes. - * + * * @param forceSync the flag value to set */ public void setForceSync(boolean forceSync) { @@ -235,7 +234,7 @@ public void setForceSync(boolean forceSync) { /** * Flag to indicate that the target file should be deleted if no items have * been written (other than header and footer) on close. Defaults to false. - * + * * @param shouldDeleteIfEmpty the flag value to set */ public void setShouldDeleteIfEmpty(boolean shouldDeleteIfEmpty) { @@ -244,7 +243,7 @@ public void setShouldDeleteIfEmpty(boolean shouldDeleteIfEmpty) { /** * Get used encoding. - * + * * @return the encoding used */ public String getEncoding() { @@ -253,7 +252,7 @@ public String getEncoding() { /** * Set encoding to be used for output file. - * + * * @param encoding the encoding to be used */ public void setEncoding(String encoding) { @@ -271,7 +270,7 @@ public String getVersion() { /** * Set XML version to be used for output XML. - * + * * @param version the XML version to be used */ public void setVersion(String version) { @@ -303,7 +302,7 @@ public void setStandalone(Boolean standalone) { /** * Get the tag name of the root element. - * + * * @return the root element tag name */ public String getRootTagName() { @@ -314,16 +313,16 @@ public String getRootTagName() { * Set the tag name of the root element. If not set, default name is used * ("root"). Namespace URI and prefix can also be set optionally using the * notation: - * + * *
 	 * {uri}prefix:root
 	 * 
- * + * * The prefix is optional (defaults to empty), but if it is specified then * the uri must be provided. In addition you might want to declare other * namespaces using the {@link #setRootElementAttributes(Map) root * attributes}. - * + * * @param rootTagName the tag name to be used for the root element */ public void setRootTagName(String rootTagName) { @@ -332,7 +331,7 @@ public void setRootTagName(String rootTagName) { /** * Get the namespace prefix of the root element. Empty by default. - * + * * @return the rootTagNamespacePrefix */ public String getRootTagNamespacePrefix() { @@ -341,7 +340,7 @@ public String getRootTagNamespacePrefix() { /** * Get the namespace of the root element. - * + * * @return the rootTagNamespace */ public String getRootTagNamespace() { @@ -350,7 +349,7 @@ public String getRootTagNamespace() { /** * Get attributes of the root element. - * + * * @return attributes of the root element */ public Map getRootElementAttributes() { @@ -360,7 +359,7 @@ public Map getRootElementAttributes() { /** * Set the root element attributes to be written. If any of the key names * begin with "xmlns:" then they are treated as namespace declarations. - * + * * @param rootElementAttributes attributes of the root element */ public void setRootElementAttributes(Map rootElementAttributes) { @@ -370,7 +369,7 @@ public void setRootElementAttributes(Map rootElementAttributes) /** * Set "overwrite" flag for the output file. Flag is ignored when output * file processing is restarted. - * + * * @param overwriteOutput If set to true, output file will be overwritten * (this flag is ignored when processing is restart). */ @@ -403,7 +402,7 @@ public void afterPropertiesSet() throws Exception { * Open the output source * * @param executionContext the batch context. - * + * * @see org.springframework.batch.item.ItemStream#open(ExecutionContext) */ @SuppressWarnings("unchecked") @@ -414,7 +413,7 @@ public void open(ExecutionContext executionContext) { Assert.notNull(resource, "The resource must be set"); long startAtPosition = 0; - + // if restart data is provided, restart from provided offset // otherwise start from beginning if (executionContext.containsKey(getExecutionContextKey(RESTART_DATA_NAME))) { @@ -424,7 +423,7 @@ public void open(ExecutionContext executionContext) { unclosedHeaderCallbackElements = (List) executionContext .get(getExecutionContextKey(UNCLOSED_HEADER_CALLBACK_ELEMENTS_NAME)); } - + restarted = true; if (shouldDeleteIfEmpty && currentRecordCount == 0) { // previous execution deleted the output file because no items were written @@ -476,7 +475,7 @@ private void open(long position) { setPosition(position); } catch (IOException ioe) { - throw new DataAccessResourceFailureException("Unable to write to file resource: [" + resource + "]", ioe); + throw new ItemStreamException("Unable to write to file resource: [" + resource + "]", ioe); } XMLOutputFactory outputFactory = createXmlOutputFactory(); @@ -524,14 +523,14 @@ public void run() { } } catch (XMLStreamException xse) { - throw new DataAccessResourceFailureException("Unable to write to file resource: [" + resource + "]", xse); + throw new ItemStreamException("Unable to write to file resource: [" + resource + "]", xse); } catch (UnsupportedEncodingException e) { - throw new DataAccessResourceFailureException("Unable to write to file resource: [" + resource + throw new ItemStreamException("Unable to write to file resource: [" + resource + "] with encoding=[" + encoding + "]", e); - } + } catch (IOException e) { - throw new DataAccessResourceFailureException("Unable to write to file resource: [" + resource + "]", e); + throw new ItemStreamException("Unable to write to file resource: [" + resource + "]", e); } } @@ -588,7 +587,7 @@ protected Result createStaxResult() { *
  • rootTagNamespacePrefix for rootTagName
  • *
  • any other xmlns namespace prefix declarations in the root element attributes
  • * - * + * * @param writer XML event writer * * @throws XMLStreamException thrown if error occurs while setting the @@ -627,7 +626,7 @@ protected void initNamespaceContext(XMLEventWriter writer) throws XMLStreamExcep * * If this is not sufficient for you, simply override this method. Encoding, * version and root tag name can be retrieved with corresponding getters. - * + * * @param writer XML event writer * * @throws XMLStreamException thrown if error occurs. @@ -685,7 +684,7 @@ protected void startDocument(XMLEventWriter writer) throws XMLStreamException { /** * Writes the EndDocument tag manually. - * + * * @param writer XML event writer * * @throws XMLStreamException thrown if error occurs. @@ -700,13 +699,13 @@ protected void endDocument(XMLEventWriter writer) throws XMLStreamException { bufferedWriter.write(""); } catch (IOException ioe) { - throw new DataAccessResourceFailureException("Unable to close file resource: [" + resource + "]", ioe); + throw new XMLStreamException("Unable to close file resource: [" + resource + "]", ioe); } } /** * Flush and close the output source. - * + * * @see org.springframework.batch.item.ItemStream#close() */ @Override @@ -727,7 +726,7 @@ public void close() { if (restarted && !unclosedHeaderCallbackElements.isEmpty()) { footerCallbackWriter = new UnopenedElementClosingEventWriter( delegateEventWriter, bufferedWriter, unclosedHeaderCallbackElements); - } + } footerCallback.write(footerCallbackWriter); } delegateEventWriter.flush(); @@ -784,7 +783,7 @@ private void closeStream() { /** * Write the value objects and flush them to the file. - * + * * @param items the value object * * @throws IOException thrown if general error occurs. @@ -809,18 +808,18 @@ public void write(List items) throws XmlMappingException, IOExcepti eventWriter.flush(); if (forceSync) { channel.force(false); - } + } } catch (XMLStreamException | IOException e) { throw new WriteFailedException("Failed to flush the events", e); - } + } } /** * Get the restart data. * * @param executionContext the batch context. - * + * * @see org.springframework.batch.item.ItemStream#update(ExecutionContext) */ @Override @@ -833,14 +832,14 @@ public void update(ExecutionContext executionContext) { if (!unclosedHeaderCallbackElements.isEmpty()) { executionContext.put(getExecutionContextKey(UNCLOSED_HEADER_CALLBACK_ELEMENTS_NAME), unclosedHeaderCallbackElements); - } + } } } /* * Get the actual position in file channel. This method flushes any buffered * data before position is read. - * + * * @return byte offset in file channel */ private long getPosition() { @@ -855,7 +854,7 @@ private long getPosition() { } } catch (Exception e) { - throw new DataAccessResourceFailureException("Unable to write to file resource: [" + resource + "]", e); + throw new ItemStreamException("Unable to write to file resource: [" + resource + "]", e); } return position; @@ -863,7 +862,7 @@ private long getPosition() { /** * Set the file channel position. - * + * * @param newPosition new file channel position */ private void setPosition(long newPosition) { @@ -873,7 +872,7 @@ private void setPosition(long newPosition) { channel.position(newPosition); } catch (IOException e) { - throw new DataAccessResourceFailureException("Unable to write to file resource: [" + resource + "]", e); + throw new ItemStreamException("Unable to write to file resource: [" + resource + "]", e); } } diff --git a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/xml/stax/DefaultFragmentEventReader.java b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/xml/stax/DefaultFragmentEventReader.java index 97e7be168c..c351ec8b15 100644 --- a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/xml/stax/DefaultFragmentEventReader.java +++ b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/xml/stax/DefaultFragmentEventReader.java @@ -1,5 +1,5 @@ /* - * Copyright 2006-2007 the original author or authors. + * Copyright 2006-2022 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. @@ -28,7 +28,7 @@ import javax.xml.stream.events.StartElement; import javax.xml.stream.events.XMLEvent; -import org.springframework.dao.DataAccessResourceFailureException; +import org.springframework.batch.item.ItemStreamException; /** * Default implementation of {@link FragmentEventReader} @@ -71,7 +71,7 @@ public DefaultFragmentEventReader(XMLEventReader wrappedEventReader) { startDocumentEvent = (StartDocument) wrappedEventReader.peek(); } catch (XMLStreamException e) { - throw new DataAccessResourceFailureException("Error reading start document from event reader", e); + throw new ItemStreamException("Error reading start document from event reader", e); } endDocumentEvent = XMLEventFactory.newInstance().createEndDocument(); @@ -91,7 +91,7 @@ public boolean hasNext() { } } catch (XMLStreamException e) { - throw new DataAccessResourceFailureException("Error reading XML stream", e); + throw new ItemStreamException("Error reading XML stream", e); } return false; } @@ -102,7 +102,7 @@ public Object next() { return nextEvent(); } catch (XMLStreamException e) { - throw new DataAccessResourceFailureException("Error reading XML stream", e); + throw new ItemStreamException("Error reading XML stream", e); } } @@ -186,7 +186,7 @@ public void markFragmentProcessed() { } } catch (XMLStreamException e) { - throw new DataAccessResourceFailureException("Error reading XML stream", e); + throw new ItemStreamException("Error reading XML stream", e); } } fakeDocumentEnd = false; diff --git a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/xml/stax/UnopenedElementClosingEventWriter.java b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/xml/stax/UnopenedElementClosingEventWriter.java index 768c9f20d0..13b8ec54f5 100644 --- a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/xml/stax/UnopenedElementClosingEventWriter.java +++ b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/xml/stax/UnopenedElementClosingEventWriter.java @@ -1,82 +1,81 @@ -/* - * Copyright 2014 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.item.xml.stax; - -import java.io.IOException; -import java.io.Writer; -import java.util.LinkedList; -import java.util.List; - -import javax.xml.namespace.QName; -import javax.xml.stream.XMLEventWriter; -import javax.xml.stream.XMLStreamException; -import javax.xml.stream.events.XMLEvent; - -import org.springframework.dao.DataAccessResourceFailureException; -import org.springframework.util.StringUtils; - -/** - * Delegating XMLEventWriter, which writes EndElement events that match a given collection of QNames directly - * to the underlying java.io.Writer instead of to the delegate XMLEventWriter. - * - * @author Jimmy Praet - * @since 3.0 - */ -public class UnopenedElementClosingEventWriter extends AbstractEventWriterWrapper { - - private LinkedList unopenedElements; - - private Writer ioWriter; - - public UnopenedElementClosingEventWriter(XMLEventWriter wrappedEventWriter, Writer ioWriter, List unopenedElements) { - super(wrappedEventWriter); - this.unopenedElements = new LinkedList<>(unopenedElements); - this.ioWriter = ioWriter; - } - - /* (non-Javadoc) - * @see org.springframework.batch.item.xml.stax.AbstractEventWriterWrapper#add(javax.xml.stream.events.XMLEvent) - */ - @Override - public void add(XMLEvent event) throws XMLStreamException { - if (isUnopenedElementCloseEvent(event)) { - QName element = unopenedElements.removeLast(); - String nsPrefix = !StringUtils.hasText(element.getPrefix()) ? "" : element.getPrefix() + ":"; - try { - super.flush(); - ioWriter.write(""); - ioWriter.flush(); - } - catch (IOException ioe) { - throw new DataAccessResourceFailureException("Unable to close tag: " + element, ioe); - } - } else { - super.add(event); - } - } - - private boolean isUnopenedElementCloseEvent(XMLEvent event) { - if (unopenedElements.isEmpty()) { - return false; - } else if (!event.isEndElement()) { - return false; - } else { - return unopenedElements.getLast().equals(event.asEndElement().getName()); - } - } - -} +/* + * Copyright 2014-2022 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.item.xml.stax; + +import java.io.IOException; +import java.io.Writer; +import java.util.LinkedList; +import java.util.List; + +import javax.xml.namespace.QName; +import javax.xml.stream.XMLEventWriter; +import javax.xml.stream.XMLStreamException; +import javax.xml.stream.events.XMLEvent; + +import org.springframework.util.StringUtils; + +/** + * Delegating XMLEventWriter, which writes EndElement events that match a given collection of QNames directly + * to the underlying java.io.Writer instead of to the delegate XMLEventWriter. + * + * @author Jimmy Praet + * @since 3.0 + */ +public class UnopenedElementClosingEventWriter extends AbstractEventWriterWrapper { + + private LinkedList unopenedElements; + + private Writer ioWriter; + + public UnopenedElementClosingEventWriter(XMLEventWriter wrappedEventWriter, Writer ioWriter, List unopenedElements) { + super(wrappedEventWriter); + this.unopenedElements = new LinkedList<>(unopenedElements); + this.ioWriter = ioWriter; + } + + /* (non-Javadoc) + * @see org.springframework.batch.item.xml.stax.AbstractEventWriterWrapper#add(javax.xml.stream.events.XMLEvent) + */ + @Override + public void add(XMLEvent event) throws XMLStreamException { + if (isUnopenedElementCloseEvent(event)) { + QName element = unopenedElements.removeLast(); + String nsPrefix = !StringUtils.hasText(element.getPrefix()) ? "" : element.getPrefix() + ":"; + try { + super.flush(); + ioWriter.write(""); + ioWriter.flush(); + } + catch (IOException ioe) { + throw new XMLStreamException("Unable to close tag: " + element, ioe); + } + } else { + super.add(event); + } + } + + private boolean isUnopenedElementCloseEvent(XMLEvent event) { + if (unopenedElements.isEmpty()) { + return false; + } else if (!event.isEndElement()) { + return false; + } else { + return unopenedElements.getLast().equals(event.asEndElement().getName()); + } + } + +} diff --git a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/xml/stax/UnopenedElementClosingEventWriterTests.java b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/xml/stax/UnopenedElementClosingEventWriterTests.java index 6fe2acdcec..5838d26f0e 100644 --- a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/xml/stax/UnopenedElementClosingEventWriterTests.java +++ b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/xml/stax/UnopenedElementClosingEventWriterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014 the original author or authors. + * Copyright 2014-2022 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. @@ -27,6 +27,7 @@ import javax.xml.namespace.QName; import javax.xml.stream.XMLEventFactory; import javax.xml.stream.XMLEventWriter; +import javax.xml.stream.XMLStreamException; import javax.xml.stream.events.EndElement; import javax.xml.stream.events.StartElement; import javax.xml.stream.events.XMLEvent; @@ -34,11 +35,10 @@ import org.junit.Before; import org.junit.Test; import org.mockito.Mockito; -import org.springframework.dao.DataAccessResourceFailureException; /** * Tests for {@link UnopenedElementClosingEventWriter} - * + * * @author Jimmy Praet */ public class UnopenedElementClosingEventWriterTests { @@ -46,17 +46,17 @@ public class UnopenedElementClosingEventWriterTests { private UnopenedElementClosingEventWriter writer; private XMLEventWriter wrappedWriter; - + private Writer ioWriter; private XMLEventFactory eventFactory = XMLEventFactory.newInstance(); - + private List unopenedElements = new LinkedList<>(); - + private QName unopenedA = new QName("http://test", "unopened-a", "t"); - + private QName unopenedB = new QName("", "unopened-b", ""); - + private QName other = new QName("http://test", "other", "t"); @Before @@ -67,7 +67,7 @@ public void setUp() throws Exception { unopenedElements.add(unopenedB); writer = new UnopenedElementClosingEventWriter(wrappedWriter, ioWriter, unopenedElements); } - + @Test public void testEndUnopenedElements() throws Exception { EndElement endElementB = eventFactory.createEndElement(unopenedB, null); @@ -83,17 +83,17 @@ public void testEndUnopenedElements() throws Exception { verify(ioWriter).write(""); verify(ioWriter, Mockito.times(2)).flush(); } - + @Test public void testEndUnopenedElementRemovesFromList() throws Exception { EndElement endElement = eventFactory.createEndElement(unopenedB, null); writer.add(endElement); - + verify(wrappedWriter, Mockito.never()).add(endElement); verify(wrappedWriter).flush(); verify(ioWriter).write(""); verify(ioWriter).flush(); - + StartElement startElement = eventFactory.createStartElement(unopenedB, null, null); writer.add(startElement); endElement = eventFactory.createEndElement(unopenedB, null); @@ -101,28 +101,28 @@ public void testEndUnopenedElementRemovesFromList() throws Exception { verify(wrappedWriter).add(startElement); verify(wrappedWriter).add(endElement); - + // only internal list should be modified - assertEquals(2, unopenedElements.size()); - } - + assertEquals(2, unopenedElements.size()); + } + @Test public void testOtherEndElement() throws Exception { EndElement endElement = eventFactory.createEndElement(other, null); writer.add(endElement); - + verify(wrappedWriter).add(endElement); - } + } @Test public void testOtherEvent() throws Exception { XMLEvent event = eventFactory.createCharacters("foo"); writer.add(event); - + verify(wrappedWriter).add(event); - } - - @Test (expected = DataAccessResourceFailureException.class) + } + + @Test (expected = XMLStreamException.class) public void testIOException() throws Exception { EndElement endElementB = eventFactory.createEndElement(unopenedB, null); Mockito.doThrow(new IOException("Simulated IOException")).when(ioWriter).write("");