From 9874a6502cbef90df3938bb3d37c0b76ed26d421 Mon Sep 17 00:00:00 2001 From: Mahmoud Ben Hassine Date: Thu, 27 Apr 2023 20:52:37 +0200 Subject: [PATCH] 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 --- .../batch/item/xml/StaxEventItemReader.java | 37 ++++----- .../builder/StaxEventItemReaderBuilder.java | 10 ++- .../item/xml/StaxEventItemReaderTests.java | 80 ++++++++++++++----- 3 files changed, 83 insertions(+), 44 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 9da16b274..6be9f926d 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 @@ -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 not thread-safe. - * + * * @author Robert Kasanicky * @author Mahmoud Ben Hassine */ @@ -140,16 +140,16 @@ ResourceAwareItemReaderItemStream, 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, 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 true if next fragment was found, false 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, 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, 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, InitializingBean { } return new QName(nameSpace, name, ""); } - + } diff --git a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/xml/builder/StaxEventItemReaderBuilder.java b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/xml/builder/StaxEventItemReaderBuilder.java index 5f9c7bb36..acf9084a3 100644 --- a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/xml/builder/StaxEventItemReaderBuilder.java +++ b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/xml/builder/StaxEventItemReaderBuilder.java @@ -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 { } /** - * 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 encoding(String encoding) { + public StaxEventItemReaderBuilder encoding(@Nullable String encoding) { this.encoding = encoding; return this; 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 0737324e3..0f643dab9 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 @@ -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 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 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); }