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