blob: 28b785bc46d74f88eadeb54c4af12a7aae1f6bc7 [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.attachments;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.activation.DataHandler;
import javax.mail.internet.ContentType;
import javax.mail.internet.ParseException;
import org.apache.axiom.attachments.lifecycle.LifecycleManager;
import org.apache.axiom.attachments.lifecycle.impl.LifecycleManagerImpl;
import org.apache.axiom.mime.Header;
import org.apache.axiom.om.OMException;
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 org.apache.james.mime4j.MimeException;
import org.apache.james.mime4j.stream.EntityState;
import org.apache.james.mime4j.stream.Field;
import org.apache.james.mime4j.stream.MimeConfig;
import org.apache.james.mime4j.stream.MimeTokenStream;
import org.apache.james.mime4j.stream.RecursionMode;
/**
* {@link AttachmentsDelegate} implementation that represents a MIME multipart message read from a
* stream.
*/
class MIMEMessage extends AttachmentsDelegate {
private static final Log log = LogFactory.getLog(MIMEMessage.class);
/** <code>ContentType</code> of the MIME message */
private final ContentType contentType;
private final int contentLength; // Content Length
private final DetachableInputStream filterIS;
private final MimeTokenStream parser;
/**
* Stores the Data Handlers of the already parsed Mime Body Parts in the order that the attachments
* occur in the message. This map is keyed using the content-ID's.
*/
private final Map attachmentsMap = new LinkedHashMap();
/** <code>partIndex</code>- Number of Mime parts parsed */
private int partIndex = 0;
/**
* The MIME part currently being processed.
*/
private PartImpl currentPart;
/** Container to hold streams for direct access */
private IncomingAttachmentStreams streams;
/** <code>boolean</code> Indicating if any streams have been directly requested */
private boolean streamsRequested;
/** <code>boolean</code> Indicating if any data handlers have been directly requested */
private boolean partsRequested;
private String firstPartId;
private final boolean fileCacheEnable;
private final String attachmentRepoDir;
private final int fileStorageThreshold;
private LifecycleManager manager;
MIMEMessage(LifecycleManager manager, InputStream inStream, String contentTypeString, boolean fileCacheEnable,
String attachmentRepoDir, int fileStorageThreshold, 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);
}
this.fileStorageThreshold = fileStorageThreshold;
try {
contentType = new ContentType(contentTypeString);
} catch (ParseException e) {
throw new OMException(
"Invalid Content Type Field in the Mime Message"
, 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;
} else {
filterIS = null;
}
MimeConfig config = new MimeConfig();
config.setStrictParsing(true);
parser = new MimeTokenStream(config);
parser.setRecursionMode(RecursionMode.M_NO_RECURSE);
parser.parseHeadless(is, contentTypeString);
// Move the parser to the beginning of the first part
while (parser.getState() != EntityState.T_START_BODYPART) {
try {
parser.next();
} catch (IOException ex) {
throw new OMException(ex);
} catch (MimeException ex) {
throw new OMException(ex);
}
}
// Read the root part and cache it
getDataHandler(getRootPartContentID());
// Now reset partsRequested. The root part is a special case which is always
// read beforehand, regardless of request.
partsRequested = false;
}
ContentType getContentType() {
return contentType;
}
LifecycleManager getLifecycleManager() {
if(manager == null) {
manager = new LifecycleManagerImpl();
}
return manager;
}
void setLifecycleManager(LifecycleManager manager) {
this.manager = manager;
}
DataHandler getDataHandler(String contentID) {
do {
DataHandler dataHandler = (DataHandler)attachmentsMap.get(contentID);
if (dataHandler != null) {
return dataHandler;
}
} while (getNextPartDataHandler() != null);
return null;
}
void addDataHandler(String contentID, DataHandler dataHandler) {
attachmentsMap.put(contentID, dataHandler);
}
void removeDataHandler(String blobContentID) {
do {
if (attachmentsMap.remove(blobContentID) != null) {
return;
}
} while (getNextPartDataHandler() != null);
}
InputStream getRootPartInputStream() throws OMException {
DataHandler dh;
try {
dh = getDataHandler(getRootPartContentID());
if (dh == null) {
throw new OMException(
"Mandatory root MIME part is missing");
}
return dh.getInputStream();
} catch (IOException e) {
throw new OMException(
"Problem with DataHandler of the Root Mime Part. ", e);
}
}
String getRootPartContentID() {
String rootContentID = contentType.getParameter("start");
if (log.isDebugEnabled()) {
log.debug("getRootPartContentID 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;
}
String getRootPartContentType() {
String rootPartContentID = getRootPartContentID();
if (rootPartContentID == null) {
throw new OMException("Unable to determine the content ID of the root part");
}
DataHandler rootPart = getDataHandler(rootPartContentID);
if (rootPart == null) {
throw new OMException("Unable to locate the root part; content ID was " + rootPartContentID);
}
return rootPart.getContentType();
}
IncomingAttachmentStreams getIncomingAttachmentStreams() {
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.");
}
streamsRequested = true;
if (this.streams == null) {
this.streams = new MultipartAttachmentStreams(parser);
}
return this.streams;
}
/**
* Force reading of all attachments.
*/
private void fetchAllParts() {
while (getNextPartDataHandler() != null) {
// Just loop until getNextPartDataHandler returns null
}
}
Set getContentIDs(boolean fetchAll) {
if (fetchAll) {
fetchAllParts();
}
return attachmentsMap.keySet();
}
Map getMap() {
fetchAllParts();
return Collections.unmodifiableMap(attachmentsMap);
}
long getContentLength() throws IOException {
if (contentLength > 0) {
return contentLength;
} else {
// Ensure all parts are read
fetchAllParts();
// Now get the count from the filter
return filterIS.length();
}
}
/**
* @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 (currentPart != null) {
currentPart.fetch();
currentPart = null;
}
if (parser.getState() == EntityState.T_END_MULTIPART) {
return null;
} else {
Part nextPart = getPart();
String partContentID = nextPart.getContentID();
if (partContentID == null & partIndex == 1) {
String id = "firstPart_" + UIDGenerator.generateContentId();
firstPartId = id;
DataHandler dataHandler = nextPart.getDataHandler();
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.");
}
DataHandler dataHandler = nextPart.getDataHandler();
addDataHandler(partContentID, dataHandler);
return dataHandler;
}
}
/**
* @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 isRootPart = (partIndex == 0);
try {
List headers = readHeaders();
partIndex++;
currentPart = new PartImpl(this, isRootPart, headers, parser);
return currentPart;
} catch (IOException ex) {
throw new OMException(ex);
} catch (MimeException ex) {
throw new OMException(ex);
}
}
int getThreshold() {
return fileCacheEnable ? fileStorageThreshold : 0;
}
String getAttachmentRepoDir() {
return attachmentRepoDir;
}
int getContentLengthIfKnown() {
return contentLength;
}
private List readHeaders() throws IOException, MimeException {
if(log.isDebugEnabled()){
log.debug("readHeaders");
}
checkParserState(parser.next(), EntityState.T_START_HEADER);
List headers = new ArrayList();
while (parser.next() == EntityState.T_FIELD) {
Field field = parser.getField();
String name = field.getName();
String value = field.getBody();
if (log.isDebugEnabled()){
log.debug("addHeader: (" + name + ") value=(" + value +")");
}
headers.add(new Header(name, value));
}
checkParserState(parser.next(), EntityState.T_BODY);
return headers;
}
private static void checkParserState(EntityState state, EntityState expected) throws IllegalStateException {
if (expected != state) {
throw new IllegalStateException("Internal error: expected parser to be in state "
+ expected + ", but got " + state);
}
}
}