/*
* 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;
}
}
}