Allow StaxEventItemReader to auto-detect the input file encoding

Before this commit, it was not possible to pass a null encoding
to the StaxEventItemReader, which prevents the XML event reader
to auto-detect the file encoding.

This commits makes the encoding setter more lenient by accepting
a null value.

Resolves #4101
This commit is contained in:
Mahmoud Ben Hassine
2023-04-27 20:52:37 +02:00
parent 81e619b433
commit 9874a6502c
3 changed files with 83 additions and 44 deletions

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2006-2020 the original author or authors.
* Copyright 2006-2023 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.
@@ -49,13 +49,13 @@ import org.springframework.util.xml.StaxUtils;
/**
* Item reader for reading XML input based on StAX.
*
*
* It extracts fragments from the input XML document which correspond to records for processing. The fragments are
* wrapped with StartDocument and EndDocument events so that the fragments can be further processed like standalone XML
* documents.
*
*
* The implementation is <b>not</b> thread-safe.
*
*
* @author Robert Kasanicky
* @author Mahmoud Ben Hassine
*/
@@ -140,16 +140,16 @@ ResourceAwareItemReaderItemStream<T>, InitializingBean {
/**
* Set encoding to be used for the input file. Defaults to {@link #DEFAULT_ENCODING}.
*
* @param encoding the encoding to be used
* @param encoding the encoding to be used. Can be {@code null}, in which case, the
* XML event reader will attempt to auto-detect the encoding from the input file.
*/
public void setEncoding(String encoding) {
Assert.notNull(encoding, "The encoding must not be null");
public void setEncoding(@Nullable String encoding) {
this.encoding = encoding;
}
/**
* Ensure that all required dependencies for the ItemReader to run are provided after all properties have been set.
*
*
* @see org.springframework.beans.factory.InitializingBean#afterPropertiesSet()
* @throws IllegalArgumentException if the Resource, FragmentDeserializer or FragmentRootElementName is null, or if
* the root element is empty.
@@ -161,19 +161,19 @@ ResourceAwareItemReaderItemStream<T>, InitializingBean {
Assert.notEmpty(fragmentRootElementNames, "The FragmentRootElementNames must not be empty");
for (QName fragmentRootElementName : fragmentRootElementNames) {
Assert.hasText(fragmentRootElementName.getLocalPart(), "The FragmentRootElementNames must not contain empty elements");
}
}
}
/**
* Responsible for moving the cursor before the StartElement of the fragment root.
*
*
* This implementation simply looks for the next corresponding element, it does not care about element nesting. You
* will need to override this method to correctly handle composite fragments.
*
* @param reader the {@link XMLEventReader} to be used to find next fragment.
*
*
* @return <code>true</code> if next fragment was found, <code>false</code> otherwise.
*
*
* @throws NonTransientResourceException if the cursor could not be moved. This will be treated as fatal and
* subsequent calls to read will return null.
*/
@@ -237,7 +237,8 @@ ResourceAwareItemReaderItemStream<T>, InitializingBean {
}
inputStream = resource.getInputStream();
eventReader = xmlInputFactory.createXMLEventReader(inputStream, this.encoding);
eventReader = this.encoding != null ? xmlInputFactory.createXMLEventReader(inputStream, this.encoding)
: xmlInputFactory.createXMLEventReader(inputStream);
fragmentReader = new DefaultFragmentEventReader(eventReader);
noInput = false;
@@ -332,19 +333,19 @@ ResourceAwareItemReaderItemStream<T>, InitializingBean {
}
}
}
protected boolean isFragmentRootElementName(QName name) {
for (QName fragmentRootElementName : fragmentRootElementNames) {
if (fragmentRootElementName.getLocalPart().equals(name.getLocalPart())) {
if (!StringUtils.hasText(fragmentRootElementName.getNamespaceURI())
|| fragmentRootElementName.getNamespaceURI().equals(name.getNamespaceURI())) {
|| fragmentRootElementName.getNamespaceURI().equals(name.getNamespaceURI())) {
return true;
}
}
}
return false;
}
}
private QName parseFragmentRootElementName(String fragmentRootElementName) {
String name = fragmentRootElementName;
String nameSpace = null;
@@ -354,5 +355,5 @@ ResourceAwareItemReaderItemStream<T>, InitializingBean {
}
return new QName(nameSpace, name, "");
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2017-2020 the original author or authors.
* Copyright 2017-2023 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.
@@ -26,6 +26,7 @@ import org.apache.commons.logging.LogFactory;
import org.springframework.batch.item.xml.StaxEventItemReader;
import org.springframework.core.io.Resource;
import org.springframework.lang.Nullable;
import org.springframework.oxm.Unmarshaller;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
@@ -203,13 +204,14 @@ public class StaxEventItemReaderBuilder<T> {
}
/**
* Encoding for the input file. Defaults to {@link StaxEventItemReader#DEFAULT_ENCODING}.
*
* Encoding for the input file. Defaults to
* {@link StaxEventItemReader#DEFAULT_ENCODING}. Can be {@code null}, in which case
* the XML event reader will attempt to auto-detect the encoding from the input file.
* @param encoding String encoding algorithm
* @return the current instance of the builder
* @see StaxEventItemReader#setEncoding(String)
*/
public StaxEventItemReaderBuilder<T> encoding(String encoding) {
public StaxEventItemReaderBuilder<T> encoding(@Nullable String encoding) {
this.encoding = encoding;
return this;

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2008-2020 the original author or authors.
* Copyright 2008-2023 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.
@@ -36,8 +36,10 @@ import org.springframework.util.xml.StaxUtils;
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.StartDocument;
import javax.xml.stream.events.StartElement;
import javax.xml.stream.events.XMLEvent;
import javax.xml.transform.Source;
@@ -57,10 +59,13 @@ import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
/**
* Tests for {@link StaxEventItemReader}.
*
*
* @author Robert Kasanicky
* @author Michael Minella
* @author Mahmoud Ben Hassine
@@ -94,7 +99,7 @@ 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;
@@ -170,6 +175,37 @@ public class StaxEventItemReaderTests {
source.close();
}
@Test
public void testNullEncoding() throws Exception {
// given
XMLEventReader eventReader = mock(XMLEventReader.class);
when(eventReader.peek()).thenReturn(mock(StartDocument.class));
Resource resource = mock(Resource.class);
InputStream inputStream = mock(InputStream.class);
when(resource.getInputStream()).thenReturn(inputStream);
when(resource.isReadable()).thenReturn(true);
when(resource.exists()).thenReturn(true);
XMLInputFactory xmlInputFactory = mock(XMLInputFactory.class);
when(xmlInputFactory.createXMLEventReader(inputStream)).thenReturn(eventReader);
StaxEventItemReader<Object> reader = new StaxEventItemReader<>();
reader.setUnmarshaller(new MockFragmentUnmarshaller());
reader.setFragmentRootElementName(FRAGMENT_ROOT_ELEMENT);
reader.setResource(resource);
reader.setEncoding(null);
reader.setStrict(false);
reader.setXmlInputFactory(xmlInputFactory);
reader.afterPropertiesSet();
// when
reader.open(new ExecutionContext());
// then
verify(xmlInputFactory).createXMLEventReader(inputStream);
reader.close();
}
@Test
public void testItemCountAwareFragment() throws Exception {
StaxEventItemReader<ItemCountAwareFragment> source = createNewItemCountAwareInputSource();
@@ -247,7 +283,7 @@ public class StaxEventItemReaderTests {
source.close();
}
@Test
public void testMultiFragment() throws Exception {
@@ -262,7 +298,7 @@ public class StaxEventItemReaderTests {
assertNull(source.read()); // there are only three fragments
source.close();
}
}
@Test
public void testMultiFragmentNameSpace() throws Exception {
@@ -277,7 +313,7 @@ public class StaxEventItemReaderTests {
assertNull(source.read()); // there are only two fragments (one has wrong namespace)
source.close();
}
}
@Test
public void testMultiFragmentRestart() throws Exception {
@@ -289,23 +325,23 @@ public class StaxEventItemReaderTests {
// see asserts in the mock unmarshaller
assertNotNull(source.read());
assertNotNull(source.read());
source.update(executionContext);
source.update(executionContext);
assertEquals(2, executionContext.getInt(ClassUtils.getShortName(StaxEventItemReader.class) + ".read.count"));
source.close();
source = createNewInputSource();
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 {
@@ -322,7 +358,7 @@ public class StaxEventItemReaderTests {
source.close();
}
@Test
public void testMultiFragmentNestedRestart() throws Exception {
@@ -333,24 +369,24 @@ public class StaxEventItemReaderTests {
// see asserts in the mock unmarshaller
assertNotNull(source.read());
assertNotNull(source.read());
source.update(executionContext);
source.update(executionContext);
assertEquals(2, executionContext.getInt(ClassUtils.getShortName(StaxEventItemReader.class) + ".read.count"));
source.close();
source = createNewInputSource();
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.
*/
@@ -714,7 +750,7 @@ public class StaxEventItemReaderTests {
/**
* A simple mapFragment implementation checking the StaxEventReaderItemReader basic read functionality.
*
*
* @param source
* @return list of the events from fragment body
*/
@@ -753,7 +789,7 @@ public class StaxEventItemReaderTests {
}
return fragmentContent;
}
private boolean isFragmentRootElement(String name) {
return FRAGMENT_ROOT_ELEMENT.equals(name) || Arrays.asList(MULTI_FRAGMENT_ROOT_ELEMENTS).contains(name);
}