From a21df75ce9dfc92e9768353b827da4248aefe425 Mon Sep 17 00:00:00 2001 From: jpraet Date: Wed, 14 Aug 2013 22:37:03 +0200 Subject: [PATCH] BATCH-2049: Support multiple fragmentRootElementNames in StaxEventItemReader --- .../batch/item/xml/StaxEventItemReader.java | 75 +++++++--- .../item/xml/StaxEventItemReaderTests.java | 129 +++++++++++++++++- 2 files changed, 177 insertions(+), 27 deletions(-) diff --git a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/xml/StaxEventItemReader.java b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/xml/StaxEventItemReader.java index a2ec0f80a..964ea286b 100644 --- a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/xml/StaxEventItemReader.java +++ b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/xml/StaxEventItemReader.java @@ -17,6 +17,8 @@ package org.springframework.batch.item.xml; import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; import java.util.NoSuchElementException; import javax.xml.namespace.QName; @@ -39,6 +41,7 @@ import org.springframework.core.io.Resource; import org.springframework.oxm.Unmarshaller; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; +import org.springframework.util.StringUtils; /** * Item reader for reading XML input based on StAX. @@ -66,14 +69,12 @@ ResourceAwareItemReaderItemStream, InitializingBean { private InputStream inputStream; - private String fragmentRootElementName; + private List fragmentRootElementNames; private boolean noInput; private boolean strict = true; - private String fragmentRootElementNameSpace; - public StaxEventItemReader() { setName(ClassUtils.getShortName(StaxEventItemReader.class)); } @@ -103,7 +104,17 @@ ResourceAwareItemReaderItemStream, InitializingBean { * @param fragmentRootElementName name of the root element of the fragment */ public void setFragmentRootElementName(String fragmentRootElementName) { - this.fragmentRootElementName = fragmentRootElementName; + setFragmentRootElementNames(new String[] {fragmentRootElementName}); + } + + /** + * @param fragmentRootElementNames list of the names of the root element of the fragment + */ + public void setFragmentRootElementNames(String[] fragmentRootElementNames) { + this.fragmentRootElementNames = new ArrayList(); + for (String fragmentRootElementName : fragmentRootElementNames) { + this.fragmentRootElementNames.add(parseFragmentRootElementName(fragmentRootElementName)); + } } /** @@ -117,11 +128,11 @@ ResourceAwareItemReaderItemStream, InitializingBean { @Override public void afterPropertiesSet() throws Exception { Assert.notNull(unmarshaller, "The Unmarshaller must not be null."); - Assert.hasLength(fragmentRootElementName, "The FragmentRootElementName must not be null"); - if (fragmentRootElementName.contains("{")) { - fragmentRootElementNameSpace = fragmentRootElementName.replaceAll("\\{(.*)\\}.*", "$1"); - fragmentRootElementName = fragmentRootElementName.replaceAll("\\{.*\\}(.*)", "$1"); - } + Assert.notNull(fragmentRootElementNames, "The FragmentRootElementNames must not be null"); + Assert.notNull(fragmentRootElementNames, "The FragmentRootElementNames must not be empty"); + for (QName fragmentRootElementName : fragmentRootElementNames) { + Assert.hasText(fragmentRootElementName.getLocalPart(), "The FragmentRootElementNames must contain empty elements"); + } } /** @@ -145,11 +156,8 @@ ResourceAwareItemReaderItemStream, InitializingBean { return false; } QName startElementName = ((StartElement) reader.peek()).getName(); - if (startElementName.getLocalPart().equals(fragmentRootElementName)) { - if (fragmentRootElementNameSpace == null - || startElementName.getNamespaceURI().equals(fragmentRootElementNameSpace)) { - return true; - } + if (isFragmentRootElementName(startElementName)) { + return true; } reader.nextEvent(); @@ -249,8 +257,8 @@ ResourceAwareItemReaderItemStream, InitializingBean { protected void jumpToItem(int itemIndex) throws Exception { for (int i = 0; i < itemIndex; i++) { try { - readToStartFragment(); - readToEndFragment(); + QName fragmentName = readToStartFragment(); + readToEndFragment(fragmentName); } catch (NoSuchElementException e) { if (itemIndex == (i + 1)) { // we can presume a NoSuchElementException on the last item means the EOF was reached on the last run @@ -264,16 +272,16 @@ ResourceAwareItemReaderItemStream, InitializingBean { } /* - * Read until the first StartElement tag that matches the provided fragmentRootElementName. Because there may be any + * Read until the first StartElement tag that matches any of the provided fragmentRootElementNames. Because there may be any * number of tags in between where the reader is now and the fragment start, this is done in a loop until the * element type and name match. */ - private void readToStartFragment() throws XMLStreamException { + private QName readToStartFragment() throws XMLStreamException { while (true) { XMLEvent nextEvent = eventReader.nextEvent(); if (nextEvent.isStartElement() - && ((StartElement) nextEvent).getName().getLocalPart().equals(fragmentRootElementName)) { - return; + && isFragmentRootElementName(((StartElement) nextEvent).getName())) { + return ((StartElement) nextEvent).getName(); } } } @@ -283,13 +291,36 @@ ResourceAwareItemReaderItemStream, InitializingBean { * number of tags in between where the reader is now and the fragment end tag, this is done in a loop until the * element type and name match */ - private void readToEndFragment() throws XMLStreamException { + private void readToEndFragment(QName fragmentRootElementName) throws XMLStreamException { while (true) { XMLEvent nextEvent = eventReader.nextEvent(); if (nextEvent.isEndElement() - && ((EndElement) nextEvent).getName().getLocalPart().equals(fragmentRootElementName)) { + && fragmentRootElementName.equals(((EndElement) nextEvent).getName())) { return; } } } + + private boolean isFragmentRootElementName(QName name) { + for (QName fragmentRootElementName : fragmentRootElementNames) { + if (fragmentRootElementName.getLocalPart().equals(name.getLocalPart())) { + if (!StringUtils.hasText(fragmentRootElementName.getNamespaceURI()) + || fragmentRootElementName.getNamespaceURI().equals(name.getNamespaceURI())) { + return true; + } + } + } + return false; + } + + private QName parseFragmentRootElementName(String fragmentRootElementName) { + String name = fragmentRootElementName; + String nameSpace = null; + if (fragmentRootElementName.contains("{")) { + nameSpace = fragmentRootElementName.replaceAll("\\{(.*)\\}.*", "$1"); + name = fragmentRootElementName.replaceAll("\\{.*\\}(.*)", "$1"); + } + return new QName(nameSpace, name, ""); + } + } diff --git a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/xml/StaxEventItemReaderTests.java b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/xml/StaxEventItemReaderTests.java index 0f2dc0ea0..1ac80b4d6 100644 --- a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/xml/StaxEventItemReaderTests.java +++ b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/xml/StaxEventItemReaderTests.java @@ -10,13 +10,16 @@ import static org.junit.Assert.fail; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; +import javax.xml.namespace.QName; import javax.xml.stream.FactoryConfigurationError; import javax.xml.stream.XMLEventReader; import javax.xml.stream.XMLInputFactory; import javax.xml.stream.XMLStreamException; import javax.xml.stream.events.EndElement; +import javax.xml.stream.events.StartElement; import javax.xml.stream.events.XMLEvent; import javax.xml.transform.Source; @@ -48,6 +51,12 @@ public class StaxEventItemReaderTests { // test xml input private String xml = " testString "; + // test xml input + private String xmlMultiFragment = " testString testString "; + + // test xml input + private String xmlMultiFragmentNested = " nested nested testString testString "; + // test xml input private String emptyXml = ""; @@ -63,6 +72,8 @@ public class StaxEventItemReaderTests { private Unmarshaller unmarshaller = new MockFragmentUnmarshaller(); private static final String FRAGMENT_ROOT_ELEMENT = "fragment"; + + private static final String[] MULTI_FRAGMENT_ROOT_ELEMENTS = {"fragmentA", "fragmentB"}; private ExecutionContext executionContext; @@ -194,7 +205,110 @@ public class StaxEventItemReaderTests { source.close(); } + + @Test + public void testMultiFragment() throws Exception { + source.setResource(new ByteArrayResource(xmlMultiFragment.getBytes())); + source.setFragmentRootElementNames(MULTI_FRAGMENT_ROOT_ELEMENTS); + source.afterPropertiesSet(); + source.open(executionContext); + // see asserts in the mock unmarshaller + assertNotNull(source.read()); + assertNotNull(source.read()); + assertNotNull(source.read()); + assertNull(source.read()); // there are only three fragments + + source.close(); + } + + @Test + public void testMultiFragmentNameSpace() throws Exception { + + source.setResource(new ByteArrayResource(xmlMultiFragment.getBytes())); + source.setFragmentRootElementNames(new String[] {"{urn:org.test.bar}fragmentA", "fragmentB"}); + source.afterPropertiesSet(); + source.open(executionContext); + // see asserts in the mock unmarshaller + assertNotNull(source.read()); + assertNotNull(source.read()); + assertNull(source.read()); // there are only two fragments (one has wrong namespace) + + source.close(); + } + + @Test + public void testMultiFragmentRestart() throws Exception { + + source.setResource(new ByteArrayResource(xmlMultiFragment.getBytes())); + source.setFragmentRootElementNames(MULTI_FRAGMENT_ROOT_ELEMENTS); + source.afterPropertiesSet(); + source.open(executionContext); + // see asserts in the mock unmarshaller + assertNotNull(source.read()); + assertNotNull(source.read()); + + source.update(executionContext); + assertEquals(2, executionContext.getInt(ClassUtils.getShortName(StaxEventItemReader.class) + ".read.count")); + + source.close(); + + source = createNewInputSouce(); + source.setResource(new ByteArrayResource(xmlMultiFragment.getBytes())); + source.setFragmentRootElementNames(MULTI_FRAGMENT_ROOT_ELEMENTS); + source.afterPropertiesSet(); + source.open(executionContext); + + assertNotNull(source.read()); + assertNull(source.read()); // there are only three fragments + + source.close(); + } + + @Test + public void testMultiFragmentNested() throws Exception { + + source.setResource(new ByteArrayResource(xmlMultiFragmentNested.getBytes())); + source.setFragmentRootElementNames(MULTI_FRAGMENT_ROOT_ELEMENTS); + source.afterPropertiesSet(); + source.open(executionContext); + // see asserts in the mock unmarshaller + assertNotNull(source.read()); + assertNotNull(source.read()); + assertNotNull(source.read()); + assertNull(source.read()); // there are only three fragments + + source.close(); + } + + @Test + public void testMultiFragmentNestedRestart() throws Exception { + + source.setResource(new ByteArrayResource(xmlMultiFragmentNested.getBytes())); + source.setFragmentRootElementNames(MULTI_FRAGMENT_ROOT_ELEMENTS); + source.afterPropertiesSet(); + source.open(executionContext); + // see asserts in the mock unmarshaller + assertNotNull(source.read()); + assertNotNull(source.read()); + + source.update(executionContext); + assertEquals(2, executionContext.getInt(ClassUtils.getShortName(StaxEventItemReader.class) + ".read.count")); + + source.close(); + + source = createNewInputSouce(); + source.setResource(new ByteArrayResource(xmlMultiFragment.getBytes())); + source.setFragmentRootElementNames(MULTI_FRAGMENT_ROOT_ELEMENTS); + source.afterPropertiesSet(); + source.open(executionContext); + + assertNotNull(source.read()); + assertNull(source.read()); // there are only three fragments + + source.close(); + } + /** * Cursor is moved before beginning of next fragment. */ @@ -504,13 +618,13 @@ public class StaxEventItemReaderTests { /** * Skips the XML fragment contents. */ - private List readRecordsInsideFragment(XMLEventReader eventReader) throws XMLStreamException { + private List readRecordsInsideFragment(XMLEventReader eventReader, QName fragmentName) throws XMLStreamException { XMLEvent eventInsideFragment; List events = new ArrayList(); do { eventInsideFragment = eventReader.peek(); if (eventInsideFragment instanceof EndElement - && ((EndElement) eventInsideFragment).getName().getLocalPart().equals(FRAGMENT_ROOT_ELEMENT)) { + && fragmentName.equals(((EndElement) eventInsideFragment).getName())) { break; } events.add(eventReader.nextEvent()); @@ -545,15 +659,16 @@ public class StaxEventItemReaderTests { // second should be StartElement of the fragment XMLEvent event2 = eventReader.nextEvent(); assertTrue(event2.isStartElement()); - assertTrue(EventHelper.startElementName(event2).equals(FRAGMENT_ROOT_ELEMENT)); + assertTrue(isFragmentRootElement(EventHelper.startElementName(event2))); + QName fragmentName = ((StartElement) event2).getName(); // jump before the end of fragment - fragmentContent = readRecordsInsideFragment(eventReader); + fragmentContent = readRecordsInsideFragment(eventReader, fragmentName); // end of fragment XMLEvent event3 = eventReader.nextEvent(); assertTrue(event3.isEndElement()); - assertTrue(EventHelper.endElementName(event3).equals(FRAGMENT_ROOT_ELEMENT)); + assertTrue(isFragmentRootElement(EventHelper.endElementName(event3))); // EndDocument should follow the end of fragment XMLEvent event4 = eventReader.nextEvent(); @@ -565,6 +680,10 @@ public class StaxEventItemReaderTests { } return fragmentContent; } + + private boolean isFragmentRootElement(String name) { + return FRAGMENT_ROOT_ELEMENT.equals(name) || Arrays.asList(MULTI_FRAGMENT_ROOT_ELEMENTS).contains(name); + } }