/*
* Copyright 2008-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
*
* 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;
import org.junit.Before;
import org.junit.Test;
import org.springframework.batch.item.ExecutionContext;
import org.springframework.batch.item.ItemCountAware;
import org.springframework.batch.item.ItemStreamException;
import org.springframework.batch.item.NonTransientResourceException;
import org.springframework.core.io.AbstractResource;
import org.springframework.core.io.ByteArrayResource;
import org.springframework.core.io.FileSystemResource;
import org.springframework.core.io.Resource;
import org.springframework.oxm.Unmarshaller;
import org.springframework.oxm.UnmarshallingFailureException;
import org.springframework.oxm.XmlMappingException;
import org.springframework.util.ClassUtils;
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;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
/**
* Tests for {@link StaxEventItemReader}.
*
* @author Robert Kasanicky
*/
public class StaxEventItemReaderTests {
// object under test
private StaxEventItemReader<List<XMLEvent>> source;
// 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>";
// test xml input
private String missingXml = "<root><misc1/><misc2>foo</misc2></root>";
private String fooXml = "<root xmlns=\"urn:org.test.foo\"> <fragment> <misc1/> </fragment> <misc2/> <fragment> testString </fragment> </root>";
private String mixedXml = "<fragment xmlns=\"urn:org.test.foo\"> <fragment xmlns=\"urn:org.test.bar\"> <misc1/> </fragment> <misc2/> <fragment xmlns=\"urn:org.test.bar\"> testString </fragment> </fragment>";
private String invalidXml = "<root> </fragment> <misc1/> </root>";
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;
@Before
public void setUp() throws Exception {
this.executionContext = new ExecutionContext();
source = createNewInputSouce();
}
@Test
public void testAfterPropertiesSet() throws Exception {
source.afterPropertiesSet();
}
@Test
public void testAfterPropertesSetException() throws Exception {
source = createNewInputSouce();
source.setFragmentRootElementName("");
try {
source.afterPropertiesSet();
fail();
}
catch (IllegalArgumentException e) {
// expected
}
source = createNewInputSouce();
source.setUnmarshaller(null);
try {
source.afterPropertiesSet();
fail();
}
catch (IllegalArgumentException e) {
// expected
}
}
/**
* Regular usage scenario. ItemReader should pass XML fragments to unmarshaller wrapped with StartDocument and
* EndDocument events.
*/
@Test
public void testFragmentWrapping() throws Exception {
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
source.close();
}
@Test
public void testItemCountAwareFragment() throws Exception {
StaxEventItemReader<ItemCountAwareFragment> source = createNewItemCountAwareInputSouce();
source.afterPropertiesSet();
source.open(executionContext);
assertEquals(1, source.read().getItemCount());
assertEquals(2, source.read().getItemCount());
assertNull(source.read()); // there are only two fragments
source.close();
}
@Test
public void testItemCountAwareFragmentRestart() throws Exception {
StaxEventItemReader<ItemCountAwareFragment> source = createNewItemCountAwareInputSouce();
source.afterPropertiesSet();
source.open(executionContext);
assertEquals(1, source.read().getItemCount());
source.update(executionContext);
source.close();
source = createNewItemCountAwareInputSouce();
source.afterPropertiesSet();
source.open(executionContext);
assertEquals(2, source.read().getItemCount());
assertNull(source.read()); // there are only two fragments
source.close();
}
@Test
public void testFragmentNamespace() throws Exception {
source.setResource(new ByteArrayResource(fooXml.getBytes()));
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
source.close();
}
@Test
public void testFragmentMixedNamespace() throws Exception {
source.setResource(new ByteArrayResource(mixedXml.getBytes()));
source.setFragmentRootElementName("{urn:org.test.bar}" + FRAGMENT_ROOT_ELEMENT);
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
source.close();
}
@Test
public void testFragmentInvalid() throws Exception {
source.setResource(new ByteArrayResource(invalidXml.getBytes()));
source.setFragmentRootElementName(FRAGMENT_ROOT_ELEMENT);
source.afterPropertiesSet();
source.open(executionContext);
// Should fail before it gets to the marshaller
try {
assertNotNull(source.read());
fail("Expected NonTransientResourceException");
}
catch (NonTransientResourceException e) {
// expected
}
assertNull(source.read()); // after an error there is no more output
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.
*/
@Test
public void testMoveCursorToNextFragment() throws XMLStreamException, FactoryConfigurationError, IOException {
Resource resource = new ByteArrayResource(xml.getBytes());
XMLEventReader reader = XMLInputFactory.newInstance().createXMLEventReader(resource.getInputStream());
final int EXPECTED_NUMBER_OF_FRAGMENTS = 2;
for (int i = 0; i < EXPECTED_NUMBER_OF_FRAGMENTS; i++) {
assertTrue(source.moveCursorToNextFragment(reader));
assertTrue(EventHelper.startElementName(reader.peek()).equals("fragment"));
reader.nextEvent(); // move away from beginning of fragment
}
assertFalse(source.moveCursorToNextFragment(reader));
}
/**
* Empty document works OK.
*/
@Test
public void testMoveCursorToNextFragmentOnEmpty() throws XMLStreamException, FactoryConfigurationError, IOException {
Resource resource = new ByteArrayResource(emptyXml.getBytes());
XMLEventReader reader = XMLInputFactory.newInstance().createXMLEventReader(resource.getInputStream());
assertFalse(source.moveCursorToNextFragment(reader));
}
/**
* Document with no fragments works OK.
*/
@Test
public void testMoveCursorToNextFragmentOnMissing() throws XMLStreamException, FactoryConfigurationError, IOException {
Resource resource = new ByteArrayResource(missingXml.getBytes());
XMLEventReader reader = XMLInputFactory.newInstance().createXMLEventReader(resource.getInputStream());
assertFalse(source.moveCursorToNextFragment(reader));
}
/**
* Save restart data and restore from it.
*/
@Test
public void testRestart() throws Exception {
source.open(executionContext);
source.read();
source.update(executionContext);
assertEquals(1, executionContext.getInt(ClassUtils.getShortName(StaxEventItemReader.class) + ".read.count"));
List<XMLEvent> expectedAfterRestart = source.read();
source = createNewInputSouce();
source.open(executionContext);
List<XMLEvent> afterRestart = source.read();
assertEquals(expectedAfterRestart.size(), afterRestart.size());
}
/**
* Test restart at end of file.
*/
@Test
public void testRestartAtEndOfFile() throws Exception {
source.open(executionContext);
assertNotNull(source.read());
assertNotNull(source.read());
assertNull(source.read());
source.update(executionContext);
source.close();
assertEquals(3, executionContext.getInt(ClassUtils.getShortName(StaxEventItemReader.class) + ".read.count"));
source = createNewInputSouce();
source.open(executionContext);
assertNull(source.read());
}
@Test
public void testRestoreWorksFromClosedStream() throws Exception {
source.close();
source.update(executionContext);
}
/**
* Statistics return the current record count. Calling read after end of input does not increase the counter.
*/
@Test
public void testExecutionContext() throws Exception {
final int NUMBER_OF_RECORDS = 2;
source.open(executionContext);
source.update(executionContext);
for (int i = 0; i < NUMBER_OF_RECORDS; i++) {
int recordCount = extractRecordCount();
assertEquals(i, recordCount);
source.read();
source.update(executionContext);
}
assertEquals(NUMBER_OF_RECORDS, extractRecordCount());
source.read();
assertEquals(NUMBER_OF_RECORDS, extractRecordCount());
}
private int extractRecordCount() {
return executionContext.getInt(ClassUtils.getShortName(StaxEventItemReader.class) + ".read.count");
}
@Test
public void testCloseWithoutOpen() throws Exception {
source.close();
// No error!
}
@Test
public void testClose() throws Exception {
MockStaxEventItemReader newSource = new MockStaxEventItemReader();
Resource resource = new ByteArrayResource(xml.getBytes());
newSource.setResource(resource);
newSource.setFragmentRootElementName(FRAGMENT_ROOT_ELEMENT);
newSource.setUnmarshaller(unmarshaller);
newSource.open(executionContext);
Object item = newSource.read();
assertNotNull(item);
assertTrue(newSource.isOpenCalled());
newSource.close();
newSource.setOpenCalled(false);
// calling read again should require re-initialization because of close
try {
newSource.read();
fail("Expected ReaderNotOpenException");
}
catch (Exception e) {
// expected
}
}
@Test
public void testOpenBadIOInput() throws Exception {
source.setResource(new AbstractResource() {
@Override
public String getDescription() {
return null;
}
@Override
public InputStream getInputStream() throws IOException {
throw new IOException();
}
@Override
public boolean exists() {
return true;
}
});
try {
source.open(executionContext);
fail();
}
catch (ItemStreamException ex) {
// expected
}
// read() should then return a null
assertNull(source.read());
source.close();
}
@Test
public void testNonExistentResource() throws Exception {
source.setResource(new NonExistentResource());
source.afterPropertiesSet();
source.setStrict(false);
source.open(executionContext);
assertNull(source.read());
}
@Test
public void testDirectoryResource() throws Exception {
FileSystemResource resource = new FileSystemResource("build/data");
resource.getFile().mkdirs();
assertTrue(resource.getFile().isDirectory());
source.setResource(resource);
source.afterPropertiesSet();
source.setStrict(false);
source.open(executionContext);
assertNull(source.read());
}
@Test
public void testRuntimeFileCreation() throws Exception {
source.setResource(new NonExistentResource());
source.afterPropertiesSet();
source.setResource(new ByteArrayResource(xml.getBytes()));
source.open(executionContext);
source.read();
}
@Test(expected = ItemStreamException.class)
public void testStrictness() throws Exception {
source.setResource(new NonExistentResource());
source.setStrict(true);
source.afterPropertiesSet();
source.open(executionContext);
}
/**
* Make sure the reader doesn't end up in inconsistent state if there's an error during unmarshalling (BATCH-1738).
* After an error during <code>read</code> the next <code>read</code> call should continue with reading the next
* fragment.
*/
@Test
public void exceptionDuringUnmarshalling() throws Exception {
source.setUnmarshaller(new TroublemakerUnmarshaller());
source.afterPropertiesSet();
source.open(executionContext);
try {
source.read();
fail();
}
catch (UnmarshallingFailureException expected) {
assert expected.getMessage() == TroublemakerUnmarshaller.MESSAGE;
}
try {
source.read();
fail();
}
catch (UnmarshallingFailureException expected) {
assert expected.getMessage() == TroublemakerUnmarshaller.MESSAGE;
}
assertNull(source.read());
}
/**
* Stub emulating problems during unmarshalling.
*/
private static class TroublemakerUnmarshaller implements Unmarshaller {
public static final String MESSAGE = "Unmarshallers on strike.";
@Override
public Object unmarshal(Source source) throws XmlMappingException, IOException {
throw new UnmarshallingFailureException(MESSAGE);
}
@Override
public boolean supports(Class<?> clazz) {
return true;
}
}
private StaxEventItemReader<List<XMLEvent>> createNewInputSouce() {
Resource resource = new ByteArrayResource(xml.getBytes());
StaxEventItemReader<List<XMLEvent>> newSource = new StaxEventItemReader<List<XMLEvent>>();
newSource.setResource(resource);
newSource.setFragmentRootElementName(FRAGMENT_ROOT_ELEMENT);
newSource.setUnmarshaller(unmarshaller);
newSource.setSaveState(true);
return newSource;
}
private StaxEventItemReader<ItemCountAwareFragment> createNewItemCountAwareInputSouce() {
Resource resource = new ByteArrayResource(xml.getBytes());
StaxEventItemReader<ItemCountAwareFragment> newSource = new StaxEventItemReader<ItemCountAwareFragment>();
newSource.setResource(resource);
newSource.setFragmentRootElementName(FRAGMENT_ROOT_ELEMENT);
newSource.setUnmarshaller(new ItemCountAwareMockFragmentUnmarshaller());
newSource.setSaveState(true);
return newSource;
}
/**
* A simple XMLEvent unmarshaller mock - check for the start and end document events for the fragment root & end
* tags + skips the fragment contents.
*/
private static class MockFragmentUnmarshaller implements Unmarshaller {
/**
* Skips the XML fragment contents.
*/
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
&& fragmentName.equals(((EndElement) eventInsideFragment).getName())) {
break;
}
events.add(eventReader.nextEvent());
} while (eventInsideFragment != null);
return events;
}
@Override
public boolean supports(Class<?> clazz) {
return true;
}
/**
* A simple mapFragment implementation checking the StaxEventReaderItemReader basic read functionality.
*
* @param source
* @return list of the events from fragment body
*/
@Override
public Object unmarshal(Source source) throws XmlMappingException, IOException {
List<XMLEvent> fragmentContent;
try {
XMLEventReader eventReader = StaxUtils.getXmlEventReader(source);
// first event should be StartDocument
XMLEvent event1 = eventReader.nextEvent();
assertTrue(event1.isStartDocument());
// second should be StartElement of the fragment
XMLEvent event2 = eventReader.nextEvent();
assertTrue(event2.isStartElement());
assertTrue(isFragmentRootElement(EventHelper.startElementName(event2)));
QName fragmentName = ((StartElement) event2).getName();
// jump before the end of fragment
fragmentContent = readRecordsInsideFragment(eventReader, fragmentName);
// end of fragment
XMLEvent event3 = eventReader.nextEvent();
assertTrue(event3.isEndElement());
assertTrue(isFragmentRootElement(EventHelper.endElementName(event3)));
// EndDocument should follow the end of fragment
XMLEvent event4 = eventReader.nextEvent();
assertTrue(event4.isEndDocument());
}
catch (Exception e) {
throw new RuntimeException("Error occurred in FragmentDeserializer", e);
}
return fragmentContent;
}
private boolean isFragmentRootElement(String name) {
return FRAGMENT_ROOT_ELEMENT.equals(name) || Arrays.asList(MULTI_FRAGMENT_ROOT_ELEMENTS).contains(name);
}
}
@SuppressWarnings("unchecked")
private static class ItemCountAwareMockFragmentUnmarshaller extends MockFragmentUnmarshaller {
@Override
public Object unmarshal(Source source) throws XmlMappingException,
IOException {
List<XMLEvent> fragment = (List<XMLEvent>) super.unmarshal(source);
if(fragment != null) {
return new ItemCountAwareFragment(fragment);
} else {
return null;
}
}
}
private static class ItemCountAwareFragment implements ItemCountAware {
private int itemCount;
public ItemCountAwareFragment(List<XMLEvent> fragment) {
}
@Override
public void setItemCount(int count) {
this.itemCount = count;
}
public int getItemCount() {
return itemCount;
}
}
private static class MockStaxEventItemReader extends StaxEventItemReader<List<XMLEvent>> {
private boolean openCalled = false;
@Override
public void open(ExecutionContext executionContext) {
super.open(executionContext);
openCalled = true;
}
public boolean isOpenCalled() {
return openCalled;
}
public void setOpenCalled(boolean openCalled) {
this.openCalled = openCalled;
}
}
private static class NonExistentResource extends AbstractResource {
public NonExistentResource() {
}
@Override
public boolean exists() {
return false;
}
@Override
public String getDescription() {
return "NonExistantResource";
}
@Override
public InputStream getInputStream() throws IOException {
return null;
}
}
}