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