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