Java tutorial
/* * 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 * <tt>xop:Include</tt> 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 * <tt>xop:Include</tt> 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 <tt>xop:Include</tt> 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; } public boolean isLoaded() { return mimePartProvider.isLoaded(contentID); } 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 * <tt>xop:Include</tt> 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 <tt>xop:Include</tt> event and return the content ID. * <p> * Precondition: The parent reader is on the START_ELEMENT event for the <tt>xop:Include</tt> * 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 * <tt>xop:Include</tt> element, i.e. the parent reader is on the END_ELEMENT event of the * element enclosing the <tt>xop:Include</tt> element. * * @return the content ID the <tt>xop:Include</tt> 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; } 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; } } public int getEventType() { return dh == null ? super.getEventType() : CHARACTERS; } 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(); } } public Object getProperty(String name) throws IllegalArgumentException { if (DataHandlerReader.PROPERTY.equals(name)) { return this; } else { return super.getProperty(name); } } 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 ""; } } } public String getPrefix() { if (dh != null) { throw new IllegalStateException(); } else { return super.getPrefix(); } } public String getNamespaceURI() { if (dh != null) { throw new IllegalStateException(); } else { return super.getNamespaceURI(); } } public String getLocalName() { if (dh != null) { throw new IllegalStateException(); } else { return super.getLocalName(); } } public QName getName() { if (dh != null) { throw new IllegalStateException(); } else { return super.getName(); } } public Location getLocation() { return super.getLocation(); } public String getNamespaceURI(String prefix) { String uri = super.getNamespaceURI(prefix); if ("xop".equals(prefix) && uri != null) { System.out.println(prefix + " -> " + uri); } return uri; } public int getNamespaceCount() { if (dh != null) { throw new IllegalStateException(); } else { return super.getNamespaceCount(); } } public String getNamespacePrefix(int index) { if (dh != null) { throw new IllegalStateException(); } else { return super.getNamespacePrefix(index); } } 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; } public String getText() { if (dh != null) { try { return toBase64(); } catch (XMLStreamException ex) { throw new RuntimeException(ex); } } else { return super.getText(); } } public char[] getTextCharacters() { if (dh != null) { try { return toBase64().toCharArray(); } catch (XMLStreamException ex) { throw new RuntimeException(ex); } } else { return super.getTextCharacters(); } } 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); } } public int getTextLength() { if (dh != null) { try { return toBase64().length(); } catch (XMLStreamException ex) { throw new RuntimeException(ex); } } else { return super.getTextLength(); } } public int getTextStart() { if (dh != null) { return 0; } else { return super.getTextStart(); } } public boolean hasText() { return dh != null || super.hasText(); } public boolean isCharacters() { return dh != null || super.isCharacters(); } public boolean isStartElement() { return dh == null && super.isStartElement(); } public boolean isEndElement() { return dh == null && super.isEndElement(); } public boolean hasName() { return dh == null && super.hasName(); } public boolean isWhiteSpace() { return dh == null && super.isWhiteSpace(); } 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); } } public boolean isBinary() { return dh != null; } public boolean isOptimized() { // xop:Include implies optimized return true; } public boolean isDeferred() { return true; } public String getContentID() { return dh.getContentID(); } public DataHandler getDataHandler() throws XMLStreamException { try { return dh.getDataHandler(); } catch (IOException ex) { throw new XMLStreamException("Failed to load MIME part '" + dh.getContentID() + "'"); } } public DataHandlerProvider getDataHandlerProvider() { return dh; } XOPEncodedStream getXOPEncodedStream() { return new XOPEncodedStream(getParent(), mimePartProvider); } }