blob: 110edcab4bf94c97909be9ff26c2d6c95c01a6d1 [file] [log] [blame]
/*
* 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.axiom.util.stax;
import java.io.IOException;
import java.io.Reader;
import java.util.Collections;
import javax.xml.XMLConstants;
import javax.xml.namespace.NamespaceContext;
import javax.xml.namespace.QName;
import javax.xml.stream.Location;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamReader;
import org.apache.axiom.util.namespace.MapBasedNamespaceContext;
/**
* {@link XMLStreamReader} implementation that
* represents a text node wrapped inside an element. The text data is provided by a
* {@link java.io.Reader Reader}.
* <p>
* It will produce the following sequence of XML events:
* <ul>
* <li>START_DOCUMENT</li>
* <li>START_ELEMENT</li>
* <li>(CHARACTER)*</li>
* <li>END_ELEMENT</li>
* <li>END_DOCMENT</li>
* </ul>
* The class is implemented as a simple state machine, where the state is identified
* by the current event type. The initial state is {@code START_DOCUMENT} and the
* following transitions are triggered by {@link #next()}:
* <ul>
* <li>START_DOCUMENT &rarr; START_ELEMENT</li>
* <li>START_ELEMENT &rarr; END_ELEMENT (if character stream is empty)</li>
* <li>START_ELEMENT &rarr; CHARACTERS (if character stream is not empty)</li>
* <li>CHARACTERS &rarr; CHARACTERS (if data available in stream)</li>
* <li>CHARACTERS &rarr; END_ELEMENT (if end of stream reached)</li>
* <li>END_ELEMENT &rarr; END_DOCUMENT</li>
* </ul>
* Additionally, {@link #getElementText()} triggers the following transition:
* <ul>
* <li>START_ELEMENT &rarr; END_ELEMENT</li>
* </ul>
* Note that since multiple consecutive CHARACTERS events may be returned, this
* "parser" is not coalescing.
*
*/
// TODO: this is a good candidate to implement the CharacterDataReader interface
public class WrappedTextNodeStreamReader implements XMLStreamReader {
/**
* The qualified name of the wrapper element.
*/
private final QName wrapperElementName;
/**
* The Reader object that represents the text data.
*/
private final Reader reader;
/**
* The maximum number of characters to return for each CHARACTER event.
*/
private final int chunkSize;
/**
* The type of the current XML event.
*/
private int eventType = START_DOCUMENT;
/**
* The character data for the current event. This is only set if the current
* event is a CHARACTER event. The size of the array is determined by
* {@link #chunkSize}
*/
private char[] charData;
/**
* The length of the character data in {@link #charData}.
*/
private int charDataLength;
/**
* The namespace context applicable in the scope of the wrapper element.
* Beside the default mappings for xml and xmlns, it only contains the
* mapping for the namespace of the wrapper element.
* This attribute is initialized lazily by {@link #getNamespaceContext()}.
*/
private NamespaceContext namespaceContext;
/**
* Create a new instance.
*
* @param wrapperElementName the qualified name of the wrapper element
* @param reader the Reader object holding the character data to be wrapped
* @param chunkSize the maximum number of characters that are returned for each CHARACTER event
*/
public WrappedTextNodeStreamReader(QName wrapperElementName, Reader reader, int chunkSize) {
this.wrapperElementName = wrapperElementName;
this.reader = reader;
this.chunkSize = chunkSize;
}
/**
* Create a new instance with chunk size 4096.
*
* @param wrapperElementName the qualified name of the wrapper element
* @param reader the Reader object holding the character data to be wrapped
*/
public WrappedTextNodeStreamReader(QName wrapperElementName, Reader reader) {
this(wrapperElementName, reader, 4096);
}
@Override
public Object getProperty(String name) throws IllegalArgumentException {
// We don't define any properties
return null;
}
//
// Methods to manipulate the parser state
//
@Override
public boolean hasNext() throws XMLStreamException {
return eventType != END_DOCUMENT;
}
@Override
public int next() throws XMLStreamException {
// Determine next event type based on current event type. If current event type
// is START_ELEMENT or CHARACTERS, pull new data from the reader.
switch (eventType) {
case START_DOCUMENT:
eventType = START_ELEMENT;
break;
case START_ELEMENT:
charData = new char[chunkSize];
// Fall through.
case CHARACTERS:
try {
charDataLength = reader.read(charData);
}
catch (IOException ex) {
throw new XMLStreamException(ex);
}
if (charDataLength == -1) {
charData = null;
eventType = END_ELEMENT;
} else {
eventType = CHARACTERS;
}
break;
case END_ELEMENT:
eventType = END_DOCUMENT;
break;
default:
throw new IllegalStateException();
}
return eventType;
}
@Override
public int nextTag() throws XMLStreamException {
// We don't have white space, comments or processing instructions
throw new XMLStreamException("Current event is not white space");
}
@Override
public int getEventType() {
return eventType;
}
@Override
public boolean isStartElement() { return eventType == START_ELEMENT; }
@Override
public boolean isEndElement() { return eventType == END_ELEMENT; }
@Override
public boolean isCharacters() { return eventType == CHARACTERS; }
@Override
public boolean isWhiteSpace() { return false; }
@Override
public boolean hasText() { return eventType == CHARACTERS; }
@Override
public boolean hasName() { return eventType == START_ELEMENT || eventType == END_ELEMENT; }
@Override
public void require(int type, String namespaceURI, String localName) throws XMLStreamException {
if (type != eventType
|| (namespaceURI != null && !namespaceURI.equals(getNamespaceURI()))
|| (localName != null && !namespaceURI.equals(getLocalName()))) {
throw new XMLStreamException("Unexpected event type");
}
}
@Override
public Location getLocation() {
// We do not support location information
return DummyLocation.INSTANCE;
}
@Override
public void close() throws XMLStreamException {
// Javadoc says that this method should not close the underlying input source,
// but we need to close the reader somewhere.
try {
reader.close();
}
catch (IOException ex) {
throw new XMLStreamException(ex);
}
}
//
// Methods related to the xml declaration.
//
@Override
public String getEncoding() {
// Encoding is not known (not relevant?)
return null;
}
@Override
public String getCharacterEncodingScheme() {
// Encoding is not known (not relevant?)
return null;
}
@Override
public String getVersion() {
// Version is not relevant
return null;
}
@Override
public boolean standaloneSet() {
return false;
}
@Override
public boolean isStandalone() {
return true;
}
//
// Methods related to the namespace context
//
@Override
public NamespaceContext getNamespaceContext() {
if (namespaceContext == null) {
namespaceContext = new MapBasedNamespaceContext(Collections.singletonMap(wrapperElementName.getPrefix(), wrapperElementName.getNamespaceURI()));
}
return namespaceContext;
}
@Override
public String getNamespaceURI(String prefix) {
String namespaceURI = getNamespaceContext().getNamespaceURI(prefix);
// NamespaceContext#getNamespaceURI and XMLStreamReader#getNamespaceURI have slightly
// different semantics for unbound prefixes.
return namespaceURI.equals(XMLConstants.NULL_NS_URI) ? null : prefix;
}
//
// Methods related to elements
//
private void checkStartElement() {
if (eventType != START_ELEMENT) {
throw new IllegalStateException();
}
}
@Override
public String getAttributeValue(String namespaceURI, String localName) {
checkStartElement();
return null;
}
@Override
public int getAttributeCount() {
checkStartElement();
return 0;
}
@Override
public QName getAttributeName(int index) {
checkStartElement();
throw new ArrayIndexOutOfBoundsException();
}
@Override
public String getAttributeLocalName(int index) {
checkStartElement();
throw new ArrayIndexOutOfBoundsException();
}
@Override
public String getAttributePrefix(int index) {
checkStartElement();
throw new ArrayIndexOutOfBoundsException();
}
@Override
public String getAttributeNamespace(int index) {
checkStartElement();
throw new ArrayIndexOutOfBoundsException();
}
@Override
public String getAttributeType(int index) {
checkStartElement();
throw new ArrayIndexOutOfBoundsException();
}
@Override
public String getAttributeValue(int index) {
checkStartElement();
throw new ArrayIndexOutOfBoundsException();
}
@Override
public boolean isAttributeSpecified(int index) {
checkStartElement();
throw new ArrayIndexOutOfBoundsException();
}
private void checkElement() {
if (eventType != START_ELEMENT && eventType != END_ELEMENT) {
throw new IllegalStateException();
}
}
@Override
public QName getName() {
return null;
}
@Override
public String getLocalName() {
checkElement();
return wrapperElementName.getLocalPart();
}
@Override
public String getPrefix() {
return wrapperElementName.getPrefix();
}
@Override
public String getNamespaceURI() {
checkElement();
return wrapperElementName.getNamespaceURI();
}
@Override
public int getNamespaceCount() {
checkElement();
// There is one namespace declared on the wrapper element
return 1;
}
@Override
public String getNamespacePrefix(int index) {
checkElement();
if (index == 0) {
return wrapperElementName.getPrefix();
} else {
throw new IndexOutOfBoundsException();
}
}
@Override
public String getNamespaceURI(int index) {
checkElement();
if (index == 0) {
return wrapperElementName.getNamespaceURI();
} else {
throw new IndexOutOfBoundsException();
}
}
@Override
public String getElementText() throws XMLStreamException {
if (eventType == START_ELEMENT) {
// Actually the purpose of this class is to avoid storing
// the character data entirely in memory, but if the caller
// wants a String, we don't have the choice...
try {
StringBuffer buffer = new StringBuffer();
char[] cbuf = new char[4096];
int c;
while ((c = reader.read(cbuf)) != -1) {
buffer.append(cbuf, 0, c);
}
eventType = END_ELEMENT;
return buffer.toString();
}
catch (IOException ex) {
throw new XMLStreamException(ex);
}
} else {
throw new XMLStreamException("Current event is not a START_ELEMENT");
}
}
private void checkCharacters() {
if (eventType != CHARACTERS) {
throw new IllegalStateException();
}
}
@Override
public String getText() {
checkCharacters();
return new String(charData, 0, charDataLength);
}
@Override
public char[] getTextCharacters() {
checkCharacters();
return charData;
}
@Override
public int getTextStart() {
checkCharacters();
return 0;
}
@Override
public int getTextLength() {
checkCharacters();
return charDataLength;
}
@Override
public int getTextCharacters(int sourceStart, char[] target, int targetStart, int length) throws XMLStreamException {
checkCharacters();
int c = Math.min(charDataLength-sourceStart, length);
System.arraycopy(charData, sourceStart, target, targetStart, c);
return c;
}
//
// Methods related to processing instructions
//
@Override
public String getPIData() {
throw new IllegalStateException();
}
@Override
public String getPITarget() {
throw new IllegalStateException();
}
}