blob: 7bde973acd1d69a32e7dc7a8e29a1bcd7fba4297 [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.ws.commons.schema.docpath;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import javax.xml.bind.ValidationException;
import javax.xml.namespace.QName;
import org.apache.ws.commons.schema.XmlSchemaAny;
import org.apache.ws.commons.schema.XmlSchemaElement;
import org.apache.ws.commons.schema.walker.XmlSchemaTypeInfo;
import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;
/**
* Performs a SAX-based walk through the XML document, determining the
* interpretation ("path") that best matches the XML Schema.
* <p>
* This is a traditional {@link DefaultHandler} that can be attached to either a
* {@link javax.xml.parsers.SAXParser} during a parse, or to
* {@link SaxWalkerOverDom} to find paths through an
* {@link org.w3c.dom.Document}.
* </p>
* <p>
* Because this is a SAX-based walk, the source information need not be an XML
* document. It can be any data that can be interpreted via a SAX walk. This can
* be helpful when trying to confirm the source data can be converted back into
* XML.
* </p>
*/
public final class XmlSchemaPathFinder<U, V> extends DefaultHandler {
/*
* If a group loops back on itself, we don't want to loop until the stack
* overflows looking for a valid match. We will stop looking once we reach
* MAX_DEPTH.
*/
private static final int MAX_DEPTH = 256;
private final XmlSchemaNamespaceContext nsContext;
private XmlSchemaPathNode<U, V> rootPathNode;
private XmlSchemaPathNode<U, V> currentPath;
private ArrayList<TraversedElement> traversedElements;
private ArrayList<DecisionPoint<U, V>> decisionPoints;
private ArrayList<QName> elementStack;
private ArrayList<QName> anyStack;
private XmlSchemaPathManager<U, V> pathMgr;
/*
* We want to keep track of all of the valid path segments to a particular
* element, but we do not want to stomp on the very first node until we know
* which path we want to follow. Likewise, we want to keep the first node in
* the segment without a "next" node, but every node after that we wish to
* chain together. To accomplish this, we start with a base node at the end
* and "prepend" previous nodes until we work our way back to the beginning.
* When we prepend a node, we link the previous start node to the node
* directly after it, while leaving the new start node unlinked. Path
* segments may also be recycled when a decision point is refuted.
*/
private static final class PathSegment<U, V> implements Comparable<PathSegment<U, V>> {
private XmlSchemaPathManager<U, V> pathMgr;
private XmlSchemaPathNode<U, V> start;
private XmlSchemaPathNode<U, V> end;
private XmlSchemaPathNode<U, V> afterStart;
private int length;
private int afterStartPathIndex;
PathSegment(XmlSchemaPathManager<U, V> pathMgr, XmlSchemaPathNode<U, V> node) {
this.pathMgr = pathMgr;
set(node);
}
@Override
public int compareTo(PathSegment<U, V> o) {
if (this == o) {
return 0;
}
/*
* Paths which end in a wildcard element are of lower rank (higher
* order) than those that end in elements.
*/
if (!end.getStateMachineNode().getNodeType()
.equals(o.getEnd().getStateMachineNode().getNodeType())) {
if (end.getStateMachineNode().getNodeType().equals(XmlSchemaStateMachineNode.Type.ANY)) {
return 1;
} else if (o.getEnd().getStateMachineNode().getNodeType()
.equals(XmlSchemaStateMachineNode.Type.ANY)) {
return -1;
} else {
throw new IllegalStateException("The end nodes do not have the same machine node type, so one "
+ "should be an ELEMENT and the other should be an ANY. "
+ "However, this end node is a "
+ end.getStateMachineNode().getNodeType()
+ " while the other has an end node of type "
+ o.getEnd().getStateMachineNode().getNodeType()
+ ".");
}
}
final int thisLength = getLength();
final int thatLength = o.getLength();
/*
* Paths that walk through earlier sequence group children are
* preferred over paths that walk through later sequence group
* children. They provide more options later. Shorter paths are also
* preferred over longer ones.
*/
if ((thisLength > 0) && (thatLength > 0)) {
// Both paths have more than just one element.
XmlSchemaPathNode<U, V> thisIter = afterStart;
XmlSchemaPathNode<U, V> thatIter = o.getAfterStart();
while ((thisIter != null) && (thatIter != null)) {
if (thisIter.getDirection().getRank() < thatIter.getDirection().getRank()) {
return -1;
} else if (thisIter.getDirection().getRank() > thatIter.getDirection().getRank()) {
return 1;
}
if (thisIter.getIndexOfNextNodeState() < thatIter.getIndexOfNextNodeState()) {
return -1;
} else if (thisIter.getIndexOfNextNodeState() > thatIter.getIndexOfNextNodeState()) {
return 1;
}
thisIter = thisIter.getNext();
thatIter = thatIter.getNext();
}
if ((thisIter == null) && (thatIter != null)) {
// This path is shorter.
return -1;
} else if ((thisIter != null) && (thatIter == null)) {
// That path is shorter.
return 1;
}
} else if ((thisLength == 0) && (thatLength > 0)) {
// This path is shorter.
return -1;
} else if ((thisLength > 0) && (thatLength == 0)) {
// That path is shorter.
return 1;
} else {
// Both paths have exactly one element.
if (end.getIndexOfNextNodeState() < o.getEnd().getIndexOfNextNodeState()) {
return -1;
} else if (end.getIndexOfNextNodeState() > o.getEnd().getIndexOfNextNodeState()) {
return 1;
}
}
/*
* If all of our different heuristics do not differentiate the
* paths, we will return equality. This is fine because
* Collections.sort(List<T>) is stable, and will preserve the
* ordering.
*/
return 0;
}
int getLength() {
if ((length == 0) && (start != end)) {
for (XmlSchemaPathNode<U, V> iter = afterStart; iter != end; iter = iter.getNext()) {
++length;
}
++length; // (afterStart -> end) + start
}
return length;
}
/*
* Prepends a new start node to this segment. We want to clone the
* previous start node as sibling paths may be sharing it. We also need
* to know the newStart's path index to reach the clonedStartNode, so we
* know how to properly link them later.
*/
void prepend(XmlSchemaPathNode<U, V> newStart, int pathIndexToNextNode) {
// We need to clone start and make it the afterStart.
final XmlSchemaPathNode<U, V> clonedStartNode = pathMgr.clone(start);
if (afterStart != null) {
afterStart.setPreviousNode(clonedStartNode);
clonedStartNode.setNextNode(afterStartPathIndex, afterStart);
afterStart = clonedStartNode;
} else {
// This path segment only has one node in it; now it has two.
end = clonedStartNode;
afterStart = clonedStartNode;
}
start = newStart;
afterStartPathIndex = pathIndexToNextNode;
length = 0; // Force a recalculation.
}
XmlSchemaPathNode<U, V> getStart() {
return start;
}
XmlSchemaPathNode<U, V> getEnd() {
return end;
}
XmlSchemaPathNode<U, V> getAfterStart() {
return afterStart;
}
int getAfterStartPathIndex() {
return afterStartPathIndex;
}
void set(XmlSchemaPathNode<U, V> node) {
if (node == null) {
throw new IllegalArgumentException("DocumentPathNode cannot be null.");
}
this.start = node;
this.end = node;
this.afterStart = null;
this.afterStartPathIndex = -1;
this.length = 0;
}
@Override
public String toString() {
final StringBuilder str = new StringBuilder("Path Segment: [ ");
str.append(start.getDirection()).append(" | ");
str.append(start.getStateMachineNode()).append(" ]");
if (afterStart != null) {
XmlSchemaPathNode<U, V> path = afterStart;
do {
str.append(" [").append(path.getDirection()).append(" | ");
str.append(path.getStateMachineNode()).append(" ]");
path = path.getNext();
} while (path != null);
} else {
str.append(" [").append(end.getDirection()).append(" | ");
str.append(end.getStateMachineNode()).append(" ]");
}
return str.toString();
}
}
/*
* A <code>DescisionPoint</code> is a location in a document path where an
* element in the document can be reached by following two or more different
* traversals through the XML Schema. When we reach such a decision point,
* we will keep track of the different paths through the XML Schema that
* reach the element. We will then follow each path, one-by-one from the
* shortest through the longest, until we find a path that successfully
* navigates both the document and the schema.
*/
private static class DecisionPoint<U, V> {
private final XmlSchemaPathNode<U, V> decisionPoint;
private final List<PathSegment<U, V>> choices;
private final int traversedElementIndex;
private final ArrayList<QName> elementStack;
private final ArrayList<QName> anyStack;
DecisionPoint(XmlSchemaPathNode<U, V> decisionPoint, List<PathSegment<U, V>> choices,
int traversedElementIndex, ArrayList<QName> elementStack, ArrayList<QName> anyStack) {
if (decisionPoint == null) {
throw new IllegalArgumentException("The decision point path node cannot be null.");
} else if (choices == null) {
throw new IllegalArgumentException("The set of choice paths to follow cannot be null.");
} else if (choices.size() < 2) {
throw new IllegalArgumentException(
"There must be at least two choices to constitute a decision point"
+ ", not " + choices.size());
}
this.decisionPoint = decisionPoint;
this.choices = choices;
this.traversedElementIndex = traversedElementIndex;
this.elementStack = new ArrayList<QName>(elementStack);
if (anyStack == null) {
this.anyStack = null;
} else {
this.anyStack = new ArrayList<QName>(anyStack);
}
java.util.Collections.sort(choices);
}
/**
* Returns the next <code>PathSegment</code> to try, or
* <code>null</code> if all <code>PathSegment</code>s have been
* followed.
*/
PathSegment<U, V> tryNextPath() {
if (choices.isEmpty()) {
return null;
} else {
return choices.remove(0);
}
}
XmlSchemaPathNode<U, V> getDecisionPoint() {
return decisionPoint;
}
ArrayList<QName> getElementStack() {
return new ArrayList<QName>(elementStack);
}
ArrayList<QName> getAnyStack() {
return (anyStack == null) ? null : new ArrayList<QName>(anyStack);
}
@Override
public String toString() {
final String nl = System.getProperty("line.separator");
final StringBuilder str = new StringBuilder("Decision Point: ");
str.append(decisionPoint.getDirection()).append(" | ");
str.append(decisionPoint.getStateMachineNode());
str.append(" ]").append(nl);
for (PathSegment<U, V> choice : choices) {
str.append('\t').append(choice).append(nl);
}
return str.toString();
}
}
/*
* Represents an element-start, element-end, or content we have seen before.
* When walking our way through the XML Schema, we need this information in
* order to properly backtrack if we took a wrong path.
*/
private static class TraversedElement {
QName elemName;
Traversal traversal;
enum Traversal {
START, CONTENT, END
}
TraversedElement(QName elemName, Traversal traversal) {
this.elemName = elemName;
this.traversal = traversal;
}
@Override
public String toString() {
StringBuilder str = new StringBuilder(elemName.toString());
str.append(" : ").append(traversal);
return str.toString();
}
}
/*
* Represents a group's fulfillment state. It is either not fulfilled,
* meaning more children are required, partially fulfilled, meaning at least
* the minimum number of children have been added, or completely fulfilled,
* meaning no more children can be introduced.
*/
private enum Fulfillment {
NOT, PARTIAL, COMPLETE
}
/**
* Creates a new <code>XmlToAvroPathCreator</code> with the root
* {@link XmlSchemaStateMachineNode} to start from when evaluating
* documents.
*/
public XmlSchemaPathFinder(XmlSchemaStateMachineNode root) {
pathMgr = new XmlSchemaPathManager<U, V>();
nsContext = new XmlSchemaNamespaceContext();
rootPathNode = pathMgr.createStartPathNode(XmlSchemaPathNode.Direction.CHILD, root);
rootPathNode.setIteration(1);
traversedElements = new ArrayList<TraversedElement>();
elementStack = new ArrayList<QName>();
currentPath = null;
decisionPoints = null; // Hopefully there won't be any!
}
/**
* Kick-starts a new SAX walk, building new <code>XmlSchemaPathNode</code>
* and <code>XmlSchemaDocumentNode</code> traversals in the process.
*
* @see DefaultHandler#startDocument()
*/
@Override
public void startDocument() throws SAXException {
currentPath = null;
traversedElements.clear();
elementStack.clear();
if (decisionPoints != null) {
decisionPoints.clear();
}
}
/**
* Handles a new prefix mapping in the SAX walk.
*
* @see DefaultHandler#startPrefixMapping(String, String)
*/
@Override
public void startPrefixMapping(String prefix, String uri) throws SAXException {
nsContext.addNamespace(prefix, uri);
}
/**
* Handles the end of a prefix mapping in the SAX walk.
*
* @see DefaultHandler#endPrefixMapping(String)
*/
@Override
public void endPrefixMapping(String prefix) throws SAXException {
nsContext.removeNamespace(prefix);
}
/**
* Find the path through the XML Schema that best matches this element,
* traversing any relevant groups, and backtracking if necessary.
*
* @see DefaultHandler#startElement(String, String, String, Attributes)
*/
@Override
public void startElement(String uri, String localName, String qName, Attributes atts) throws SAXException {
final QName elemQName = new QName(uri, localName);
try {
if (currentPath == null) {
/*
* We just started a new document. Likewise we need to move into
* a position to process the root element.
*/
currentPath = rootPathNode;
} else if (currentPath.getStateMachineNode().getNodeType()
.equals(XmlSchemaStateMachineNode.Type.ANY)
&& (anyStack != null) && !anyStack.isEmpty()) {
/*
* If we are currently following a wildcard element node, we
* don't know anything about the element or its children. So it
* does not make sense to follow the children or grandchildren
* of this element.
*/
elementStack.add(elemQName);
anyStack.add(elemQName);
return;
}
// 1. Find possible paths.
List<PathSegment<U, V>> possiblePaths = find(currentPath, elemQName);
PathSegment<U, V> nextPath = null;
if ((possiblePaths != null) && !possiblePaths.isEmpty()) {
/*
* 2. If multiple paths were returned, add a DecisionPoint. Sort
* the paths where paths ending in elements are favored over
* element wild cards, and shorter paths are favored over longer
* paths.
*/
if (possiblePaths.size() > 1) {
final DecisionPoint<U, V> decisionPoint =
new DecisionPoint<U, V>(currentPath, possiblePaths, traversedElements.size(),
elementStack, anyStack);
if (decisionPoints == null) {
decisionPoints = new ArrayList<DecisionPoint<U, V>>(4);
}
decisionPoints.add(decisionPoint);
nextPath = decisionPoint.tryNextPath();
} else {
nextPath = possiblePaths.get(0);
}
if (nextPath == null) {
throw new IllegalStateException("When searching for " + elemQName
+ ", received a set of path choices of size "
+ possiblePaths.size() + ", but the next path is null.");
}
followPath(nextPath);
} else {
// OR: If no paths are returned:
while ((decisionPoints != null) && !decisionPoints.isEmpty()) {
/*
* 2a. Backtrack to the most recent decision point. Remove
* the top path (the one we just tried), and select the next
* one.
*/
final DecisionPoint<U, V> priorPoint = decisionPoints.get(decisionPoints.size() - 1);
nextPath = priorPoint.tryNextPath();
if (nextPath == null) {
/*
* We have tried all paths at this decision point.
* Remove it and try the next prior decision point.
*/
decisionPoints.remove(decisionPoints.size() - 1);
continue;
}
pathMgr.unfollowPath(priorPoint.getDecisionPoint());
elementStack = priorPoint.getElementStack();
anyStack = priorPoint.getAnyStack();
/*
* Walk through the traversedElements list again from that
* index and see if we traverse through all of the elements
* in the list, including this one. If not, repeat step 2a,
* removing decision points from the stack as we refute
* them.
*/
followPath(nextPath);
final QName traversedQName = traversedElements.get(priorPoint.traversedElementIndex).elemName;
elementStack.add(traversedQName);
if (currentPath.getStateMachineNode().getNodeType()
.equals(XmlSchemaStateMachineNode.Type.ANY)) {
if (anyStack == null) {
anyStack = new ArrayList<QName>();
}
anyStack.add(traversedQName);
}
int index = priorPoint.traversedElementIndex + 1;
for (; index < traversedElements.size(); ++index) {
nextPath = null;
final TraversedElement te = traversedElements.get(index);
if (te.traversal.equals(TraversedElement.Traversal.START)) {
possiblePaths = find(currentPath, te.elemName);
if ((possiblePaths == null) || possiblePaths.isEmpty()) {
break;
} else if (possiblePaths.size() > 1) {
final DecisionPoint<U, V> decisionPoint =
new DecisionPoint<U, V>(currentPath, possiblePaths, index, elementStack, anyStack);
decisionPoints.add(decisionPoint);
nextPath = decisionPoint.tryNextPath();
} else {
nextPath = possiblePaths.get(0);
}
if (nextPath == null) {
throw new IllegalStateException("Somehow after finding a new path to follow,"
+ " that path is null.");
}
// If we find (a) path(s) that match(es), success!
// Follow it.
followPath(nextPath);
if (currentPath.getStateMachineNode().getNodeType()
.equals(XmlSchemaStateMachineNode.Type.ANY)) {
if (anyStack == null) {
anyStack = new ArrayList<QName>();
}
anyStack.add(te.elemName);
}
elementStack.add(te.elemName);
} else if (te.traversal.equals(TraversedElement.Traversal.END)) {
final boolean isAny = (currentPath.getStateMachineNode().getNodeType()
.equals(XmlSchemaStateMachineNode.Type.ANY)
&& (anyStack != null) && !anyStack.isEmpty());
if (!isAny) {
walkUpToElement(te.elemName);
}
walkUpTree(te.elemName);
final QName endingElemName = elementStack.remove(elementStack.size() - 1);
if (!te.elemName.equals(endingElemName)) {
throw new IllegalStateException("Attempted to end element " + te.elemName
+ " but found " + endingElemName
+ " on the stack instead!");
}
if (isAny) {
anyStack.remove(anyStack.size() - 1);
}
} else if (te.traversal.equals(TraversedElement.Traversal.CONTENT)) {
final XmlSchemaPathNode<U, V> contentPath =
pathMgr
.addParentSiblingOrContentNodeToPath(currentPath,
XmlSchemaPathNode.Direction.CONTENT);
currentPath.setNextNode(-1, contentPath);
currentPath = contentPath;
} else {
throw new IllegalStateException("Unrecognized element traversal direction for "
+ te.elemName + " of " + te.traversal + '.');
}
}
if (index < traversedElements.size()) {
/*
* This attempt is also incorrect. However, we may have
* introduced new decision points along the way, and we
* want to follow them first. So let's go back around
* for another try.
*/
continue;
}
/*
* We made it to the end of the element list! Now try the
* current one again.
*/
possiblePaths = find(currentPath, elemQName);
if (possiblePaths == null) {
// Still incorrect!
continue;
} else if (possiblePaths.size() > 1) {
final DecisionPoint<U, V> decisionPoint =
new DecisionPoint<U, V>(currentPath, possiblePaths, traversedElements.size(),
elementStack, anyStack);
decisionPoints.add(decisionPoint);
nextPath = decisionPoint.tryNextPath();
} else {
nextPath = possiblePaths.get(0);
}
if (nextPath != null) {
followPath(nextPath);
break;
}
}
}
if (nextPath == null) {
/*
* If we go through all prior decision points and are unable to
* find one or more paths through the XML Schema that match the
* document, throw an error. There is nothing more we can do
* here.
*/
throw new IllegalStateException(
"Walked through XML Schema and could not find a traversal that "
+ "represented this XML Document.");
}
/*
* Current path now points to the element we just started. Validate
* its attributes.
*/
validateAttributes(atts);
traversedElements.add(new TraversedElement(elemQName, TraversedElement.Traversal.START));
elementStack.add(elemQName);
/*
* If this is element is of type xsd:any, we do not track it or its
* children. So, we keep a stack of the element and its children,
* allowing us to know when we leave and can start tracking elements
* again.
*/
if (currentPath.getStateMachineNode().getNodeType().equals(XmlSchemaStateMachineNode.Type.ANY)) {
if (anyStack == null) {
anyStack = new ArrayList<QName>();
}
anyStack.add(elemQName);
}
} catch (Exception e) {
/*
* A SAX Exception cannot be thrown because it is caught, and its
* internal exception is thrown instead. Likewise, any useful info
* about the error reported in the wrapper SAXException is lost.
*/
throw new RuntimeException("Error occurred while starting element " + elemQName
+ "; traversed path is " + getElementsTraversedAsString(), e);
}
}
/**
* Adds a new {@link XmlSchemaPathNode.Direction#CONTENT}
* {@link XmlSchemaPathNode} to the path. Throws an
* {@link IllegalStateException} if the owning element should not receive
* content, or the content is empty when it should not be.
*
* @see org.xml.sax.helpers.DefaultHandler#characters(char[], int, int)
*/
@Override
public void characters(char[] ch, int start, int length) throws SAXException {
/*
* If the most recent path node is an element with simple content,
* confirm these characters match the data type expected. If we are not
* expecting an element with simple content, and the characters don't
* represent all whitespace, throw an exception.
*/
try {
if (currentPath.getStateMachineNode().getNodeType().equals(XmlSchemaStateMachineNode.Type.ANY)
&& (anyStack != null) && !anyStack.isEmpty()) {
/*
* If this represents a wildcard element, we don't care - we
* won't be processing the content.
*/
return;
}
final XmlSchemaStateMachineNode state = getStateMachineOfOwningElement();
final XmlSchemaElement element = state.getElement();
final XmlSchemaTypeInfo elemTypeInfo = state.getElementType();
final String text = new String(ch, start, length).trim();
final boolean elemExpectsContent = ((elemTypeInfo != null) && (!elemTypeInfo.getType()
.equals(XmlSchemaTypeInfo.Type.COMPLEX) || elemTypeInfo.isMixed()));
if (!elemExpectsContent && (text.length() == 0)) {
// Nothing to see here.
return;
} else if (!elemExpectsContent && (text.length() > 0)) {
throw new IllegalStateException("Element " + state.getElement().getQName()
+ " has no content, but we received \"" + text + "\" for it.");
} else if (elemExpectsContent && (text.length() == 0) && !state.getElement().isNillable()
&& !elemTypeInfo.isMixed() && (element.getDefaultValue() == null)
&& (element.getFixedValue() == null)) {
throw new IllegalStateException("Received empty text for element "
+ state.getElement().getQName()
+ " when content was expected.");
}
XmlSchemaElementValidator.validateContent(state, text, nsContext);
currentPath.getDocumentNode().setReceivedContent(true);
final XmlSchemaPathNode<U, V> contentPath = pathMgr
.addParentSiblingOrContentNodeToPath(currentPath, XmlSchemaPathNode.Direction.CONTENT);
currentPath.setNextNode(-1, contentPath);
currentPath = contentPath;
traversedElements
.add(new TraversedElement(element.getQName(), TraversedElement.Traversal.CONTENT));
} catch (Exception e) {
throw new RuntimeException("Error occurred while processing characters; traversed path was "
+ getElementsTraversedAsString(), e);
}
}
/**
* Ends the current element. If the current element is not of the provided
* <code>uri</code> and <code>localName</code>, throws an
* {@link IllegalStateException}.
*
* @see DefaultHandler#endElement(String, String, String)
*/
@Override
public void endElement(String uri, String localName, String qName) throws SAXException {
final QName elemQName = new QName(uri, localName);
try {
final boolean isAny = (currentPath.getStateMachineNode().getNodeType()
.equals(XmlSchemaStateMachineNode.Type.ANY)
&& (anyStack != null) && !anyStack.isEmpty());
if (!isAny && !elementStack.get(elementStack.size() - 1).equals(elemQName)) {
throw new IllegalStateException("Attempting to end element " + elemQName
+ " but the stack is expecting "
+ elementStack.get(elementStack.size() - 1));
}
if (!isAny) {
walkUpToElement(elemQName);
}
final XmlSchemaStateMachineNode state = currentPath.getStateMachineNode();
if (state.getNodeType().equals(XmlSchemaStateMachineNode.Type.ELEMENT)) {
// 1. Is this the element we are looking for?
if (!state.getElement().getQName().equals(elemQName)) {
throw new IllegalStateException("We are ending element " + elemQName
+ " but our current position is for element "
+ state.getElement().getQName() + '.');
}
// 2. Check the element received the expected content, if any.
final XmlSchemaTypeInfo elemTypeInfo = state.getElementType();
final boolean elemExpectsContent = (elemTypeInfo != null)
&& (!elemTypeInfo.getType()
.equals(XmlSchemaTypeInfo.Type.COMPLEX));
if (elemExpectsContent && !state.getElement().isNillable()
&& (state.getElement().getDefaultValue() == null)
&& (state.getElement().getFixedValue() == null)
&& !currentPath.getDocumentNode().getReceivedContent()) {
throw new IllegalStateException("We are ending element " + elemQName
+ "; it expected to receive content but did not.");
}
}
traversedElements.add(new TraversedElement(elemQName, TraversedElement.Traversal.END));
elementStack.remove(elementStack.size() - 1);
if (isAny) {
anyStack.remove(anyStack.size() - 1);
}
if ((anyStack == null) || anyStack.isEmpty()) {
walkUpTree(elemQName);
}
} catch (Exception e) {
throw new RuntimeException("Error occurred while ending element " + elemQName
+ "; traversed path was " + getElementsTraversedAsString(), e);
}
}
/**
* Called when the XML Document traversal is complete.
* <p>
* Confirms all open elements have been closed. If not, throws an
* {@link IllegalStateException}.
* </p>
*/
@Override
public void endDocument() throws SAXException {
if (!elementStack.isEmpty()) {
throw new IllegalStateException("Ended the document but " + elementStack.size()
+ " elements have not been closed.");
}
pathMgr.clear();
if (decisionPoints != null) {
decisionPoints.clear();
}
}
/**
* Once a traversal completes successfully, this method may be called to
* retrieve the relevant interpretation of the path through the
* {@link XmlSchemaStateMachineNode}s.
* <p>
* {@link XmlSchemaPathNode#getDocumentNode()} can be called to retrieve the
* interpretation of the XML Schema as applied to the document; meanwhile
* the walk through {@link XmlSchemaPathNode}s will show how that schema was
* traversed.
* </p>
*/
public XmlSchemaPathNode<U, V> getXmlSchemaTraversal() {
return rootPathNode;
}
private static <U, V> Fulfillment isPositionFulfilled(XmlSchemaPathNode<U, V> currentPath,
List<Integer> possiblePaths) {
boolean completelyFulfilled = true;
boolean partiallyFulfilled = true;
final XmlSchemaStateMachineNode state = currentPath.getStateMachineNode();
if (currentPath.getDocumentNode() == null) {
// This is the root node. It is not fulfilled.
partiallyFulfilled = false;
} else if (currentPath.getDocIteration() >= state.getMinOccurs()) {
partiallyFulfilled = true;
} else {
partiallyFulfilled = false;
}
if (currentPath.getDocumentNode() == null) {
completelyFulfilled = false;
} else if (currentPath.getDocIteration() == state.getMaxOccurs()) {
completelyFulfilled = true;
} else if (currentPath.getDocIteration() > state.getMaxOccurs()) {
throw new IllegalStateException("Current path's document iteration of "
+ currentPath.getDocIteration()
+ " is greater than the maximum number of occurrences ("
+ state.getMaxOccurs() + ").");
} else {
completelyFulfilled = false;
}
final List<XmlSchemaStateMachineNode> nextStates = state.getPossibleNextStates();
Map<Integer, XmlSchemaDocumentNode<U>> children = null;
if (currentPath.getDocumentNode() != null) {
children = currentPath.getDocumentNode().getChildren();
}
switch (state.getNodeType()) {
case ELEMENT:
case ANY:
// We only needed to perform the occurrence check.
break;
case CHOICE:
case SUBSTITUTION_GROUP: {
/*
* If any child meets the minimum number, we are partially
* fulfilled. If all elements meet the maximum, we are completely
* fulfilled.
*/
boolean groupPartiallyFulfilled = false;
boolean groupCompletelyFulfilled = false;
for (int stateIndex = 0; stateIndex < nextStates.size(); ++stateIndex) {
XmlSchemaStateMachineNode nextState = nextStates.get(stateIndex);
if ((children != null) && children.containsKey(stateIndex)) {
final XmlSchemaDocumentNode<U> child = children.get(stateIndex);
final int iteration = child.getIteration();
if (iteration >= nextState.getMinOccurs()) {
groupPartiallyFulfilled = true;
if (possiblePaths != null) {
possiblePaths.clear();
if (iteration < nextState.getMaxOccurs()) {
possiblePaths.add(stateIndex);
} else {
groupCompletelyFulfilled = true;
}
}
break;
} else if (possiblePaths != null) {
possiblePaths.add(stateIndex);
}
} else {
if (nextState.getMinOccurs() == 0) {
groupPartiallyFulfilled = true;
}
if (nextState.getMaxOccurs() == 0) {
groupCompletelyFulfilled = true;
} else if (possiblePaths != null) {
possiblePaths.add(stateIndex);
}
}
}
partiallyFulfilled &= groupPartiallyFulfilled;
completelyFulfilled &= groupCompletelyFulfilled;
break;
}
case ALL: {
// If all children meet the minimum number, we succeeded.
for (int stateIndex = 0; stateIndex < nextStates.size(); ++stateIndex) {
final XmlSchemaStateMachineNode nextState = nextStates.get(stateIndex);
if ((children != null) && children.containsKey(stateIndex)) {
final XmlSchemaDocumentNode<U> child = children.get(stateIndex);
final int iteration = child.getIteration();
if (iteration < nextState.getMinOccurs()) {
partiallyFulfilled = false;
}
if (iteration < nextState.getMaxOccurs()) {
completelyFulfilled = false;
if (possiblePaths != null) {
possiblePaths.add(stateIndex);
}
}
} else {
if (nextState.getMinOccurs() > 0) {
partiallyFulfilled = false;
}
if (nextState.getMaxOccurs() > 0) {
completelyFulfilled = false;
if (possiblePaths != null) {
possiblePaths.add(stateIndex);
}
}
}
}
break;
}
case SEQUENCE: {
// If the sequence is complete, we succeeded.
int stateIndex = currentPath.getDocSequencePosition();
if (stateIndex < 0) {
stateIndex = 0;
}
for (; stateIndex < nextStates.size(); ++stateIndex) {
final XmlSchemaStateMachineNode nextState = nextStates.get(stateIndex);
if ((children != null) && children.containsKey(stateIndex)) {
final XmlSchemaDocumentNode<U> child = children.get(stateIndex);
if (child.getIteration() < nextState.getMinOccurs()) {
partiallyFulfilled = false;
}
if (child.getIteration() < nextState.getMaxOccurs()) {
completelyFulfilled = false;
if (possiblePaths != null) {
possiblePaths.add(stateIndex);
}
}
} else {
if (nextState.getMinOccurs() > 0) {
partiallyFulfilled = false;
}
if (nextState.getMaxOccurs() > 0) {
completelyFulfilled = false;
if (possiblePaths != null) {
possiblePaths.add(stateIndex);
}
}
}
}
break;
}
default:
throw new IllegalStateException("Current position has a node of unrecognized type \""
+ currentPath.getStateMachineNode().getNodeType() + '\"');
}
Fulfillment fulfillment = Fulfillment.NOT;
if (completelyFulfilled) {
fulfillment = Fulfillment.COMPLETE;
} else if (partiallyFulfilled) {
fulfillment = Fulfillment.PARTIAL;
}
return fulfillment;
}
private List<PathSegment<U, V>> find(XmlSchemaPathNode<U, V> startNode, QName elemQName) {
final XmlSchemaPathNode<U, V> startOfPath = startNode;
if (startNode.getStateMachineNode().getNodeType().equals(XmlSchemaStateMachineNode.Type.ELEMENT)
&& !elementStack.isEmpty()
&& startNode.getStateMachineNode().getElement().getQName()
.equals(elementStack.get(elementStack.size() - 1))) {
/*
* We are at an element in an existing document. This is the start
* node of a child. Likewise we need to move down one level to the
* children.
*/
verifyCurrentPositionIsAtElement("Started element " + elemQName);
if (startNode.getStateMachineNode().getPossibleNextStates() == null) {
final String elemName = getLeafNodeName(startNode.getStateMachineNode());
throw new IllegalStateException("Element " + elemName
+ " has null children! Exactly one is expected.");
} else if (startNode.getStateMachineNode().getPossibleNextStates().isEmpty()) {
final String elemName = getLeafNodeName(startNode.getStateMachineNode());
throw new IllegalStateException("Element " + elemName
+ " has zero children! Exactly one is expected.");
} else if (currentPath.getStateMachineNode().getPossibleNextStates().size() > 1) {
final String elemName = getLeafNodeName(currentPath.getStateMachineNode());
throw new IllegalStateException("Element "
+ elemName
+ " has "
+ currentPath.getStateMachineNode().getPossibleNextStates()
.size() + " children! Only one was expected.");
}
if ((startNode.getDocumentNode() != null) && (startNode.getDocumentNode().getChildren() != null)
&& !startNode.getDocumentNode().getChildren().isEmpty()
&& (startNode.getDocumentNode().getChildren().size() > 1)) {
throw new IllegalStateException(
"There are multiple children in the document node for element "
+ currentPath.getStateMachineNode().getElement()
.getQName());
}
final XmlSchemaPathNode<U, V> childPath = pathMgr.addChildNodeToPath(startNode, 0);
startNode.setNextNode(0, childPath);
startNode = childPath;
}
final List<PathSegment<U, V>> choices = find(startNode, elemQName, null);
if ((choices != null) && (startOfPath != startNode)) {
/*
* If we moved down to the children, we need to prepend the path
* with the original start node.
*/
for (PathSegment<U, V> choice : choices) {
choice.prepend(startOfPath, 0);
}
}
return choices;
}
private List<PathSegment<U, V>> find(XmlSchemaPathNode<U, V> startNode, QName elemQName,
XmlSchemaStateMachineNode doNotFollow) {
final ArrayList<Integer> childrenNodes = new ArrayList<Integer>();
final boolean isFulfilled = !isPositionFulfilled(startNode, childrenNodes).equals(Fulfillment.NOT);
// First, try searching down the tree.
List<PathSegment<U, V>> choices = null;
List<PathSegment<U, V>> currChoices = null;
if (startNode.getIteration() > startNode.getDocIteration()) {
choices = find(startNode, elemQName, 0);
} else {
for (Integer childPath : childrenNodes) {
if (doNotFollow == startNode.getStateMachineNode().getPossibleNextStates().get(childPath)) {
/*
* We are coming up from a child node; do not traverse back
* down to that child.
*/
continue;
}
final XmlSchemaPathNode<U, V> currPath = pathMgr.addChildNodeToPath(startNode, childPath);
currChoices = find(currPath, elemQName, 0);
if (currChoices != null) {
for (PathSegment<U, V> choice : currChoices) {
choice.prepend(startNode, childPath);
}
if (choices == null) {
choices = currChoices;
} else {
choices.addAll(currChoices);
}
}
}
}
// Second, if the node is currently fulfilled, try siblings and parents.
if (isFulfilled) {
// Try siblings.
if (startNode.getIteration() < startNode.getMaxOccurs()) {
final XmlSchemaPathNode<U, V> siblingPath = pathMgr
.addParentSiblingOrContentNodeToPath(startNode, XmlSchemaPathNode.Direction.SIBLING);
siblingPath.setIteration(startNode.getIteration() + 1);
currChoices = find(siblingPath, elemQName, 0);
if (currChoices != null) {
for (PathSegment<U, V> choice : currChoices) {
choice.prepend(startNode, -1);
}
if (choices == null) {
choices = currChoices;
} else {
choices.addAll(currChoices);
}
}
}
// Try parent.
if (startNode.getDocumentNode().getParent() == null) {
// This is the root element; there is no parent.
return choices;
}
final XmlSchemaPathNode<U, V> path = pathMgr
.addParentSiblingOrContentNodeToPath(startNode, XmlSchemaPathNode.Direction.PARENT);
if (path.getStateMachineNode().getNodeType().equals(XmlSchemaStateMachineNode.Type.ELEMENT)
&& path.getStateMachineNode().getElement().getQName()
.equals(elementStack.get(elementStack.size() - 1))) {
return choices;
}
final List<PathSegment<U, V>> pathsOfParent = find(path, elemQName, startNode.getStateMachineNode());
if (pathsOfParent != null) {
for (PathSegment<U, V> choice : pathsOfParent) {
choice.prepend(startNode, -1);
}
if (choices == null) {
choices = pathsOfParent;
} else {
choices.addAll(pathsOfParent);
}
} else {
// path would not have been recycled at a lower level.
pathMgr.recyclePathNode(path);
}
}
return choices;
}
private List<PathSegment<U, V>> find(XmlSchemaPathNode<U, V> startNode, QName elemQName, int currDepth) {
final XmlSchemaStateMachineNode state = startNode.getStateMachineNode();
if (currDepth > MAX_DEPTH) {
/*
* We are likely in an infinite recursive loop looking for an
* element in a group whose definition includes itself. Likewise,
* we'll stop here and say we were unable to find the element we
* were looking for.
*/
return null;
} else if (startNode.getStateMachineNode() != state) {
throw new IllegalStateException("While searching for " + elemQName
+ ", the DocumentPathNode state machine ("
+ startNode.getStateMachineNode().getNodeType()
+ ") does not match the tree node (" + state.getNodeType() + ").");
} else if (startNode.getIteration() <= startNode.getDocIteration()) {
throw new IllegalStateException("While searching for " + elemQName
+ ", the DocumentPathNode iteration (" + startNode.getIteration()
+ ") should be greater than the tree node's iteration ("
+ startNode.getDocIteration()
+ "). Current state machine position is " + state.getNodeType());
} else if (state.getMaxOccurs() < startNode.getIteration()) {
return null;
}
// If this is a group, confirm it has children.
if (!state.getNodeType().equals(XmlSchemaStateMachineNode.Type.ELEMENT)
&& !state.getNodeType().equals(XmlSchemaStateMachineNode.Type.ANY)
&& ((state.getPossibleNextStates() == null) || state.getPossibleNextStates().isEmpty())) {
throw new IllegalStateException("Group " + state.getNodeType()
+ " has no children. Found when processing " + elemQName);
}
List<PathSegment<U, V>> choices = null;
switch (state.getNodeType()) {
case ELEMENT: {
if (state.getElement().getQName().equals(elemQName)
&& startNode.getIteration() <= state.getMaxOccurs()) {
choices = new ArrayList<PathSegment<U, V>>(1);
choices.add(new PathSegment<U, V>(pathMgr, startNode));
}
}
break;
case SEQUENCE: {
// Find the next one in the sequence that matches.
int position = startNode.getDocSequencePosition();
if (startNode.getDocIteration() > startNode.getMaxOccurs()) {
throw new IllegalStateException("Somehow the document iteration for "
+ startNode.getStateMachineNode() + " of "
+ startNode.getDocIteration()
+ " exceeds the maximum number of occurrences of "
+ startNode.getMaxOccurs());
} else if (startNode.getDocIteration() == startNode.getMaxOccurs()) {
++position;
}
for (int stateIndex = position; stateIndex < startNode.getStateMachineNode()
.getPossibleNextStates().size(); ++stateIndex) {
// Process child.
final XmlSchemaPathNode<U, V> nextPath = pathMgr.addChildNodeToPath(startNode, stateIndex);
/*
* Both the tree node's and the document path node's state
* machine nodes should point to the same state machine node in
* memory.
*/
if (nextPath.getIteration() > nextPath.getMaxOccurs()) {
throw new IllegalStateException("Reached a sequence group when searching for "
+ elemQName
+ " whose iteration at the current position ("
+ nextPath.getIteration() + ") was already maxed out ("
+ nextPath.getMaxOccurs() + "). Was at position "
+ stateIndex + "; tree node's starting position was "
+ startNode.getDocSequencePosition());
}
final boolean reachedMinOccurs = (nextPath.getDocIteration() >= nextPath.getMinOccurs());
final List<PathSegment<U, V>> seqPaths = find(nextPath, elemQName, currDepth + 1);
if (seqPaths != null) {
for (PathSegment<U, V> seqPath : seqPaths) {
seqPath.prepend(startNode, stateIndex);
}
// nextPath was cloned by all path segments, so it can be
// recycled.
pathMgr.recyclePathNode(nextPath);
if (choices == null) {
choices = seqPaths;
} else {
choices.addAll(seqPaths);
}
}
if (!reachedMinOccurs) {
/*
* If we have not traversed this node in the sequence the
* minimum number of times, we cannot advance to the next
* node in the sequence.
*/
break;
}
}
break;
}
case ALL:
case SUBSTITUTION_GROUP:
case CHOICE: {
/*
* All groups only contain elements. Find one that matches. The
* max-occurrence check will confirm it wasn't already selected.
* Choice groups may have multiple paths through its children which
* are valid. In addition, a wild card ("any" element) may be a
* child of any group, thus creating another decision point.
*/
for (int stateIndex = 0; stateIndex < state.getPossibleNextStates().size(); ++stateIndex) {
final XmlSchemaStateMachineNode nextState = state.getPossibleNextStates().get(stateIndex);
if (state.getNodeType().equals(XmlSchemaStateMachineNode.Type.ALL)
&& !nextState.getNodeType().equals(XmlSchemaStateMachineNode.Type.ELEMENT)
&& !nextState.getNodeType().equals(XmlSchemaStateMachineNode.Type.SUBSTITUTION_GROUP)
&& !nextState.getNodeType().equals(XmlSchemaStateMachineNode.Type.ANY)) {
throw new IllegalStateException(
"While searching for "
+ elemQName
+ ", encountered an All group which contained a child of type "
+ nextState.getNodeType() + '.');
}
final XmlSchemaPathNode<U, V> nextPath = pathMgr.addChildNodeToPath(startNode, stateIndex);
final List<PathSegment<U, V>> choicePaths = find(nextPath, elemQName, currDepth + 1);
if (choicePaths != null) {
for (PathSegment<U, V> choicePath : choicePaths) {
choicePath.prepend(startNode, stateIndex);
}
// nextPath was cloned by all path segments, so it can be
// recycled.
pathMgr.recyclePathNode(nextPath);
if (choices == null) {
choices = choicePaths;
} else {
choices.addAll(choicePaths);
}
}
}
break;
}
case ANY: {
/*
* If the XmlSchemaAny namespace and processing rules apply, this
* element matches. False otherwise.
*/
if (traversedElements.size() < 2) {
throw new IllegalStateException("Reached a wildcard element while searching for " + elemQName
+ ", but we've only seen " + traversedElements.size()
+ " element(s)!");
}
final XmlSchemaAny any = state.getAny();
if (any.getNamespace() == null) {
choices = new ArrayList<PathSegment<U, V>>(1);
choices.add(new PathSegment<U, V>(pathMgr, startNode));
break;
}
boolean needTargetNamespace = false;
boolean matches = false;
boolean matchOnNotTargetNamespace = false;
List<String> validNamespaces = null;
if (any.getNamespace().equals("##any")) {
// Any namespace is valid. This matches.
matches = true;
} else if (any.getNamespace().equals("##other")) {
needTargetNamespace = true;
matchOnNotTargetNamespace = true;
validNamespaces = new ArrayList<String>(1);
} else {
final String[] namespaces = any.getNamespace().trim().split(" ");
validNamespaces = new ArrayList<String>(namespaces.length);
for (String namespace : namespaces) {
if ("##targetNamespace".equals(namespace)) {
needTargetNamespace = true;
} else if ("##local".equals(namespace) && (elemQName.getNamespaceURI() == null)) {
matches = true;
} else {
validNamespaces.add(namespace);
}
}
}
if (!matches) {
if (needTargetNamespace) {
validNamespaces.add(any.getTargetNamespace());
}
matches = validNamespaces.contains(elemQName.getNamespaceURI());
if (matchOnNotTargetNamespace) {
matches = !matches;
}
}
if (matches) {
choices = new ArrayList<PathSegment<U, V>>(1);
choices.add(new PathSegment<U, V>(pathMgr, startNode));
}
}
break;
default:
throw new IllegalStateException("Unrecognized node type " + state.getNodeType()
+ " when processing element " + elemQName);
}
if ((choices == null) && (currDepth > 0)) {
pathMgr.recyclePathNode(startNode);
}
return choices;
}
/*
* Walks up the tree from the current element to the prior one. Confirms the
* provided QName matches the current one before traversing. If currElem is
* null, the current position must be a wildcard element.
*/
private void walkUpTree(QName currElem) {
final XmlSchemaStateMachineNode state = currentPath.getStateMachineNode();
switch (state.getNodeType()) {
case ANY:
break;
case ELEMENT:
if (!state.getElement().getQName().equals(currElem)) {
throw new IllegalStateException("We expected to walk upwards from element " + currElem
+ ", but our current element is "
+ state.getElement().getQName());
}
break;
default:
throw new IllegalStateException("We expected to walk upwards from element " + currElem
+ ", but our current position is in a node of type "
+ state.getNodeType());
}
XmlSchemaDocumentNode<U> iter = currentPath.getDocumentNode();
XmlSchemaPathNode<U, V> path = currentPath;
do {
if (iter.getIteration() < iter.getStateMachineNode().getMaxOccurs()) {
break;
}
if (!isPositionFulfilled(path, null).equals(Fulfillment.COMPLETE)) {
break;
}
iter = iter.getParent();
if (iter == null) {
// We are exiting the root node. Nothing to see here!
break;
}
final XmlSchemaPathNode<U, V> nextPath = pathMgr
.addParentSiblingOrContentNodeToPath(path, XmlSchemaPathNode.Direction.PARENT);
path.setNextNode(-1, nextPath);
path = nextPath;
} while (!iter.getStateMachineNode().getNodeType().equals(XmlSchemaStateMachineNode.Type.ELEMENT));
currentPath = path;
}
private void walkUpToElement(QName element) {
XmlSchemaDocumentNode<U> iter = currentPath.getDocumentNode();
if (iter.getStateMachineNode().getNodeType().equals(XmlSchemaStateMachineNode.Type.ELEMENT)
&& iter.getStateMachineNode().getElement().getQName().equals(element)) {
// We are already at the element!
return;
}
do {
iter = iter.getParent();
if (iter != null) {
final XmlSchemaPathNode<U, V> nextPath = pathMgr
.addParentSiblingOrContentNodeToPath(currentPath, XmlSchemaPathNode.Direction.PARENT);
currentPath.setNextNode(-1, nextPath);
currentPath = nextPath;
}
} while ((iter != null)
&& !iter.getStateMachineNode().getNodeType().equals(XmlSchemaStateMachineNode.Type.ELEMENT));
if (!currentPath.getStateMachineNode().getNodeType().equals(XmlSchemaStateMachineNode.Type.ELEMENT)
|| !currentPath.getStateMachineNode().getElement().getQName().equals(element)) {
throw new IllegalStateException("Walked up tree and stopped at node "
+ currentPath.getStateMachineNode()
+ ", which does not represent element " + element);
}
}
private void followPath(PathSegment<U, V> path) {
switch (path.getEnd().getStateMachineNode().getNodeType()) {
case ELEMENT:
case ANY:
break;
default:
throw new IllegalStateException("Path does not end in an element or a wildcard element.");
}
// Join the start element with the new path.
XmlSchemaPathNode<U, V> startNode = path.getStart();
if (path.getAfterStart() != null) {
startNode.setNextNode(path.getAfterStartPathIndex(), path.getAfterStart());
}
pathMgr.followPath(startNode);
currentPath = path.getEnd();
}
/*
* Perhaps this would be better implemented as a bunch of starting and
* ending tags on separate lines, properly indented, to generate an XML
* document similar to the one being parsed? An idea to consider later.
*/
private String getElementsTraversedAsString() {
final StringBuilder traversed = new StringBuilder("[");
if ((traversedElements != null) && !traversedElements.isEmpty()) {
for (int i = 0; i < traversedElements.size() - 1; ++i) {
traversed.append(traversedElements.get(i)).append(" | ");
}
traversed.append(traversedElements.get(traversedElements.size() - 1));
}
traversed.append(" ]");
return traversed.toString();
}
private void verifyCurrentPositionIsAtElement(String errMsgPrefix) {
if (!currentPath.getStateMachineNode().getNodeType().equals(XmlSchemaStateMachineNode.Type.ELEMENT)
&& !currentPath.getStateMachineNode().getNodeType().equals(XmlSchemaStateMachineNode.Type.ANY)) {
throw new IllegalStateException(errMsgPrefix + " when our current position in the tree is a "
+ currentPath.getStateMachineNode().getNodeType() + '.');
}
}
private String getLeafNodeName(XmlSchemaStateMachineNode node) {
if (!node.getNodeType().equals(XmlSchemaStateMachineNode.Type.ELEMENT)
&& !node.getNodeType().equals(XmlSchemaStateMachineNode.Type.ANY)) {
throw new IllegalStateException(
"State machine node needs to be an element or a wildcard element, "
+ "not a " + currentPath.getStateMachineNode().getNodeType()
+ '.');
}
String elemName = "a wildcard element";
if (node.getNodeType().equals(XmlSchemaStateMachineNode.Type.ELEMENT)) {
elemName = node.getElement().getQName().toString();
}
return elemName;
}
private XmlSchemaStateMachineNode getStateMachineOfOwningElement() {
QName element = elementStack.get(elementStack.size() - 1);
XmlSchemaDocumentNode<U> iter = currentPath.getDocumentNode();
if (iter.getStateMachineNode().getNodeType().equals(XmlSchemaStateMachineNode.Type.ELEMENT)
&& iter.getStateMachineNode().getElement().getQName().equals(element)) {
// We are already at the element!
return currentPath.getStateMachineNode();
}
do {
iter = iter.getParent();
} while ((iter != null)
&& !iter.getStateMachineNode().getNodeType().equals(XmlSchemaStateMachineNode.Type.ELEMENT));
if (iter == null || !iter.getStateMachineNode().getNodeType().equals(XmlSchemaStateMachineNode.Type.ELEMENT)
|| !iter.getStateMachineNode().getElement().getQName().equals(element)) {
throw new IllegalStateException("Walked up tree and stopped at node "
+ currentPath.getStateMachineNode()
+ ", which does not represent element " + element);
}
return iter.getStateMachineNode();
}
private void validateAttributes(Attributes attrs) {
if (currentPath.getStateMachineNode().getNodeType().equals(XmlSchemaStateMachineNode.Type.ANY)) {
// No validation is performed on ANY elements.
return;
}
try {
XmlSchemaElementValidator.validateAttributes(currentPath.getStateMachineNode(), attrs, nsContext);
} catch (ValidationException ve) {
throw new IllegalStateException(
"Cannot validate attributes of "
+ currentPath.getStateMachineNode().getElement().getQName()
+ '.', ve);
}
}
}