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