blob: d58b845f518faf2284afb3955c6f1dc8b942e1ce [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.xop;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import javax.activation.DataHandler;
import javax.xml.namespace.QName;
import javax.xml.stream.Location;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamReader;
import org.apache.axiom.ext.stax.datahandler.DataHandlerProvider;
import org.apache.axiom.ext.stax.datahandler.DataHandlerReader;
import org.apache.axiom.util.base64.Base64Utils;
import org.apache.axiom.util.stax.XMLEventUtils;
import org.apache.axiom.util.stax.wrapper.XMLStreamReaderWrapper;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
/**
* {@link XMLStreamReader} wrapper that decodes XOP. It uses the extension defined by
* {@link DataHandlerReader} to expose the {@link DataHandler} objects referenced by
* {@code xop:Include} elements encountered in the underlying stream. If the consumer uses
* {@link #getText()}, {@link #getTextCharacters()},
* {@link #getTextCharacters(int, char[], int, int)} or {@link #getElementText()} when an
* {@code xop:Include} element is present in the underlying stream, then the decoder will produce
* a base64 representation of the data.
* <p>
* Note that this class only implements infoset transformation, but doesn't handle MIME processing.
* A {@link MimePartProvider} implementation must be provided to the constructor of this class. This
* object will be used to load MIME parts referenced by {@code xop:Include} elements encountered
* in the underlying stream.
* <p>
* This class supports deferred loading of MIME parts: If the consumer uses
* {@link DataHandlerReader#getDataHandlerProvider()}, then the {@link MimePartProvider} will only
* be invoked when {@link DataHandlerProvider#getDataHandler()} is called.
*/
public class XOPDecodingStreamReader extends XMLStreamReaderWrapper implements DataHandlerReader {
private static final String SOLE_CHILD_MSG =
"Expected xop:Include as the sole child of an element information item (see section " +
"3.2 of http://www.w3.org/TR/xop10/)";
private static class DataHandlerProviderImpl implements DataHandlerProvider {
private final MimePartProvider mimePartProvider;
private final String contentID;
public DataHandlerProviderImpl(MimePartProvider mimePartProvider, String contentID) {
this.mimePartProvider = mimePartProvider;
this.contentID = contentID;
}
public String getContentID() {
return contentID;
}
@Override
public DataHandler getDataHandler() throws IOException {
return mimePartProvider.getDataHandler(contentID);
}
}
private static final Log log = LogFactory.getLog(XOPDecodingStreamReader.class);
private final MimePartProvider mimePartProvider;
private DataHandlerProviderImpl dh;
private String base64;
/**
* Constructor.
*
* @param parent
* the XML stream to decode
* @param mimePartProvider
* An implementation of the {@link MimePartProvider} interface that will be used to
* load the {@link DataHandler} objects for MIME parts referenced by
* {@code xop:Include} element information items encountered in the underlying
* stream.
*/
public XOPDecodingStreamReader(XMLStreamReader parent, MimePartProvider mimePartProvider) {
super(parent);
this.mimePartProvider = mimePartProvider;
}
private void resetDataHandler() {
dh = null;
base64 = null;
}
/**
* Process an {@code xop:Include} event and return the content ID.
* <p>
* Precondition: The parent reader is on the START_ELEMENT event for the {@code xop:Include}
* element. Note that the method doesn't check this condition.
* <p>
* Postcondition: The parent reader is on the event following the END_ELEMENT event for the
* {@code xop:Include} element, i.e. the parent reader is on the END_ELEMENT event of the
* element enclosing the {@code xop:Include} element.
*
* @return the content ID the {@code xop:Include} refers to
*
* @throws XMLStreamException
*/
private String processXopInclude() throws XMLStreamException {
if (super.getAttributeCount() != 1 ||
!super.getAttributeLocalName(0).equals(XOPConstants.HREF)) {
throw new XMLStreamException("Expected xop:Include element information item with " +
"a (single) href attribute");
}
String href = super.getAttributeValue(0);
if(log.isDebugEnabled()){
log.debug("processXopInclude - found href : " + href);
}
if (!href.startsWith("cid:")) {
throw new XMLStreamException("Expected href attribute containing a URL in the " +
"cid scheme");
}
String contentID;
try {
// URIs should always be decoded using UTF-8. On the other hand, since non ASCII
// characters are not allowed in content IDs, we can simply decode using ASCII
// (which is a subset of UTF-8)
contentID = URLDecoder.decode(href.substring(4), "ascii");
if(log.isDebugEnabled()){
log.debug("processXopInclude - decoded contentID : " + contentID);
}
} catch (UnsupportedEncodingException ex) {
// We should never get here
throw new XMLStreamException(ex);
}
if (super.next() != END_ELEMENT) {
throw new XMLStreamException(
"Expected xop:Include element information item to be empty");
}
// Also consume the END_ELEMENT event of the xop:Include element. There are
// two reasons for this:
// - It allows us to validate that the message conforms to the XOP specs.
// - It makes it easier to implement the getNamespaceContext method.
if (super.next() != END_ELEMENT) {
throw new XMLStreamException(SOLE_CHILD_MSG);
}
if (log.isDebugEnabled()) {
log.debug("Encountered xop:Include for content ID '" + contentID + "'");
}
return contentID;
}
@Override
public int next() throws XMLStreamException {
boolean wasStartElement;
int event;
if (dh != null) {
resetDataHandler();
// We already advanced to the next event after the xop:Include (see below), so there
// is no call to parent.next() here
event = END_ELEMENT;
wasStartElement = false;
} else {
wasStartElement = super.getEventType() == START_ELEMENT;
event = super.next();
}
if (event == START_ELEMENT
&& super.getLocalName().equals(XOPConstants.INCLUDE)
&& super.getNamespaceURI().equals(XOPConstants.NAMESPACE_URI)) {
if (!wasStartElement) {
throw new XMLStreamException(SOLE_CHILD_MSG);
}
dh = new DataHandlerProviderImpl(mimePartProvider, processXopInclude());
return CHARACTERS;
} else {
return event;
}
}
@Override
public int getEventType() {
return dh == null ? super.getEventType() : CHARACTERS;
}
@Override
public int nextTag() throws XMLStreamException {
if (dh != null) {
resetDataHandler();
// We already advanced to the next event after the xop:Include (see the implementation
// of the next() method) and we now that it is an END_ELEMENT event.
return END_ELEMENT;
} else {
return super.nextTag();
}
}
@Override
public Object getProperty(String name) throws IllegalArgumentException {
if (DataHandlerReader.PROPERTY.equals(name)) {
return this;
} else {
return super.getProperty(name);
}
}
@Override
public String getElementText() throws XMLStreamException {
if (super.getEventType() != START_ELEMENT) {
throw new XMLStreamException("The current event is not a START_ELEMENT event");
}
int event = super.next();
// Note that an xop:Include must be the first child of the element
if (event == START_ELEMENT
&& super.getLocalName().equals(XOPConstants.INCLUDE)
&& super.getNamespaceURI().equals(XOPConstants.NAMESPACE_URI)) {
String contentID = processXopInclude();
try {
return toBase64(mimePartProvider.getDataHandler(contentID));
} catch (IOException ex) {
throw new XMLStreamException("Failed to load MIME part '" + contentID + "'", ex);
}
} else {
String text = null;
StringBuffer buffer = null;
while (event != END_ELEMENT) {
switch (event) {
case CHARACTERS:
case CDATA:
case SPACE:
case ENTITY_REFERENCE:
if (text == null && buffer == null) {
text = super.getText();
} else {
String thisText = super.getText();
if (buffer == null) {
buffer = new StringBuffer(text.length() + thisText.length());
buffer.append(text);
}
buffer.append(thisText);
}
break;
case PROCESSING_INSTRUCTION:
case COMMENT:
// Skip this event
break;
default:
throw new XMLStreamException("Unexpected event " +
XMLEventUtils.getEventTypeString(event) +
" while reading element text");
}
event = super.next();
}
if (buffer != null) {
return buffer.toString();
} else if (text != null) {
return text;
} else {
return "";
}
}
}
@Override
public String getPrefix() {
if (dh != null) {
throw new IllegalStateException();
} else {
return super.getPrefix();
}
}
@Override
public String getNamespaceURI() {
if (dh != null) {
throw new IllegalStateException();
} else {
return super.getNamespaceURI();
}
}
@Override
public String getLocalName() {
if (dh != null) {
throw new IllegalStateException();
} else {
return super.getLocalName();
}
}
@Override
public QName getName() {
if (dh != null) {
throw new IllegalStateException();
} else {
return super.getName();
}
}
@Override
public Location getLocation() {
return super.getLocation();
}
@Override
public String getNamespaceURI(String prefix) {
String uri = super.getNamespaceURI(prefix);
if ("xop".equals(prefix) && uri != null) {
System.out.println(prefix + " -> " + uri);
}
return uri;
}
@Override
public int getNamespaceCount() {
if (dh != null) {
throw new IllegalStateException();
} else {
return super.getNamespaceCount();
}
}
@Override
public String getNamespacePrefix(int index) {
if (dh != null) {
throw new IllegalStateException();
} else {
return super.getNamespacePrefix(index);
}
}
@Override
public String getNamespaceURI(int index) {
if (dh != null) {
throw new IllegalStateException();
} else {
return super.getNamespaceURI(index);
}
}
private static String toBase64(DataHandler dh) throws XMLStreamException {
try {
return Base64Utils.encode(dh);
} catch (IOException ex) {
throw new XMLStreamException("Exception when encoding data handler as base64", ex);
}
}
private String toBase64() throws XMLStreamException {
if (base64 == null) {
try {
base64 = toBase64(dh.getDataHandler());
} catch (IOException ex) {
throw new XMLStreamException("Failed to load MIME part '" + dh.getContentID() + "'", ex);
}
}
return base64;
}
@Override
public String getText() {
if (dh != null) {
try {
return toBase64();
} catch (XMLStreamException ex) {
throw new RuntimeException(ex);
}
} else {
return super.getText();
}
}
@Override
public char[] getTextCharacters() {
if (dh != null) {
try {
return toBase64().toCharArray();
} catch (XMLStreamException ex) {
throw new RuntimeException(ex);
}
} else {
return super.getTextCharacters();
}
}
@Override
public int getTextCharacters(int sourceStart, char[] target, int targetStart, int length)
throws XMLStreamException {
if (dh != null) {
String text = toBase64();
int copied = Math.min(length, text.length()-sourceStart);
text.getChars(sourceStart, sourceStart + copied, target, targetStart);
return copied;
} else {
return super.getTextCharacters(sourceStart, target, targetStart, length);
}
}
@Override
public int getTextLength() {
if (dh != null) {
try {
return toBase64().length();
} catch (XMLStreamException ex) {
throw new RuntimeException(ex);
}
} else {
return super.getTextLength();
}
}
@Override
public int getTextStart() {
if (dh != null) {
return 0;
} else {
return super.getTextStart();
}
}
@Override
public boolean hasText() {
return dh != null || super.hasText();
}
@Override
public boolean isCharacters() {
return dh != null || super.isCharacters();
}
@Override
public boolean isStartElement() {
return dh == null && super.isStartElement();
}
@Override
public boolean isEndElement() {
return dh == null && super.isEndElement();
}
@Override
public boolean hasName() {
return dh == null && super.hasName();
}
@Override
public boolean isWhiteSpace() {
return dh == null && super.isWhiteSpace();
}
@Override
public void require(int type, String namespaceURI, String localName)
throws XMLStreamException {
if (dh != null) {
if (type != CHARACTERS) {
throw new XMLStreamException("Expected CHARACTERS event");
}
} else {
super.require(type, namespaceURI, localName);
}
}
@Override
public boolean isBinary() {
return dh != null;
}
@Override
public boolean isOptimized() {
// xop:Include implies optimized
return true;
}
@Override
public boolean isDeferred() {
return true;
}
@Override
public String getContentID() {
return dh.getContentID();
}
@Override
public DataHandler getDataHandler() throws XMLStreamException{
try {
return dh.getDataHandler();
} catch (IOException ex) {
throw new XMLStreamException("Failed to load MIME part '" + dh.getContentID() + "'");
}
}
@Override
public DataHandlerProvider getDataHandlerProvider() {
return dh;
}
XOPEncodedStream getXOPEncodedStream() {
return new XOPEncodedStream(getParent(), mimePartProvider);
}
}