# AbstractXMLStreamReader.java

    /*
     * Copyright 2011, 2012 Odysseus Software GmbH
     *
     * Licensed 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.synapse.commons.staxon.core.base;

    import java.io.IOException;
    import java.util.LinkedList;
    import java.util.Queue;

    import javax.xml.XMLConstants;
    import javax.xml.namespace.NamespaceContext;
    import javax.xml.namespace.QName;
    import javax.xml.stream.Location;
    import javax.xml.stream.XMLStreamConstants;
    import javax.xml.stream.XMLStreamException;
    import javax.xml.stream.XMLStreamReader;

    /**
     * Abstract XML stream reader.
     */
    public abstract class AbstractXMLStreamReader<T> implements XMLStreamReader {
        class Event {
            private final int type;
            private final XMLStreamReaderScope<T> scope;
            private final String text;
            private final Object data;
            private final int lineNumber;
            private final int columnNumber;
            private final int characterOffset;

            Event(int type, XMLStreamReaderScope<T> scope) {
                this(type, scope, null, null);
            }

            Event(int type, XMLStreamReaderScope<T> scope, String text, Object data) {
                this.type = type;
                this.scope = scope;
                this.text = text;
                this.data = data;
                this.lineNumber = locationProvider.getLineNumber();
                this.columnNumber = locationProvider.getColumnNumber();
                this.characterOffset = locationProvider.getCharacterOffset();
            }

            XMLStreamReaderScope<T> getScope() {
                return scope;
            }

            int getType() {
                return type;
            }

            String getText() {
                return text;
            }

            Object getData() {
                return data;
            }

            Location getLocation() {
                return new Location() {
                    @Override
                    public int getLineNumber() {
                        return lineNumber;
                    }

                    @Override
                    public int getColumnNumber() {
                        return columnNumber;
                    }

                    @Override
                    public int getCharacterOffset() {
                        return characterOffset;
                    }

                    @Override
                    public String getPublicId() {
                        return locationProvider.getPublicId();
                    }

                    @Override
                    public String getSystemId() {
                        return locationProvider.getSystemId();
                    }
                };
            }

            @Override
            public String toString() {
                return new StringBuilder()
                        .append(getEventName(type))
                        .append(": ").append(getText() != null ? getText() : getLocalName())
                        .toString();
            }
        }

        static String getEventName(int type) {
            switch (type) {
                case XMLStreamConstants.ATTRIBUTE:
                    return "ATTRIBUTE";
                case XMLStreamConstants.CDATA:
                    return "CDATA";
                case XMLStreamConstants.CHARACTERS:
                    return "CHARACTERS";
                case XMLStreamConstants.COMMENT:
                    return "COMMENT";
                case XMLStreamConstants.DTD:
                    return "DTD";
                case XMLStreamConstants.END_DOCUMENT:
                    return "END_DOCUMENT";
                case XMLStreamConstants.END_ELEMENT:
                    return "END_ELEMENT";
                case XMLStreamConstants.ENTITY_DECLARATION:
                    return "ENTITY_DECLARATION";
                case XMLStreamConstants.ENTITY_REFERENCE:
                    return "ENTITY_REFERENCE";
                case XMLStreamConstants.NAMESPACE:
                    return "NAMESPACE";
                case XMLStreamConstants.NOTATION_DECLARATION:
                    return "NOTATION_DECLARATION";
                case XMLStreamConstants.PROCESSING_INSTRUCTION:
                    return "PROCESSING_INSTRUCTION";
                case XMLStreamConstants.SPACE:
                    return "SPACE";
                case XMLStreamConstants.START_DOCUMENT:
                    return "START_DOCUMENT";
                case XMLStreamConstants.START_ELEMENT:
                    return "START_ELEMENT";
                default:
                    return String.valueOf(type); // should not happen...
            }
        }

        static boolean hasData(int type) {
            return type == XMLStreamConstants.CHARACTERS
                    || type == XMLStreamConstants.COMMENT
                    || type == XMLStreamConstants.CDATA
                    || type == XMLStreamConstants.DTD
                    || type == XMLStreamConstants.ENTITY_REFERENCE
                    || type == XMLStreamConstants.SPACE;
        }

        private static final Location UNKNOWN_LOCATION = new Location() {
            @Override
            public int getCharacterOffset() {
                return -1;
            }

            @Override
            public int getColumnNumber() {
                return -1;
            }

            @Override
            public int getLineNumber() {
                return -1;
            }

            @Override
            public String getPublicId() {
                return null;
            }

            @Override
            public String getSystemId() {
                return null;
            }
        };

        private final Queue<Event> queue = new LinkedList<Event>();
        private final Location locationProvider;

        private XMLStreamReaderScope<T> scope;
        private boolean moreTokens;
        private Event event;
        private boolean startDocumentRead;

        private String encodingScheme;
        private String version;
        private Boolean standalone;

        /**
         * Create new reader instance.
         *
         * @param rootInfo root scope information
         */
        public AbstractXMLStreamReader(T rootInfo) {
            this(rootInfo, UNKNOWN_LOCATION);
        }

        /**
         * Create new reader instance.
         *
         * @param rootInfo root scope information
         */
        public AbstractXMLStreamReader(T rootInfo, Location locationProvider) {
            this.scope = new XMLStreamReaderScope<T>(XMLConstants.NULL_NS_URI, rootInfo);
            this.locationProvider = locationProvider;
        }

        private void ensureStartTagClosed() throws XMLStreamException {
            if (!scope.isStartTagClosed()) {
                scope.setStartTagClosed(true);
            }
        }

        /**
         * @return current scope
         */
        protected XMLStreamReaderScope<T> getScope() {
            return scope;
        }

        /**
         * @return <code>true</code> if <code>START_DOCUMENT</code> event has been read
         */
        protected boolean isStartDocumentRead() {
            return startDocumentRead;
        }

        /**
         * Consume initial event.
         * This method must be called by subclasses prior to any use of an instance (typically in constructor).
         *
         * @throws XMLStreamException
         */
        protected void initialize() throws XMLStreamException {
            try {
                moreTokens = consume();
            } catch (IOException e) {
                throw new XMLStreamException(e);
            }

            if (hasNext()) {
                event = queue.remove();
            } else {
                event = new Event(XMLStreamConstants.END_DOCUMENT, scope);
            }
        }

        /**
         * Main method to be implemented by subclasses.
         * This method is called by the reader when the event queue runs dry.
         * Consume some events and delegate to the various <code>readXXX()</code> methods.
         * When encountering an element start event, all attributes and namespace delarations
         * must be consumed too, otherwise these won't be available during start element.
         *
         * @return <code>true</code> if there's more to read
         * @throws XMLStreamException
         * @throws IOException
         */
        protected abstract boolean consume() throws XMLStreamException, IOException;

        /**
         * Read start document
         *
         * @param version        XML version
         * @param encodingScheme encoding scheme (may be <code>null</code>)
         * @param standalone     standalone flag (may be <code>null</code>)
         */
        protected void readStartDocument(String version, String encodingScheme, Boolean standalone) throws XMLStreamException {
            if (startDocumentRead || !scope.isRoot()) {
                throw new XMLStreamException("Cannot start document", locationProvider);
            }
            queue.add(new Event(XMLStreamConstants.START_DOCUMENT, scope));
            startDocumentRead = true;

            this.version = version;
            this.encodingScheme = encodingScheme;
            this.standalone = standalone;
        }

        /**
         * Read start element.
         * A new scope is created and made the current scope. The provided <code>scopeInfo</code> is
         * stored in the new scope and will be available via <code>getScope().getInfo()</code>.
         *
         * @param prefix       element prefix (use <code>null</code> if unknown)
         * @param localName    local name
         * @param namespaceURI (use <code>null</code> if unknown)
         * @param scopeInfo    new scope info
         * @throws XMLStreamException
         */
        protected void readStartElementTag(String prefix, String localName, String namespaceURI, T scopeInfo) throws XMLStreamException {
            if (prefix == null && namespaceURI == null) {
                throw new IllegalArgumentException("at least one of prefix and namespaceURI must not be null!");
            }
            ensureStartTagClosed();
            scope = new XMLStreamReaderScope<T>(scope, prefix, localName, namespaceURI);
            scope.setInfo(scopeInfo);
            queue.add(new Event(XMLStreamConstants.START_ELEMENT, scope));
        }

        /**
         * Read attribute.
         *
         * @param prefix       attribute prefix (use <code>null</code> if unknown)
         * @param localName    local name
         * @param namespaceURI (use <code>null</code> if unknown)
         * @param value        attribute value
         * @throws XMLStreamException
         */
        protected void readAttr(String prefix, String localName, String namespaceURI, String value) throws XMLStreamException {
            if (scope.isStartTagClosed()) {
                throw new XMLStreamException("Cannot read attribute: element has children or text", locationProvider);
            }
            if (prefix == null && namespaceURI == null) {
                throw new IllegalArgumentException("at least one of prefix and namespaceURI must not be null!");
            }
            scope.addAttribute(prefix, localName, namespaceURI, value);
        }

        /**
         * Read namespace declaration.
         *
         * @param prefix       namespace prefix (must not be <code>null</code>)
         * @param namespaceURI namespace URI (must not be <code>null</code>)
         * @throws XMLStreamException
         */
        protected void readNsDecl(String prefix, String namespaceURI) throws XMLStreamException {
            if (scope.isStartTagClosed()) {
                throw new XMLStreamException("Cannot read namespace: element has children or text", locationProvider);
            }
            if (prefix == null || namespaceURI == null) {
                throw new IllegalArgumentException("at least one of prefix and namespaceURI must not be null!");
            }
            scope.addNamespaceURI(prefix, namespaceURI);
            scope.setPrefix(prefix, namespaceURI);
        }

        /**
         * Read characters/comment/dtd/entity data.
         *
         * @param text text
         * @param data additional data exposed by {@link #getEventData()} (e.g. type conversion)
         * @param type one of <code>CHARACTERS, COMMENT, CDATA, DTD, ENTITY_REFERENCE, SPACE</code>
         * @throws XMLStreamException
         */
        protected void readData(String text, Object data, int type) throws XMLStreamException {
            if (hasData(type)) {
                ensureStartTagClosed();
                queue.add(new Event(type, scope, text, data));
            } else {
                throw new XMLStreamException("Unexpected event type " + getEventName(), locationProvider);
            }
        }

        /**
         * Read processing instruction.
         *
         * @param target PI target
         * @param data   PI data (may be <code>null</code>)
         * @throws XMLStreamException
         */
        protected void readPI(String target, String data) throws XMLStreamException {
            ensureStartTagClosed();
            String text = data == null ? target : target + ':' + data;
            queue.add(new Event(XMLStreamConstants.PROCESSING_INSTRUCTION, scope, text, null));
        }

        /**
         * Read end element.
         * This will pop the current scope and make its parent the new current scope.
         *
         * @throws XMLStreamException
         */
        protected void readEndElementTag() throws XMLStreamException {
            ensureStartTagClosed();
            queue.add(new Event(XMLStreamConstants.END_ELEMENT, scope));
            scope = scope.getParent();
        }

        /**
         * Read end document.
         */
        protected void readEndDocument() throws XMLStreamException {
            if (!startDocumentRead || !scope.isRoot()) {
                throw new XMLStreamException("Cannot end document", locationProvider);
            }
            queue.add(new Event(XMLStreamConstants.END_DOCUMENT, scope));
            startDocumentRead = false;
        }

        @Override
        public void require(int eventType, String namespaceURI, String localName) throws XMLStreamException {
            if (eventType != getEventType()) {
                throw new XMLStreamException("Expected event type " + getEventName(eventType) + ", was " + getEventName(getEventType()), getLocation());
            }
            if (namespaceURI != null && !namespaceURI.equals(getNamespaceURI())) {
                throw new XMLStreamException("Expected namespace " + namespaceURI + ", was " + getNamespaceURI(), getLocation());
            }
            if (localName != null && !localName.equals(getLocalName())) {
                throw new XMLStreamException("Expected local name " + localName + ", was " + getLocalName(), getLocation());
            }
        }

        @Override
        public String getElementText() throws XMLStreamException {
            require(XMLStreamConstants.START_ELEMENT, null, null);
            StringBuilder builder = null;
            String leadText = null;
            while (true) {
                switch (next()) {
                    case XMLStreamConstants.CHARACTERS:
                    case XMLStreamConstants.CDATA:
                    case XMLStreamConstants.SPACE:
                    case XMLStreamConstants.ENTITY_REFERENCE:
                        if (leadText == null) { // first event?
                            leadText = getText();
                        } else {
                            if (builder == null) { // second event?
                                builder = new StringBuilder(leadText);
                            }
                            builder.append(getText());
                        }
                        break;
                    case XMLStreamConstants.PROCESSING_INSTRUCTION:
                    case XMLStreamConstants.COMMENT:
                        break;
                    case XMLStreamConstants.END_ELEMENT:
                        return builder == null ? leadText : builder.toString();
                    default:
                        throw new XMLStreamException("Unexpected event type " + getEventName(), getLocation());
                }
            }
        }

        @Override
        public boolean hasNext() throws XMLStreamException {
            try {
                while (queue.isEmpty() && moreTokens) {
                    moreTokens = consume();
                }
            } catch (IOException e) {
                throw new XMLStreamException(e.getMessage(), locationProvider, e);
            }
            return !queue.isEmpty();
        }

        @Override
        public int next() throws XMLStreamException {
            if (!hasNext()) {
                throw new IllegalStateException("No more events");
            }
            event = queue.remove();
            return event.getType();
        }

        @Override
        public int nextTag() throws XMLStreamException {
            int eventType = next();
            while ((eventType == XMLStreamConstants.CHARACTERS && isWhiteSpace()) // skip whitespace
                    || (eventType == XMLStreamConstants.CDATA && isWhiteSpace()) // skip whitespace
                    || eventType == XMLStreamConstants.SPACE
                    || eventType == XMLStreamConstants.PROCESSING_INSTRUCTION
                    || eventType == XMLStreamConstants.COMMENT) {
                eventType = next();
            }
            if (!isStartElement() && !isEndElement()) {
                throw new XMLStreamException("expected start or end tag", getLocation());
            }
            return eventType;
        }

        @Override
        public void close() throws XMLStreamException {
            scope = null;
            queue.clear();
        }

        @Override
        public boolean isStartElement() {
            return getEventType() == XMLStreamConstants.START_ELEMENT;
        }

        @Override
        public boolean isEndElement() {
            return getEventType() == XMLStreamConstants.END_ELEMENT;
        }

        @Override
        public boolean isCharacters() {
            return getEventType() == XMLStreamConstants.CHARACTERS;
        }

        @Override
        public boolean isWhiteSpace() {
            if (getEventType() == XMLStreamConstants.CHARACTERS || getEventType() == XMLStreamConstants.CDATA) {
                for (char ch : getText().toCharArray()) {
                    if (!Character.isWhitespace(ch)) {
                        return false;
                    }
                }
                return true;
            }
            return false;
        }

        @Override
        public int getAttributeCount() {
            return event.getScope().getAttributeCount();
        }

        @Override
        public QName getAttributeName(int index) {
            return event.getScope().getAttributeName(index);
        }

        @Override
        public String getAttributeLocalName(int index) {
            return getAttributeName(index).getLocalPart();
        }

        @Override
        public String getAttributeValue(int index) {
            return event.getScope().getAttributeValue(index);
        }

        @Override
        public String getAttributePrefix(int index) {
            return getAttributeName(index).getPrefix();
        }

        @Override
        public String getAttributeNamespace(int index) {
            return getAttributeName(index).getNamespaceURI();
        }

        @Override
        public String getAttributeType(int index) {
            return null;
        }

        @Override
        public boolean isAttributeSpecified(int index) {
            return index < getAttributeCount();
        }

        @Override
        public String getAttributeValue(String namespaceURI, String localName) {
            return event.getScope().getAttributeValue(namespaceURI, localName);
        }

        @Override
        public String getNamespaceURI(String prefix) {
            return event.getScope().getNamespaceURI(prefix);
        }

        @Override
        public int getNamespaceCount() {
            return hasName() ? event.getScope().getNamespaceCount() : 0;
        }

        @Override
        public String getNamespacePrefix(int index) {
            return hasName() ? event.getScope().getNamespacePrefix(index) : null;
        }

        @Override
        public String getNamespaceURI(int index) {
            return hasName() ? event.getScope().getNamespaceURI(index) : null;
        }

        @Override
        public NamespaceContext getNamespaceContext() {
            return event.getScope();
        }

        @Override
        public int getEventType() {
            return event.getType();
        }

        protected String getEventName() {
            return getEventName(getEventType());
        }

        /**
         * @return raw event data
         */
        protected final Object getEventData() {
            return event.getData();
        }

        @Override
        public Location getLocation() {
            return event.getLocation();
        }

        @Override
        public boolean hasText() {
            return hasData(getEventType());
        }

        @Override
        public String getText() {
            return hasText() ? event.getText() : null;
        }

        @Override
        public char[] getTextCharacters() {
            return hasText() ? event.getText().toCharArray() : null;
        }

        @Override
        public int getTextCharacters(int sourceStart, char[] target, int targetStart, int length) throws XMLStreamException {
            int count = Math.min(length, getTextLength());
            if (count > 0) {
                System.arraycopy(getTextCharacters(), sourceStart, target, targetStart, count);
            }
            return count;
        }

        @Override
        public int getTextStart() {
            return 0;
        }

        @Override
        public int getTextLength() {
            return hasText() ? event.getText().length() : 0;
        }

        @Override
        public boolean hasName() {
            return getEventType() == XMLStreamConstants.START_ELEMENT || getEventType() == XMLStreamConstants.END_ELEMENT;
        }

        @Override
        public QName getName() {
            return hasName() ? new QName(getNamespaceURI(), getLocalName(), getPrefix()) : null;
        }

        @Override
        public String getLocalName() {
            return hasName() ? event.getScope().getLocalName() : null;
        }

        @Override
        public String getNamespaceURI() {
            return hasName() ? event.getScope().getNamespaceURI() : null;
        }

        @Override
        public String getPrefix() {
            return hasName() ? event.getScope().getPrefix() : null;
        }

        @Override
        public String getVersion() {
            return version;
        }

        @Override
        public String getEncoding() {
            return null;
        }

        @Override
        public boolean isStandalone() {
            return standaloneSet() ? standalone.booleanValue() : false;
        }

        @Override
        public boolean standaloneSet() {
            return standalone != null;
        }

        @Override
        public String getCharacterEncodingScheme() {
            return encodingScheme;
        }

        @Override
        public String getPITarget() {
            if (event.getType() != XMLStreamConstants.PROCESSING_INSTRUCTION) {
                return null;
            }
            int colon = event.getText().indexOf(':');
            return colon < 0 ? event.getText() : event.getText().substring(0, colon);
        }

        @Override
        public String getPIData() {
            if (event.getType() != XMLStreamConstants.PROCESSING_INSTRUCTION) {
                return null;
            }
            int colon = event.getText().indexOf(':');
            return colon < 0 ? null : event.getText().substring(colon + 1);
        }

        @Override
        public Object getProperty(String name) throws IllegalArgumentException {
            throw new IllegalArgumentException("Unsupported property: " + name);
        }

        @Override
        public String toString() {
            return getClass().getSimpleName() + "(" + getEventName() + ")";
        }
    }