| /* |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you 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.apache.axiom.testutils.stax; |
| |
| import static com.google.common.truth.Truth.assertThat; |
| import static org.junit.Assert.assertEquals; |
| import static org.junit.Assert.fail; |
| |
| import java.lang.reflect.InvocationTargetException; |
| import java.lang.reflect.Method; |
| import java.util.Arrays; |
| import java.util.HashMap; |
| import java.util.HashSet; |
| import java.util.Iterator; |
| import java.util.LinkedList; |
| import java.util.Map; |
| import java.util.NoSuchElementException; |
| import java.util.Set; |
| |
| import javax.xml.namespace.NamespaceContext; |
| import javax.xml.namespace.QName; |
| import javax.xml.stream.XMLStreamReader; |
| |
| /** |
| * Helper class that compares the events produced by two {@link XMLStreamReader} objects. Note that |
| * this class is not meant to be used to compare two XML documents (the error reporting would not be |
| * clear enough for that purpose), but to validate implementations of the {@link XMLStreamReader} |
| * interface. It uses a brute force approach: for each event, all methods (that don't modify the |
| * reader state) are called on both readers and the results (return values or exceptions thrown) of |
| * these invocations are compared to each other. |
| */ |
| public class XMLStreamReaderComparator { |
| private XMLStreamReader expected; |
| private XMLStreamReader actual; |
| private boolean compareInternalSubset = true; |
| private boolean compareEntityReplacementValue = true; |
| private boolean compareCharacterEncodingScheme = true; |
| private boolean compareEncoding = true; |
| private boolean sortAttributes = false; |
| private boolean treatSpaceAsCharacters = false; |
| private final LinkedList<QName> path = new LinkedList<>(); |
| |
| /** |
| * Set collecting all prefixes seen in the document to be able to test {@link |
| * XMLStreamReader#getNamespaceURI(String)}. |
| */ |
| private final Set<String> prefixes = new HashSet<>(); |
| |
| /** |
| * Set collecting all namespace URIs seen in the document to be able to test {@link |
| * NamespaceContext#getPrefix(String)}. |
| */ |
| private final Set<String> namespaceURIs = new HashSet<>(); |
| |
| public XMLStreamReaderComparator(XMLStreamReader expected, XMLStreamReader actual) { |
| this.expected = expected; |
| this.actual = actual; |
| } |
| |
| private String getLocation() { |
| StringBuffer buffer = new StringBuffer(); |
| buffer.append("event type "); |
| buffer.append(expected.getEventType()); |
| buffer.append("; location "); |
| for (QName qname : path) { |
| buffer.append('/'); |
| buffer.append(qname); |
| } |
| return buffer.toString(); |
| } |
| |
| private <T> InvocationResults<T> invoke( |
| Class<T> returnType, String methodName, Class<?>[] paramTypes, Object[] args) |
| throws Exception { |
| Method method = XMLStreamReader.class.getMethod(methodName, paramTypes); |
| |
| T expectedResult; |
| Throwable expectedException; |
| try { |
| expectedResult = returnType.cast(method.invoke(expected, args)); |
| expectedException = null; |
| } catch (InvocationTargetException ex) { |
| expectedResult = null; |
| expectedException = ex.getCause(); |
| } |
| |
| T actualResult; |
| Throwable actualException; |
| try { |
| actualResult = returnType.cast(method.invoke(actual, args)); |
| actualException = null; |
| } catch (InvocationTargetException ex) { |
| actualResult = null; |
| actualException = ex.getCause(); |
| } |
| |
| if (expectedException == null) { |
| if (actualException != null) { |
| actualException.printStackTrace(System.out); |
| fail( |
| "Method " |
| + methodName |
| + " threw unexpected exception " |
| + actualException.getClass().getName() |
| + " (" |
| + getLocation() |
| + ")"); |
| } else { |
| return new InvocationResults<>(expectedResult, actualResult); |
| } |
| } else { |
| if (actualException == null) { |
| fail( |
| "Expected " |
| + methodName |
| + " to throw " |
| + expectedException.getClass().getName() |
| + ", but the method returned normally (" |
| + getLocation() |
| + ")"); |
| } else { |
| assertEquals( |
| "Unexpected exception thrown by " + methodName, |
| expectedException.getClass(), |
| actualException.getClass()); |
| } |
| } |
| return null; |
| } |
| |
| private <T> InvocationResults<T> invoke(Class<T> returnType, String methodName) |
| throws Exception { |
| return invoke(returnType, methodName, new Class[0], new Object[0]); |
| } |
| |
| private <T> T assertSameResult( |
| Class<T> returnType, |
| String methodName, |
| Class<?>[] paramTypes, |
| Object[] args, |
| Normalizer<T> normalizer) |
| throws Exception { |
| |
| InvocationResults<T> results = invoke(returnType, methodName, paramTypes, args); |
| if (results != null) { |
| T expected = |
| normalizer == null |
| ? results.getExpected() |
| : normalizer.normalize(results.getExpected()); |
| T actual = |
| normalizer == null |
| ? results.getActual() |
| : normalizer.normalize(results.getActual()); |
| assertEquals( |
| "Return value of " |
| + methodName |
| + " for arguments " |
| + Arrays.asList(args) |
| + " (" |
| + getLocation() |
| + ")", |
| expected, |
| actual); |
| return results.getExpected(); |
| } else { |
| return null; |
| } |
| } |
| |
| private <T> T assertSameResult( |
| Class<T> returnType, String methodName, Class<?>[] paramTypes, Object[] args) |
| throws Exception { |
| return assertSameResult(returnType, methodName, paramTypes, args, null); |
| } |
| |
| private <T> T assertSameResult(Class<T> returnType, String methodName, Normalizer<T> normalizer) |
| throws Exception { |
| return assertSameResult(returnType, methodName, new Class[0], new Object[0], normalizer); |
| } |
| |
| private <T> T assertSameResult(Class<T> returnType, String methodName) throws Exception { |
| return assertSameResult(returnType, methodName, null); |
| } |
| |
| private Set<String> toPrefixSet(Iterator<?> it) { |
| Set<String> set = new HashSet<>(); |
| while (it.hasNext()) { |
| String prefix = (String) it.next(); |
| // TODO: Woodstox returns null instead of "" for the default namespace. |
| // This seems incorrect, but the javax.namespace.NamespaceContext specs are |
| // not very clear. |
| set.add(prefix == null ? "" : prefix); |
| } |
| return set; |
| } |
| |
| private void compareNamespaceContexts(NamespaceContext expected, NamespaceContext actual) { |
| for (String prefix : prefixes) { |
| if (prefix != null) { |
| assertEquals( |
| "Namespace URI for prefix '" + prefix + "' (" + getLocation() + ")", |
| expected.getNamespaceURI(prefix), |
| actual.getNamespaceURI(prefix)); |
| } |
| } |
| for (String namespaceURI : namespaceURIs) { |
| if (namespaceURI != null && namespaceURI.length() > 0) { |
| Set<String> prefixes = toPrefixSet(expected.getPrefixes(namespaceURI)); |
| assertEquals( |
| "Prefixes for namespace URI '" + namespaceURI + "' (" + getLocation() + ")", |
| prefixes, |
| toPrefixSet(actual.getPrefixes(namespaceURI))); |
| if (prefixes.size() <= 1) { |
| assertEquals( |
| "Prefix for namespace URI '" |
| + namespaceURI |
| + "' (" |
| + getLocation() |
| + ")", |
| expected.getPrefix(namespaceURI), |
| actual.getPrefix(namespaceURI)); |
| } else { |
| assertThat(actual.getPrefix(namespaceURI)).isIn(prefixes); |
| } |
| } |
| } |
| } |
| |
| /** |
| * Add a prefix that should be used in testing the {@link |
| * XMLStreamReader#getNamespaceURI(String)} method. |
| * |
| * @param prefix the prefix to add |
| */ |
| public void addPrefix(String prefix) { |
| prefixes.add(prefix); |
| } |
| |
| public void setCompareInternalSubset(boolean compareInternalSubset) { |
| this.compareInternalSubset = compareInternalSubset; |
| } |
| |
| /** |
| * Specify whether the replacement value for entity references (as reported by {@link |
| * XMLStreamReader#getText()}) should be compared. The default value for this option is <code> |
| * true</code>. |
| * |
| * @param value <code>true</code> if the replacement value should be compared; <code>false |
| * </code> if replacement values for entity references are ignored |
| */ |
| public void setCompareEntityReplacementValue(boolean value) { |
| compareEntityReplacementValue = value; |
| } |
| |
| public void setCompareCharacterEncodingScheme(boolean value) { |
| compareCharacterEncodingScheme = value; |
| } |
| |
| public void setCompareEncoding(boolean value) { |
| compareEncoding = value; |
| } |
| |
| public void setSortAttributes(boolean sortAttributes) { |
| this.sortAttributes = sortAttributes; |
| } |
| |
| public void setTreatSpaceAsCharacters(boolean treatSpaceAsCharacters) { |
| this.treatSpaceAsCharacters = treatSpaceAsCharacters; |
| } |
| |
| public void compare() throws Exception { |
| if (sortAttributes) { |
| expected = new AttributeSortingXMLStreamReaderFilter(expected); |
| actual = new AttributeSortingXMLStreamReaderFilter(actual); |
| } |
| if (treatSpaceAsCharacters) { |
| expected = new SpaceAsCharactersXMLStreamReaderFilter(expected); |
| actual = new SpaceAsCharactersXMLStreamReaderFilter(actual); |
| } |
| while (true) { |
| int eventType = assertSameResult(Integer.class, "getEventType"); |
| if (eventType == XMLStreamReader.START_ELEMENT) { |
| path.addLast(expected.getName()); |
| } |
| if (compareCharacterEncodingScheme) { |
| assertSameResult(String.class, "getCharacterEncodingScheme"); |
| } |
| if (compareEncoding) { |
| assertSameResult(String.class, "getEncoding", Normalizer.LOWER_CASE); |
| } |
| Integer attributeCount = assertSameResult(Integer.class, "getAttributeCount"); |
| // Test the behavior of the getAttributeXxx methods for all types of events, |
| // to check that an appropriate exception is thrown for events other than |
| // START_ELEMENT |
| for (int i = 0; i < (attributeCount == null ? 1 : attributeCount.intValue()); i++) { |
| Class<?>[] paramTypes = {Integer.TYPE}; |
| Object[] args = {new Integer(i)}; |
| assertSameResult(String.class, "getAttributeLocalName", paramTypes, args); |
| assertSameResult(QName.class, "getAttributeName", paramTypes, args); |
| namespaceURIs.add( |
| assertSameResult(String.class, "getAttributeNamespace", paramTypes, args)); |
| prefixes.add( |
| assertSameResult( |
| String.class, |
| "getAttributePrefix", |
| paramTypes, |
| args, |
| Normalizer.EMPTY_STRING_TO_NULL)); |
| assertSameResult(String.class, "getAttributeType", paramTypes, args); |
| assertSameResult(String.class, "getAttributeValue", paramTypes, args); |
| assertSameResult(Boolean.class, "isAttributeSpecified", paramTypes, args); |
| } |
| if (attributeCount != null) { |
| for (int i = 0; i < attributeCount.intValue(); i++) { |
| QName qname = expected.getAttributeName(i); |
| assertSameResult( |
| String.class, |
| "getAttributeValue", |
| new Class[] {String.class, String.class}, |
| new Object[] {qname.getNamespaceURI(), qname.getLocalPart()}); |
| } |
| } |
| assertSameResult(String.class, "getLocalName"); |
| assertSameResult(QName.class, "getName"); |
| Integer namespaceCount = assertSameResult(Integer.class, "getNamespaceCount"); |
| if (namespaceCount != null) { |
| Map<String, String> expectedNamespaces = new HashMap<>(); |
| Map<String, String> actualNamespaces = new HashMap<>(); |
| for (int i = 0; i < namespaceCount.intValue(); i++) { |
| String expectedPrefix = expected.getNamespacePrefix(i); |
| String expectedNamespaceURI = expected.getNamespaceURI(i); |
| if (expectedNamespaceURI != null && expectedNamespaceURI.length() == 0) { |
| expectedNamespaceURI = null; |
| } |
| String actualPrefix = actual.getNamespacePrefix(i); |
| String actualNamespaceURI = actual.getNamespaceURI(i); |
| if (actualNamespaceURI != null && actualNamespaceURI.length() == 0) { |
| actualNamespaceURI = null; |
| } |
| expectedNamespaces.put(expectedPrefix, expectedNamespaceURI); |
| actualNamespaces.put(actualPrefix, actualNamespaceURI); |
| prefixes.add(expectedPrefix); |
| namespaceURIs.add(expectedNamespaceURI); |
| } |
| assertEquals(expectedNamespaces, actualNamespaces); |
| } |
| namespaceURIs.add(assertSameResult(String.class, "getNamespaceURI")); |
| assertSameResult(String.class, "getPIData"); |
| assertSameResult(String.class, "getPITarget"); |
| prefixes.add(assertSameResult(String.class, "getPrefix")); |
| if ((eventType != XMLStreamReader.DTD || compareInternalSubset) |
| && (eventType != XMLStreamReader.ENTITY_REFERENCE |
| || compareEntityReplacementValue)) { |
| assertSameResult( |
| String.class, |
| "getText", |
| eventType == XMLStreamReader.DTD ? Normalizer.DTD : null); |
| } |
| Integer textLength = assertSameResult(Integer.class, "getTextLength"); |
| InvocationResults<Integer> textStart = invoke(Integer.class, "getTextStart"); |
| InvocationResults<char[]> textCharacters = invoke(char[].class, "getTextCharacters"); |
| if (textLength != null) { |
| assertEquals( |
| new String( |
| textCharacters.getExpected(), textStart.getExpected(), textLength), |
| new String(textCharacters.getActual(), textStart.getActual(), textLength)); |
| } |
| assertSameResult(Boolean.class, "hasName"); |
| assertSameResult(Boolean.class, "hasText"); |
| assertSameResult(Boolean.class, "isCharacters"); |
| assertSameResult(Boolean.class, "isEndElement"); |
| assertSameResult(Boolean.class, "isStartElement"); |
| assertSameResult(Boolean.class, "isWhiteSpace"); |
| |
| // Only check getNamespaceURI(String) for START_ELEMENT and END_ELEMENT. The Javadoc |
| // of XMLStreamReader suggests that this method is valid for all states, but Woodstox |
| // only allows it for some states. |
| if (eventType == XMLStreamReader.START_ELEMENT |
| || eventType == XMLStreamReader.END_ELEMENT) { |
| for (String prefix : prefixes) { |
| // The StAX specs are not clear about the expected result of getNamespaceURI |
| // when called with prefix "xml" (which doesn't require an explicit declaration) |
| if (prefix != null && !prefix.equals("xml")) { |
| assertSameResult( |
| String.class, |
| "getNamespaceURI", |
| new Class[] {String.class}, |
| new Object[] {prefix}); |
| } |
| } |
| } |
| |
| compareNamespaceContexts(expected.getNamespaceContext(), actual.getNamespaceContext()); |
| |
| if (eventType == XMLStreamReader.END_ELEMENT) { |
| path.removeLast(); |
| } |
| |
| assertSameResult(Boolean.class, "hasNext"); |
| |
| int expectedNextEvent; |
| try { |
| expectedNextEvent = expected.next(); |
| } catch (IllegalStateException ex) { |
| expectedNextEvent = -1; |
| } catch (NoSuchElementException ex) { |
| expectedNextEvent = -1; |
| } |
| if (expectedNextEvent == -1) { |
| try { |
| actual.next(); |
| } catch (IllegalStateException ex) { |
| break; |
| } catch (NoSuchElementException ex) { |
| break; |
| } |
| fail("Expected reader to throw IllegalStateException or NoSuchElementException"); |
| } else { |
| assertEquals("Event type at " + getLocation(), expectedNextEvent, actual.next()); |
| } |
| } |
| } |
| } |