blob: 417260ac841ff6d24f4e40181976d3bb265634a0 [file] [log] [blame]
/*
* 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.truth.xml;
import static com.google.common.truth.Truth.assertThat;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import javax.xml.namespace.QName;
import org.apache.axiom.truth.xml.spi.Event;
import org.apache.axiom.truth.xml.spi.Traverser;
import org.apache.axiom.truth.xml.spi.TraverserException;
import org.apache.axiom.truth.xml.spi.XML;
import com.google.common.truth.FailureMetadata;
import com.google.common.truth.Subject;
/**
* Propositions for objects representing XML data.
*/
public final class XMLSubject extends Subject {
private final XML xml;
private boolean ignoreComments;
private boolean ignoreElementContentWhitespace;
private boolean ignoreWhitespace;
private boolean ignoreWhitespaceInPrologAndEpilog;
private boolean ignorePrologAndEpilog;
private boolean ignoreNamespaceDeclarations;
private boolean ignoreNamespacePrefixes;
private boolean ignoreRedundantNamespaceDeclarations;
private boolean expandEntityReferences;
private boolean treatWhitespaceAsText;
XMLSubject(FailureMetadata metadata, Object subject) {
super(metadata, subject);
xml = XMLTruth.xml(subject);
}
/**
* Ignore comments; same as {@code ignoringComments(true)}.
*
* @return {@code this}
*/
public XMLSubject ignoringComments() {
return ignoringComments(true);
}
/**
* Specifies if comments should be ignored.
*
* @param value
* {@code true} if comments should be ignored, {@code false} otherwise
* @return {@code this}
*/
public XMLSubject ignoringComments(boolean value) {
ignoreComments = value;
return this;
}
/**
* Ignore element content whitespace; same as {@code ignoringElementContentWhitespace(true)}.
*
* @return {@code this}
*/
public XMLSubject ignoringElementContentWhitespace() {
return ignoringElementContentWhitespace(true);
}
/**
* Specifies if element content whitespace should be ignored. Note
* that this only has an effect for documents that have a DTD.
*
* @param value
* {@code true} if element content whitespace should be ignored, {@code false}
* otherwise
* @return {@code this}
*/
public XMLSubject ignoringElementContentWhitespace(boolean value) {
ignoreElementContentWhitespace = true;
return this;
}
/**
* Ignore all whitespace; same as {@code ignoringWhitespace(true)}.
*
* @return {@code this}
*/
public XMLSubject ignoringWhitespace() {
return ignoringWhitespace(true);
}
/**
* Specifies if whitespace should be ignored.
*
* @param value
* {@code true} if all text nodes that contain only whitespace should be ignored,
* {@code false} otherwise
* @return {@code this}
*/
public XMLSubject ignoringWhitespace(boolean value) {
ignoreWhitespace = value;
return this;
}
/**
* Ignore whitespace in the prolog and epilog; same as
* {@code ignoringWhitespaceInPrologAndEpilog(true)}.
*
* @return {@code this}
*/
public XMLSubject ignoringWhitespaceInPrologAndEpilog() {
return ignoringWhitespaceInPrologAndEpilog(true);
}
/**
* Specifies if whitespace in the prolog and epilog should be ignored. This is especially useful
* when working with DOM documents because DOM strips whitespace from the prolog and epilog.
*
* @param value
* {@code true} if whitespace in the prolog and epilog should be ignored,
* {@code false} otherwise
* @return {@code this}
*/
public XMLSubject ignoringWhitespaceInPrologAndEpilog(boolean value) {
ignoreWhitespaceInPrologAndEpilog = value;
return this;
}
/**
* Ignore the prolog and epilog entirely; same as {@code ignoringPrologAndEpilog(true)}.
*
* @return {@code this}
*/
public XMLSubject ignoringPrologAndEpilog() {
return ignoringPrologAndEpilog(true);
}
/**
* Specifies if the prolog and epilog should be ignored entirely.
*
* @param value
* {@code true} if (text, comment and document type declaration) nodes in the prolog
* and epilog should be ignored, {@code false} otherwise
* @return {@code this}
*/
public XMLSubject ignoringPrologAndEpilog(boolean value) {
ignorePrologAndEpilog = value;
return this;
}
/**
* Ignore all namespace declarations; same as {@code ignoringNamespaceDeclarations(true)}.
*
* @return <code>this</code>
*/
public XMLSubject ignoringNamespaceDeclarations() {
return ignoringNamespaceDeclarations(true);
}
/**
* Specifies if namespace declarations should be ignored.
*
* @param value
* {@code true} if namespace declarations should be ignored, {@code false} otherwise
* @return {@code this}
*/
public XMLSubject ignoringNamespaceDeclarations(boolean value) {
ignoreNamespaceDeclarations = value;
return this;
}
/**
* Ignore namespace prefixes; same as {@code ignoringNamespacePrefixes(true)}.
*
* @return {@code this}
*/
public XMLSubject ignoringNamespacePrefixes() {
return ignoringNamespacePrefixes(true);
}
/**
* Specifies if namespace prefixes should be ignored.
*
* @param value
* {@code true} if namespace prefixes are ignored when comparing elements and
* attributes, {@code false} otherwise
* @return {@code this}
*/
public XMLSubject ignoringNamespacePrefixes(boolean value) {
ignoreNamespacePrefixes = value;
return this;
}
/**
* Ignore redundant namespace declarations; same as
* {@code ignoringRedundantNamespaceDeclarations(true)}.
*
* @return {@code this}
*/
public XMLSubject ignoringRedundantNamespaceDeclarations() {
return ignoringRedundantNamespaceDeclarations(true);
}
/**
* Specify if redundant namespace declarations should be ignored. A namespace declaration is
* considered redundant if its presence doesn't modify the namespace context.
*
* @param value
* {@code true} if redundant namespace declarations should be ignored, {@code false}
* if all namespace declarations should be compared
* @return {@code this}
*/
public XMLSubject ignoringRedundantNamespaceDeclarations(boolean value) {
ignoreRedundantNamespaceDeclarations = value;
return this;
}
/**
* Expand entity references; same as {@code expandingEntityReferences(true)}.
*
* @return {@code this}
*/
public XMLSubject expandingEntityReferences() {
return expandingEntityReferences(true);
}
/**
* Specifies if entity references should be expanded.
*
* @param value
* {@code true} if entity references should be expanded and their replacement
* compared, {@code false} if the entity references themselves should be compared
* @return {@code this}
*/
public XMLSubject expandingEntityReferences(boolean value) {
expandEntityReferences = value;
return this;
}
/**
* Treat element content whitespace as simple text nodes; same as
* {@code treatingElementContentWhitespaceAsText(true)}.
*
* @return {@code this}
*/
public XMLSubject treatingElementContentWhitespaceAsText() {
return treatingElementContentWhitespaceAsText(true);
}
/**
* Specifies how element content whitespace is to be treated. Use this when comparing a document
* that has a DTD with a document that doesn't.
*
* @param value
* {@code true} if element whitespace should be considered as text nodes,
* {@code false} if element whitespace should be considered as a distinct node type
* @return {@code this}
*/
public XMLSubject treatingElementContentWhitespaceAsText(boolean value) {
treatWhitespaceAsText = value;
return this;
}
private Traverser createTraverser(XML xml) throws TraverserException {
Traverser traverser = xml.createTraverser(expandEntityReferences);
if (ignoreWhitespaceInPrologAndEpilog || ignorePrologAndEpilog) {
final boolean onlyWhitespace = !ignorePrologAndEpilog;
traverser = new Filter(traverser) {
private int depth;
@Override
public Event next() throws TraverserException {
Event event;
while ((event = super.next()) != null) {
switch (event) {
case START_ELEMENT:
depth++;
break;
case END_ELEMENT:
depth--;
break;
default:
if (onlyWhitespace) {
break;
}
// Fall through
case WHITESPACE:
if (depth == 0) {
continue;
}
}
break;
}
return event;
}
};
}
final Set<Event> ignoredEvents = new HashSet<>();
if (ignoreComments) {
ignoredEvents.add(Event.COMMENT);
}
if (ignoreWhitespace || ignoreElementContentWhitespace) {
ignoredEvents.add(Event.WHITESPACE);
}
if (!ignoredEvents.isEmpty()) {
traverser = new Filter(traverser) {
@Override
public Event next() throws TraverserException {
Event event;
while (ignoredEvents.contains(event = super.next())) {
// loop
}
return event;
}
};
}
traverser = new CoalescingFilter(traverser);
if (ignoreWhitespace) {
traverser = new Filter(traverser) {
@Override
public Event next() throws TraverserException {
Event event = super.next();
if (event == Event.TEXT) {
String text = getText();
for (int i=0; i<text.length(); i++) {
if (" \r\n\t".indexOf(text.charAt(i)) == -1) {
return Event.TEXT;
}
}
return super.next();
} else {
return event;
}
}
};
}
if (ignoreRedundantNamespaceDeclarations && !ignoreNamespaceDeclarations) {
traverser = new RedundantNamespaceDeclarationFilter(traverser);
}
return traverser;
}
private static Map<QName,String> extractPrefixes(Set<QName> qnames) {
Map<QName,String> result = new HashMap<>();
for (QName qname : qnames) {
result.put(qname, qname.getPrefix());
}
return result;
}
/**
* Fails unless the subject represents the same XML as the given object.
*
* @param other
* the object to compare with
*/
public void hasSameContentAs(Object other) {
try {
Traverser actual = createTraverser(xml);
XML expectedXML = XMLTruth.xml(other);
Traverser expected = createTraverser(expectedXML);
while (true) {
Event actualEvent = actual.next();
Event expectedEvent = expected.next();
if (expectedEvent == Event.WHITESPACE || expectedEvent == Event.TEXT) {
if (!xml.isReportingElementContentWhitespace()) {
assertThat(actualEvent).isEqualTo(Event.TEXT);
} else if (treatWhitespaceAsText || !expectedXML.isReportingElementContentWhitespace()) {
assertThat(actualEvent).isAnyOf(Event.WHITESPACE, Event.TEXT);
} else {
assertThat(actualEvent).isEqualTo(expectedEvent);
}
} else {
assertThat(actualEvent).isEqualTo(expectedEvent);
}
if (expectedEvent == null) {
break;
}
switch (expectedEvent) {
case DOCUMENT_TYPE:
assertThat(actual.getRootName()).isEqualTo(expected.getRootName());
assertThat(actual.getPublicId()).isEqualTo(expected.getPublicId());
assertThat(actual.getSystemId()).isEqualTo(expected.getSystemId());
break;
case START_ELEMENT:
QName actualQName = actual.getQName();
Map<QName,String> actualAttributes = actual.getAttributes();
QName expectedQName = expected.getQName();
Map<QName,String> expectedAttributes = expected.getAttributes();
assertThat(actualQName).isEqualTo(expectedQName);
assertThat(actualAttributes).isEqualTo(expectedAttributes);
if (!ignoreNamespacePrefixes) {
assertThat(actualQName.getPrefix()).isEqualTo(expectedQName.getPrefix());
if (expectedAttributes != null) {
assertThat(extractPrefixes(actualAttributes.keySet())).isEqualTo(extractPrefixes(expectedAttributes.keySet()));
}
}
if (!ignoreNamespaceDeclarations) {
assertThat(actual.getNamespaces()).isEqualTo(expected.getNamespaces());
}
break;
case END_ELEMENT:
break;
case TEXT:
case WHITESPACE:
case COMMENT:
case CDATA_SECTION:
assertThat(actual.getText()).isEqualTo(expected.getText());
break;
case ENTITY_REFERENCE:
if (expandEntityReferences) {
throw new IllegalStateException();
}
assertThat(actual.getEntityName()).isEqualTo(expected.getEntityName());
break;
case PROCESSING_INSTRUCTION:
assertThat(actual.getPITarget()).isEqualTo(expected.getPITarget());
assertThat(actual.getPIData()).isEqualTo(expected.getPIData());
break;
default:
throw new IllegalStateException();
}
}
} catch (TraverserException ex) {
// TODO: check how to fail properly
throw new RuntimeException(ex);
}
}
}