/* * Copyright 2002-2017 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.util.xml; import java.io.ByteArrayInputStream; import java.io.InputStream; import javax.xml.stream.XMLInputFactory; import javax.xml.stream.XMLStreamException; import javax.xml.transform.Transformer; import javax.xml.transform.TransformerFactory; import javax.xml.transform.dom.DOMResult; import javax.xml.transform.sax.SAXSource; import org.junit.Before; import org.junit.Test; import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; import org.w3c.dom.Node; import org.xml.sax.Attributes; import org.xml.sax.ContentHandler; import org.xml.sax.InputSource; import org.xml.sax.Locator; import org.xml.sax.XMLReader; import org.xml.sax.ext.LexicalHandler; import org.xml.sax.helpers.AttributesImpl; import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.Resource; import org.springframework.tests.MockitoUtils; import org.springframework.tests.MockitoUtils.InvocationArgumentsAdapter; import static org.junit.Assert.*; import static org.mockito.BDDMockito.*; /** * @author Arjen Poutsma */ public abstract class AbstractStaxXMLReaderTestCase { protected static XMLInputFactory inputFactory; private XMLReader standardReader; private ContentHandler standardContentHandler; @Before @SuppressWarnings("deprecation") // on JDK 9 public void setUp() throws Exception { inputFactory = XMLInputFactory.newInstance(); standardReader = org.xml.sax.helpers.XMLReaderFactory.createXMLReader(); standardContentHandler = mockContentHandler(); standardReader.setContentHandler(standardContentHandler); } @Test public void contentHandlerNamespacesNoPrefixes() throws Exception { standardReader.setFeature("http://xml.org/sax/features/namespaces", true); standardReader.setFeature("http://xml.org/sax/features/namespace-prefixes", false); standardReader.parse(new InputSource(createTestInputStream())); AbstractStaxXMLReader staxXmlReader = createStaxXmlReader(createTestInputStream()); ContentHandler contentHandler = mockContentHandler(); staxXmlReader.setFeature("http://xml.org/sax/features/namespaces", true); staxXmlReader.setFeature("http://xml.org/sax/features/namespace-prefixes", false); staxXmlReader.setContentHandler(contentHandler); staxXmlReader.parse(new InputSource()); verifyIdenticalInvocations(standardContentHandler, contentHandler); } @Test public void contentHandlerNamespacesPrefixes() throws Exception { standardReader.setFeature("http://xml.org/sax/features/namespaces", true); standardReader.setFeature("http://xml.org/sax/features/namespace-prefixes", true); standardReader.parse(new InputSource(createTestInputStream())); AbstractStaxXMLReader staxXmlReader = createStaxXmlReader(createTestInputStream()); ContentHandler contentHandler = mockContentHandler(); staxXmlReader.setFeature("http://xml.org/sax/features/namespaces", true); staxXmlReader.setFeature("http://xml.org/sax/features/namespace-prefixes", true); staxXmlReader.setContentHandler(contentHandler); staxXmlReader.parse(new InputSource()); verifyIdenticalInvocations(standardContentHandler, contentHandler); } @Test public void contentHandlerNoNamespacesPrefixes() throws Exception { standardReader.setFeature("http://xml.org/sax/features/namespaces", false); standardReader.setFeature("http://xml.org/sax/features/namespace-prefixes", true); standardReader.parse(new InputSource(createTestInputStream())); AbstractStaxXMLReader staxXmlReader = createStaxXmlReader(createTestInputStream()); ContentHandler contentHandler = mockContentHandler(); staxXmlReader.setFeature("http://xml.org/sax/features/namespaces", false); staxXmlReader.setFeature("http://xml.org/sax/features/namespace-prefixes", true); staxXmlReader.setContentHandler(contentHandler); staxXmlReader.parse(new InputSource()); verifyIdenticalInvocations(standardContentHandler, contentHandler); } @Test public void whitespace() throws Exception { String xml = "<?xml version=\"1.0\" encoding=\"UTF-8\"?><test><node1> </node1><node2> Some text </node2></test>"; Transformer transformer = TransformerFactory.newInstance().newTransformer(); AbstractStaxXMLReader staxXmlReader = createStaxXmlReader( new ByteArrayInputStream(xml.getBytes("UTF-8"))); SAXSource source = new SAXSource(staxXmlReader, new InputSource()); DOMResult result = new DOMResult(); transformer.transform(source, result); Node node1 = result.getNode().getFirstChild().getFirstChild(); assertEquals(" ", node1.getTextContent()); assertEquals(" Some text ", node1.getNextSibling().getTextContent()); } @Test public void lexicalHandler() throws Exception { Resource testLexicalHandlerXml = new ClassPathResource("testLexicalHandler.xml", getClass()); LexicalHandler expectedLexicalHandler = mockLexicalHandler(); standardReader.setContentHandler(null); standardReader.setProperty("http://xml.org/sax/properties/lexical-handler", expectedLexicalHandler); standardReader.parse(new InputSource(testLexicalHandlerXml.getInputStream())); inputFactory.setProperty("javax.xml.stream.isCoalescing", Boolean.FALSE); inputFactory.setProperty("http://java.sun.com/xml/stream/properties/report-cdata-event", Boolean.TRUE); inputFactory.setProperty("javax.xml.stream.isReplacingEntityReferences", Boolean.FALSE); inputFactory.setProperty("javax.xml.stream.isSupportingExternalEntities", Boolean.FALSE); LexicalHandler actualLexicalHandler = mockLexicalHandler(); willAnswer(invocation -> invocation.getArguments()[0] = "element"). given(actualLexicalHandler).startDTD(anyString(), anyString(), anyString()); AbstractStaxXMLReader staxXmlReader = createStaxXmlReader(testLexicalHandlerXml.getInputStream()); staxXmlReader.setProperty("http://xml.org/sax/properties/lexical-handler", actualLexicalHandler); staxXmlReader.parse(new InputSource()); // TODO: broken comparison since Mockito 2.2 upgrade // verifyIdenticalInvocations(expectedLexicalHandler, actualLexicalHandler); } private LexicalHandler mockLexicalHandler() throws Exception { LexicalHandler lexicalHandler = mock(LexicalHandler.class); willAnswer(new CopyCharsAnswer()).given(lexicalHandler).comment(any(char[].class), anyInt(), anyInt()); return lexicalHandler; } private InputStream createTestInputStream() { return getClass().getResourceAsStream("testContentHandler.xml"); } protected final ContentHandler mockContentHandler() throws Exception { ContentHandler contentHandler = mock(ContentHandler.class); willAnswer(new CopyCharsAnswer()).given(contentHandler).characters(any(char[].class), anyInt(), anyInt()); willAnswer(new CopyCharsAnswer()).given(contentHandler).ignorableWhitespace(any(char[].class), anyInt(), anyInt()); willAnswer(new Answer<Object>() { @Override public Object answer(InvocationOnMock invocation) throws Throwable { invocation.getArguments()[3] = new AttributesImpl((Attributes) invocation.getArguments()[3]); return null; } }).given(contentHandler).startElement(anyString(), anyString(), anyString(), any(Attributes.class)); return contentHandler; } protected <T> void verifyIdenticalInvocations(T expected, T actual) { MockitoUtils.verifySameInvocations(expected, actual, new SkipLocatorArgumentsAdapter(), new CharArrayToStringAdapter(), new PartialAttributesAdapter()); } protected abstract AbstractStaxXMLReader createStaxXmlReader(InputStream inputStream) throws XMLStreamException; private static class SkipLocatorArgumentsAdapter implements InvocationArgumentsAdapter { @Override public Object[] adaptArguments(Object[] arguments) { for (int i = 0; i < arguments.length; i++) { if (arguments[i] instanceof Locator) { arguments[i] = null; } } return arguments; } } private static class CharArrayToStringAdapter implements InvocationArgumentsAdapter { @Override public Object[] adaptArguments(Object[] arguments) { if (arguments.length == 3 && arguments[0] instanceof char[] && arguments[1] instanceof Integer && arguments[2] instanceof Integer) { return new Object[] {new String((char[]) arguments[0], (Integer) arguments[1], (Integer) arguments[2])}; } return arguments; } } private static class PartialAttributesAdapter implements InvocationArgumentsAdapter { @Override public Object[] adaptArguments(Object[] arguments) { for (int i = 0; i < arguments.length; i++) { if (arguments[i] instanceof Attributes) { arguments[i] = new PartialAttributes((Attributes) arguments[i]); } }; return arguments; } } private static class CopyCharsAnswer implements Answer<Object> { @Override public Object answer(InvocationOnMock invocation) throws Throwable { char[] chars = (char[]) invocation.getArguments()[0]; char[] copy = new char[chars.length]; System.arraycopy(chars, 0, copy, 0, chars.length); invocation.getArguments()[0] = copy; return null; } } private static class PartialAttributes { private final Attributes attributes; public PartialAttributes(Attributes attributes) { this.attributes = attributes; } @Override public boolean equals(Object obj) { Attributes other = ((PartialAttributes) obj).attributes; if (this.attributes.getLength() != other.getLength()) { return false; } for (int i = 0; i < other.getLength(); i++) { boolean found = false; for (int j = 0; j < attributes.getLength(); j++) { if (other.getURI(i).equals(attributes.getURI(j)) && other.getQName(i).equals(attributes.getQName(j)) && other.getType(i).equals(attributes.getType(j)) && other.getValue(i).equals(attributes.getValue(j))) { found = true; break; } } if (!found) { return false; } } return true; } @Override public int hashCode() { return 1; } } }