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.attachments; import org.apache.axiom.attachments.impl.PartFactory; import org.apache.axiom.attachments.lifecycle.LifecycleManager; import org.apache.axiom.attachments.lifecycle.impl.LifecycleManagerImpl; import org.apache.axiom.om.OMAttachmentAccessor; import org.apache.axiom.om.OMException; import org.apache.axiom.om.impl.MTOMConstants; import org.apache.axiom.om.util.DetachableInputStream; import org.apache.axiom.util.UIDGenerator; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import javax.activation.DataHandler; import javax.mail.MessagingException; import javax.mail.internet.ContentType; import javax.mail.internet.ParseException; import java.io.IOException; import java.io.InputStream; import java.io.PushbackInputStream; import java.io.UnsupportedEncodingException; import java.util.ArrayList; import java.util.List; import java.util.Set; import java.util.TreeMap; import java.util.Map; import java.util.Collections; public class Attachments implements OMAttachmentAccessor { /** <code>ContentType</code> of the MIME message */ ContentType contentType; int contentLength; // Content Length /** Mime <code>boundary</code> which separates mime parts */ byte[] boundary; /** * <code>applicationType</code> used to distinguish between MTOM & SWA If the message is MTOM * optimised type is application/xop+xml If the message is SWA, type is ??have to find out */ String applicationType; /** * <code>pushbackInStream</code> stores the reference to the incoming stream A PushbackStream * has the ability to "push back" or "unread" one byte. */ PushbackInputStream pushbackInStream; int PUSHBACK_SIZE = 4 * 1024; DetachableInputStream filterIS = null; /** * <code>attachmentsMap</code> stores the Data Handlers of the already parsed Mime Body Parts. * This ordered Map is keyed using the content-ID's. */ TreeMap attachmentsMap; /** * <code>cids</code> stores the content ids in the order that the attachments * occur in the message */ ArrayList cids = new ArrayList(); /** <code>partIndex</code>- Number of Mime parts parsed */ int partIndex = 0; /** Container to hold streams for direct access */ IncomingAttachmentStreams streams = null; /** <code>boolean</code> Indicating if any streams have been directly requested */ private boolean streamsRequested = false; /** <code>boolean</code> Indicating if any data handlers have been directly requested */ private boolean partsRequested = false; /** * <code>endOfStreamReached</code> flag which is to be set by MIMEBodyPartStream when MIME * message terminator is found. */ private boolean endOfStreamReached; /** * <code>noStreams</code> flag which is to be set when this class is instantiated by the SwA API * to handle programatic added attachements. An InputStream with attachments is not present at * that occation. */ private boolean noStreams = false; private String firstPartId; private boolean fileCacheEnable; private String attachmentRepoDir; private int fileStorageThreshold; private LifecycleManager manager; protected static Log log = LogFactory.getLog(Attachments.class); public LifecycleManager getLifecycleManager() { if (manager == null) { manager = new LifecycleManagerImpl(); } return manager; } public void setLifecycleManager(LifecycleManager manager) { this.manager = manager; } /** * Moves the pointer to the beginning of the first MIME part. Reads till first MIME boundary is * found or end of stream is reached. * * @param inStream * @param contentTypeString * @param fileCacheEnable * @param attachmentRepoDir * @throws OMException */ public Attachments(LifecycleManager manager, InputStream inStream, String contentTypeString, boolean fileCacheEnable, String attachmentRepoDir, String fileThreshold) throws OMException { this(manager, inStream, contentTypeString, fileCacheEnable, attachmentRepoDir, fileThreshold, 0); } /** * Moves the pointer to the beginning of the first MIME part. Reads * till first MIME boundary is found or end of stream is reached. * * @param inStream * @param contentTypeString * @param fileCacheEnable * @param attachmentRepoDir * @param fileThreshold * @param contentLength * @throws OMException */ public Attachments(LifecycleManager manager, InputStream inStream, String contentTypeString, boolean fileCacheEnable, String attachmentRepoDir, String fileThreshold, int contentLength) throws OMException { this.manager = manager; this.contentLength = contentLength; this.attachmentRepoDir = attachmentRepoDir; this.fileCacheEnable = fileCacheEnable; if (log.isDebugEnabled()) { log.debug("Attachments contentLength=" + contentLength + ", contentTypeString=" + contentTypeString); } if (fileThreshold != null && (!"".equals(fileThreshold))) { this.fileStorageThreshold = Integer.parseInt(fileThreshold); } else { this.fileStorageThreshold = 1; } attachmentsMap = new TreeMap(); try { contentType = new ContentType(contentTypeString); } catch (ParseException e) { throw new OMException("Invalid Content Type Field in the Mime Message", e); } // REVIEW: This conversion is hard-coded to UTF-8. // The complete solution is to respect the charset setting of the message. // However this may cause problems in BoundaryDelimittedStream and other // lower level classes. // Boundary always have the prefix "--". try { String encoding = contentType.getParameter("charset"); if (encoding == null || encoding.length() == 0) { encoding = "UTF-8"; } String boundaryParam = contentType.getParameter("boundary"); if (boundaryParam == null) { throw new OMException("Content-type has no 'boundary' parameter"); } this.boundary = ("--" + boundaryParam).getBytes(encoding); if (log.isDebugEnabled()) { log.debug("boundary=" + new String(this.boundary)); } } catch (UnsupportedEncodingException e) { throw new OMException(e); } // If the length is not known, install a TeeInputStream // so that we can retrieve it later. InputStream is = inStream; if (contentLength <= 0) { filterIS = new DetachableInputStream(inStream); is = filterIS; } pushbackInStream = new PushbackInputStream(is, PUSHBACK_SIZE); // Move the read pointer to the beginning of the first part // read till the end of first boundary while (true) { int value; try { value = pushbackInStream.read(); if ((byte) value == boundary[0]) { int boundaryIndex = 0; while ((boundaryIndex < boundary.length) && ((byte) value == boundary[boundaryIndex])) { value = pushbackInStream.read(); if (value == -1) { throw new OMException( "Unexpected End of Stream while searching for first Mime Boundary"); } boundaryIndex++; } if (boundaryIndex == boundary.length) { // boundary found pushbackInStream.read(); break; } } else if (value == -1) { throw new OMException("Mime parts not found. Stream ended while searching for the boundary"); } } catch (IOException e1) { throw new OMException("Stream Error" + e1.toString(), e1); } } // Read the SOAP part and cache it getDataHandler(getSOAPPartContentID()); // Now reset partsRequested. SOAP part is a special case which is always // read beforehand, regardless of request. partsRequested = false; } /** * Moves the pointer to the beginning of the first MIME part. Reads till first MIME boundary is * found or end of stream is reached. * * @param inStream * @param contentTypeString * @param fileCacheEnable * @param attachmentRepoDir * @throws OMException */ public Attachments(InputStream inStream, String contentTypeString, boolean fileCacheEnable, String attachmentRepoDir, String fileThreshold) throws OMException { this(null, inStream, contentTypeString, fileCacheEnable, attachmentRepoDir, fileThreshold, 0); } /** * Moves the pointer to the beginning of the first MIME part. Reads * till first MIME boundary is found or end of stream is reached. * * @param inStream * @param contentTypeString * @param fileCacheEnable * @param attachmentRepoDir * @param fileThreshold * @param contentLength * @throws OMException */ public Attachments(InputStream inStream, String contentTypeString, boolean fileCacheEnable, String attachmentRepoDir, String fileThreshold, int contentLength) throws OMException { this(null, inStream, contentTypeString, fileCacheEnable, attachmentRepoDir, fileThreshold, contentLength); } /** * Sets file cache to false. * * @param inStream * @param contentTypeString * @throws OMException */ public Attachments(InputStream inStream, String contentTypeString) throws OMException { this(null, inStream, contentTypeString, false, null, null); } /** * Use this constructor when instantiating this to store the attachments set programatically * through the SwA API. */ public Attachments() { attachmentsMap = new TreeMap(); noStreams = true; } /** * Identify the type of message (MTOM or SOAP with attachments) represented by this * object. * * @return One of the {@link MTOMConstants#MTOM_TYPE}, {@link MTOMConstants#SWA_TYPE} * or {@link MTOMConstants#SWA_TYPE_12} constants. * @throws OMException if the message doesn't have one of the supported types, i.e. is * neither MTOM nor SOAP with attachments */ public String getAttachmentSpecType() { if (this.applicationType == null) { applicationType = contentType.getParameter("type"); if ((MTOMConstants.MTOM_TYPE).equalsIgnoreCase(applicationType)) { this.applicationType = MTOMConstants.MTOM_TYPE; } else if ((MTOMConstants.SWA_TYPE).equalsIgnoreCase(applicationType)) { this.applicationType = MTOMConstants.SWA_TYPE; } else if ((MTOMConstants.SWA_TYPE_12).equalsIgnoreCase(applicationType)) { this.applicationType = MTOMConstants.SWA_TYPE_12; } else { throw new OMException("Invalid Application type. Support available for MTOM & SwA only."); } } return this.applicationType; } /** * Get the {@link DataHandler} object for the MIME part with a given content ID. * * @param contentID * the raw content ID (without the surrounding angle brackets and <tt>cid:</tt> * prefix) of the MIME part * @return the {@link DataHandler} of the MIME part referred by the content ID or * <code>null</code> if the MIME part referred by the content ID does not exist */ public DataHandler getDataHandler(String contentID) { // Check whether the MIME part is already parsed by checking the attachments HashMap. If it is // not parsed yet then call the getNextPart() till the required part is found. DataHandler dataHandler; if (attachmentsMap.containsKey(contentID)) { dataHandler = (DataHandler) attachmentsMap.get(contentID); return dataHandler; } else if (!noStreams) { //This loop will be terminated by the Exceptions thrown if the Mime // part searching was not found while ((dataHandler = this.getNextPartDataHandler()) != null) { if (attachmentsMap.containsKey(contentID)) { dataHandler = (DataHandler) attachmentsMap.get(contentID); return dataHandler; } } } return null; } /** * Programatically adding an SOAP with Attachments(SwA) Attachment. These attachments will get * serialized only if SOAP with Attachments is enabled. * * @param contentID * @param dataHandler */ public void addDataHandler(String contentID, DataHandler dataHandler) { attachmentsMap.put(contentID, dataHandler); if (!cids.contains(contentID)) { cids.add(contentID); } } /** * Removes the DataHandler corresponding to the given contenID. If it is not present, then * trying to find it calling the getNextPart() till the required part is found. * * @param blobContentID */ public void removeDataHandler(String blobContentID) { if (attachmentsMap.containsKey(blobContentID)) { attachmentsMap.remove(blobContentID); } else if (!noStreams) { //This loop will be terminated by the Exceptions thrown if the Mime // part searching was not found while (this.getNextPartDataHandler() != null) { if (attachmentsMap.containsKey(blobContentID)) { attachmentsMap.remove(blobContentID); } } } if (cids.contains(blobContentID)) { cids.remove(blobContentID); } } /** * @return the InputStream which includes the SOAP Envelope. It assumes that the root mime part * is always pointed by "start" parameter in content-type. */ public InputStream getSOAPPartInputStream() throws OMException { DataHandler dh; if (noStreams) { throw new OMException("Invalid operation. Attachments are created programatically."); } try { dh = getDataHandler(getSOAPPartContentID()); if (dh == null) { throw new OMException("Mandatory Root MIME part containing the SOAP Envelope is missing"); } return dh.getInputStream(); } catch (IOException e) { throw new OMException("Problem with DataHandler of the Root Mime Part. ", e); } } /** * Get the content ID of the SOAP part or the MIME message. This content ID is determined as * follows: * <ul> * <li>If the content type of the MIME message has a <tt>start</tt> parameter, then the content * ID will be extracted from that parameter. * <li>Otherwise the content ID of the first MIME part of the MIME message is returned. * </ul> * * @return the content ID of the SOAP part (without the surrounding angle brackets) */ public String getSOAPPartContentID() { if (contentType == null) { return null; } String rootContentID = contentType.getParameter("start"); if (log.isDebugEnabled()) { log.debug("getSOAPPartContentID rootContentID=" + rootContentID); } // to handle the Start parameter not mentioned situation if (rootContentID == null) { if (partIndex == 0) { getNextPartDataHandler(); } rootContentID = firstPartId; } else { rootContentID = rootContentID.trim(); if ((rootContentID.indexOf("<") > -1) & (rootContentID.indexOf(">") > -1)) { rootContentID = rootContentID.substring(1, (rootContentID.length() - 1)); } } // Strips off the "cid:" part from content-id if (rootContentID.length() > 4 && "cid:".equalsIgnoreCase(rootContentID.substring(0, 4))) { rootContentID = rootContentID.substring(4); } return rootContentID; } /** * Get the content type of the SOAP part of the MIME message. * * @return the content type of the SOAP part * @throws OMException * if the content type could not be determined */ public String getSOAPPartContentType() { if (!noStreams) { String soapPartContentID = getSOAPPartContentID(); if (soapPartContentID == null) { throw new OMException("Unable to determine the content ID of the SOAP part"); } DataHandler soapPart = getDataHandler(soapPartContentID); if (soapPart == null) { throw new OMException("Unable to locate the SOAP part; content ID was " + soapPartContentID); } return soapPart.getContentType(); } else { throw new OMException("The attachments map was created programatically. Unsupported operation."); } } /** * Stream based access * * @return The stream container of type <code>IncomingAttachmentStreams</code> * @throws IllegalStateException if application has alreadt started using Part's directly */ public IncomingAttachmentStreams getIncomingAttachmentStreams() throws IllegalStateException { if (partsRequested) { throw new IllegalStateException( "The attachments stream can only be accessed once; either by using the IncomingAttachmentStreams class or by getting a " + "collection of AttachmentPart objects. They cannot both be called within the life time of the same service request."); } if (noStreams) { throw new IllegalStateException( "The attachments map was created programatically. No streams are available."); } streamsRequested = true; if (this.streams == null) { BoundaryDelimitedStream boundaryDelimitedStream = new BoundaryDelimitedStream(pushbackInStream, boundary, 1024); this.streams = new MultipartAttachmentStreams(boundaryDelimitedStream); } return this.streams; } /** * Force reading of all attachments. */ private void fetchAllParts() { DataHandler dataHandler; while (!noStreams) { dataHandler = this.getNextPartDataHandler(); if (dataHandler == null) { break; } } } /** * Get the content IDs of all MIME parts in the message. This includes the content ID of the * SOAP part as well as the content IDs of the attachments. Note that if this object has been * created from a stream, a call to this method will force reading of all MIME parts that * have not been fetched from the stream yet. * * @return an array with the content IDs in order of appearance in the message */ public String[] getAllContentIDs() { fetchAllParts(); return (String[]) cids.toArray(new String[cids.size()]); } /** * Get the content IDs of all MIME parts in the message. This includes the content ID of the * SOAP part as well as the content IDs of the attachments. Note that if this object has been * created from a stream, a call to this method will force reading of all MIME parts that * have not been fetched from the stream yet. * * @return the set of content IDs */ public Set getContentIDSet() { fetchAllParts(); return attachmentsMap.keySet(); } /** * Get a map of all MIME parts in the message. This includes the SOAP part as well as the * attachments. Note that if this object has been created from a stream, a call to this * method will force reading of all MIME parts that have not been fetched from the stream yet. * * @return A map of all MIME parts in the message, with content IDs as keys and * {@link DataHandler} objects as values. */ public Map getMap() { fetchAllParts(); return Collections.unmodifiableMap(attachmentsMap); } /** * Get the content IDs of the already loaded MIME parts in the message. This includes the * content ID of the SOAP part as well as the content IDs of the attachments. If this * object has been created from a stream, only the content IDs of the MIME parts that * have already been fetched from the stream are returned. If this is not the desired * behavior, {@link #getAllContentIDs()} or {@link #getContentIDSet()} should be used * instead. * * @return List of content IDs in order of appearance in message */ public List getContentIDList() { return cids; } /** * If the Attachments is backed by an InputStream, then this * method returns the length of the message contents * (Length of the entire message - Length of the Transport Headers) * @return length of message content or -1 if Attachments is not * backed by an InputStream */ public long getContentLength() throws IOException { if (contentLength > 0) { return contentLength; } else if (filterIS != null) { // Ensure all parts are read this.getContentIDSet(); // Now get the count from the filter return filterIS.length(); } else { return -1; // not backed by an input stream } } /** * endOfStreamReached will be set to true if the message ended in MIME Style having "--" suffix * with the last mime boundary * * @param value */ protected void setEndOfStream(boolean value) { this.endOfStreamReached = value; } /** * Returns the rest of mime stream. It will contain all attachments without * soappart (first attachment) with headers and mime boundary. Raw content! */ public InputStream getIncomingAttachmentsAsSingleStream() throws IllegalStateException { if (partsRequested) { throw new IllegalStateException( "The attachments stream can only be accessed once; either by using the IncomingAttachmentStreams class or by getting a " + "collection of AttachmentPart objects. They cannot both be called within the life time of the same service request."); } if (noStreams) { throw new IllegalStateException( "The attachments map was created programatically. No streams are available."); } streamsRequested = true; return this.pushbackInStream; } /** * @return the Next valid MIME part + store the Part in the Parts List * @throws OMException throw if content id is null or if two MIME parts contain the same * content-ID & the exceptions throws by getPart() */ private DataHandler getNextPartDataHandler() throws OMException { if (endOfStreamReached) { return null; } Part nextPart; nextPart = getPart(); if (nextPart == null) { return null; } else try { long size = nextPart.getSize(); String partContentID; DataHandler dataHandler; try { partContentID = nextPart.getContentID(); if (partContentID == null & partIndex == 1) { String id = "firstPart_" + UIDGenerator.generateContentId(); firstPartId = id; if (size > 0) { dataHandler = nextPart.getDataHandler(); } else { // Either the mime part is empty or the stream ended without having // a MIME message terminator dataHandler = new DataHandler(new ByteArrayDataSource(new byte[] {})); } addDataHandler(id, dataHandler); return dataHandler; } if (partContentID == null) { throw new OMException("Part content ID cannot be blank for non root MIME parts"); } if ((partContentID.indexOf("<") > -1) & (partContentID.indexOf(">") > -1)) { partContentID = partContentID.substring(1, (partContentID.length() - 1)); } if (partIndex == 1) { firstPartId = partContentID; } if (attachmentsMap.containsKey(partContentID)) { throw new OMException("Two MIME parts with the same Content-ID not allowed."); } if (size > 0) { dataHandler = nextPart.getDataHandler(); } else { // Either the mime part is empty or the stream ended without having // a MIME message terminator dataHandler = new DataHandler(new ByteArrayDataSource(new byte[] {})); } addDataHandler(partContentID, dataHandler); return dataHandler; } catch (MessagingException e) { throw new OMException("Error reading Content-ID from the Part." + e); } } catch (MessagingException e) { throw new OMException(e); } } /** * @return This will return the next available MIME part in the stream. * @throws OMException if Stream ends while reading the next part... */ private Part getPart() throws OMException { if (streamsRequested) { throw new IllegalStateException( "The attachments stream can only be accessed once; either by using the IncomingAttachmentStreams class or by getting a collection of AttachmentPart objects. They cannot both be called within the life time of the same service request."); } partsRequested = true; boolean isSOAPPart = (partIndex == 0); int threshhold = (fileCacheEnable) ? fileStorageThreshold : 0; // Create a MIMEBodyPartInputStream that simulates a single stream for this MIME body part MIMEBodyPartInputStream partStream = new MIMEBodyPartInputStream(pushbackInStream, boundary, this, PUSHBACK_SIZE); // The PartFactory will determine which Part implementation is most appropriate. Part part = PartFactory.createPart(getLifecycleManager(), partStream, isSOAPPart, threshhold, attachmentRepoDir, contentLength); // content-length for the whole message partIndex++; return part; } /** * Read bytes into the buffer until full or until the EOS * @param is * @param buffer * @return number of bytes read * @throws IOException */ private static int readToBuffer(InputStream is, byte[] buffer) throws IOException { int index = 0; int remainder = buffer.length; do { int bytesRead; while ((bytesRead = is.read(buffer, index, remainder)) > 0) { index += bytesRead; remainder -= bytesRead; } } while (remainder > 0 && is.available() > 0); // repeat if more bytes are now available return index; } }