BATCH-2049: Support multiple fragmentRootElementNames in
StaxEventItemReader
This commit is contained in:
@@ -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<T>, InitializingBean {
|
||||
|
||||
private InputStream inputStream;
|
||||
|
||||
private String fragmentRootElementName;
|
||||
private List<QName> fragmentRootElementNames;
|
||||
|
||||
private boolean noInput;
|
||||
|
||||
private boolean strict = true;
|
||||
|
||||
private String fragmentRootElementNameSpace;
|
||||
|
||||
public StaxEventItemReader() {
|
||||
setName(ClassUtils.getShortName(StaxEventItemReader.class));
|
||||
}
|
||||
@@ -103,7 +104,17 @@ ResourceAwareItemReaderItemStream<T>, 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<QName>();
|
||||
for (String fragmentRootElementName : fragmentRootElementNames) {
|
||||
this.fragmentRootElementNames.add(parseFragmentRootElementName(fragmentRootElementName));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -117,11 +128,11 @@ ResourceAwareItemReaderItemStream<T>, 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<T>, 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<T>, 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<T>, 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<T>, 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, "");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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 = "<root> <fragment> <misc1/> </fragment> <misc2/> <fragment> testString </fragment> </root>";
|
||||
|
||||
// test xml input
|
||||
private String xmlMultiFragment = "<root> <fragmentA> <misc1/> </fragmentA> <misc2/> <fragmentB> testString </fragmentB> <fragmentA xmlns=\"urn:org.test.bar\"> testString </fragmentA></root>";
|
||||
|
||||
// test xml input
|
||||
private String xmlMultiFragmentNested = "<root> <fragmentA> <misc1/> <fragmentB> nested</fragmentB> <fragmentB> nested </fragmentB></fragmentA> <misc2/> <fragmentB> testString </fragmentB> <fragmentA xmlns=\"urn:org.test.bar\"> testString </fragmentA></root>";
|
||||
|
||||
// test xml input
|
||||
private String emptyXml = "<root></root>";
|
||||
|
||||
@@ -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<XMLEvent> readRecordsInsideFragment(XMLEventReader eventReader) throws XMLStreamException {
|
||||
private List<XMLEvent> readRecordsInsideFragment(XMLEventReader eventReader, QName fragmentName) throws XMLStreamException {
|
||||
XMLEvent eventInsideFragment;
|
||||
List<XMLEvent> events = new ArrayList<XMLEvent>();
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user