com.adaptris.util.text.mime.MultiPartOutput.java Source code

Java tutorial

Introduction

Here is the source code for com.adaptris.util.text.mime.MultiPartOutput.java

Source

/*
 * Copyright 2015 Adaptris Ltd.
 * 
 * Licensed 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 com.adaptris.util.text.mime;

import static org.apache.commons.lang.StringUtils.defaultIfEmpty;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FilterOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintStream;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.List;

import javax.mail.MessagingException;
import javax.mail.internet.InternetHeaders;
import javax.mail.internet.MimeBodyPart;
import javax.mail.internet.MimeMultipart;
import javax.mail.internet.MimeUtility;

import org.apache.commons.io.IOUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Handle creation of multipart mime output.
 * <p>
 * When a getBytes() is performed, then the output is something similar to
 * 
 * <pre>
 * {@code 
 *  Message-ID: db03b6ef-ffff-ffc0-019b-04e2b47a4d8e
 *  Mime-Version: 1.0
 *  Content-Type: multipart/mixed;
 *    boundary="----=_Part_1_33189144.1047351507632"
 *  Content-Length: 383
 * 
 *  ------=_Part_1_33189144.1047351507632
 *  Content-Id: AdaptrisMessage/payload
 * 
 *  This is the message 03/11/2003 01:57 PM
 * 
 *  ------=_Part_1_33189144.1047351507632
 *  Content-Id: AdaptrisMessage/metadata
 * 
 *  workflowId=loopback
 *  previousGuid=db03b6ef-ffff-ffc0-019b-04e2b47a4d8e
 *  emailmessageid=<200303110257.h2B2v9sC030299@localhost.localdomain>
 * 
 *  ------=_Part_1_33189144.1047351507632--
 * }
 * </pre>
 * 
 * <p>
 * With each additional call to <code>addPart()>/code> an additional mime
 *  bodypart is created.  Repeated calls to <code>getBytes()</code> will return the latest version of the MimeMultiPart as written
 * out. There is no guarantee that this is the same as the last call to <code>getBytes()</code> as the part id's may change.
 * </p>
 * <p>
 * If non-unique content-id's are used for each bodypart, then an invocation of <code>removePart(String)</code> will remove
 * <b>all</b> matching body parts.
 * </p>
 */
public class MultiPartOutput implements MimeConstants {

    // private MimeMultipart multipart;
    private String messageId;
    private transient Logger logR = LoggerFactory.getLogger(this.getClass());
    private InternetHeaders mimeHeader;
    private List<KeyedBodyPart> parts;
    private String subType;

    private MultiPartOutput() throws MessagingException {
        // multipart = new MimeMultipart();
        mimeHeader = new InternetHeaders();
        mimeHeader.setHeader(HEADER_MIME_VERSION, "1.0");
        parts = new ArrayList<KeyedBodyPart>();
    }

    /**
     * Constructor.
     * <p>
     * This implicitly sets the multipart sub-type to be mixed.
     * </p>
     * 
     * @param mimeId the Message-ID header to assign to this multi-part
     * @throws MessagingException if the bytes did not contain a valid MimeMultiPart
     */
    public MultiPartOutput(String mimeId) throws MessagingException {
        this();
        if ((mimeId != null) && !mimeId.equals("")) {
            mimeHeader.setHeader(HEADER_MESSAGE_ID, mimeId);
        } else {
            throw new MessagingException("Message Id cannot be null");
        }
    }

    /**
     * Constructor.
     * 
     * @param mimeId the Message-ID header to assign to this multi-part
     * @param subtype the multi-part subtype.
     * @throws MessagingException if there was a failure to create the underlying Mime Multipart
     */
    public MultiPartOutput(String mimeId, String subtype) throws MessagingException {
        this(mimeId);
        this.subType = subtype;
    }

    /**
     * Add a new part to the mime multipart.
     * 
     * @param payload the data.
     * @param encoding the encoding to apply
     * @param contentId the id to set the content with.
     * @throws MessagingException if there was a failure adding the part. MimeMultiPart
     * @throws IOException if there was an IOException
     */
    public void addPart(String payload, String encoding, String contentId) throws MessagingException, IOException {
        InternetHeaders header = new InternetHeaders();
        byte[] encodedBytes = encodeData(payload, encoding, header);
        MimeBodyPart part = new MimeBodyPart(header, encodedBytes);
        this.addPart(part, contentId);
    }

    /**
     * Add a new part to the mime multipart.
     * 
     * @param payload the data.
     * @param contentId the id to set the content with.
     * @throws MessagingException if there was a failure adding the part. MimeMultiPart
     * @throws IOException if there was an IOException
     */
    public void addPart(String payload, String contentId) throws MessagingException, IOException {
        addPart(payload, null, contentId);
    }

    /**
     * Add a new part to the mime multipart.
     * 
     * @param payload the data.
     * @param contentId the id to set the content with.
     * @throws MessagingException if there was a failure adding the part. MimeMultiPart
     * @throws IOException if there was an IOException
     */
    public void addPart(byte[] payload, String contentId) throws MessagingException, IOException {
        addPart(payload, null, contentId);
    }

    /**
     * Add a new part to the mime multipart.
     * 
     * @param payload the data.
     * @param encoding the encoding to apply
     * @param contentId the id to set the content with.
     * @throws MessagingException on error manipulating the bodypart
     * @throws IOException on general IO error.
     */
    public void addPart(byte[] payload, String encoding, String contentId) throws MessagingException, IOException {

        InternetHeaders header = new InternetHeaders();
        byte[] encodedBytes = encodeData(payload, encoding, header);
        MimeBodyPart part = new MimeBodyPart(header, encodedBytes);
        this.addPart(part, contentId);
    }

    /**
     * Add a new part to the mime multipart.
     * 
     * @param part an already existing mimebody part
     * @param contentId the id to set the content with.
     * @throws MessagingException on error manipulating the bodypart
     * @throws IOException on general IO error.
     */
    public void addPart(MimeBodyPart part, String contentId) throws MessagingException, IOException {
        part.setHeader(HEADER_CONTENT_ID, contentId);
        parts.add(new KeyedBodyPart(contentId, part));
    }

    /**
     * Remove a part from this multipart output.
     * <p>
     * If non-unique content-id's are used for each bodypart, then an invocation of <code>removePart(String)</code> will remove
     * <b>all</b> matching body parts.
     * </p>
     * 
     * @param contentId the content-id associated with a previously added part.
     * @throws MessagingException if there was a failure removing the part.
     * @throws IOException if there was an IOException
     */
    public void removePart(String contentId) throws MessagingException, IOException {

        ArrayList toRemove = new ArrayList();
        Iterator i = parts.iterator();
        while (i.hasNext()) {

            KeyedBodyPart k = (KeyedBodyPart) i.next();
            if (k.getKey().equals(contentId)) {
                toRemove.add(k);
            }
        }
        parts.removeAll(toRemove);
    }

    /**
     * Get bytes created by this multi-part.
     * 
     * @throws MessagingException if there was a failure retrieving the bytes.
     * @throws IOException if there was an IOException
     * @return the bytes represented by this MimeMultipartOutput
     */
    public byte[] getBytes() throws MessagingException, IOException {
        return toByteArray();
    }

    /**
     * Write the multipart to the given output stream.
     * 
     * @param out the output stream.
     * @throws MessagingException if there was a failure retrieving the bytes.
     * @throws IOException if there was an IOException
     */
    public void writeTo(OutputStream out) throws MessagingException, IOException {
        writeTo(out, null);
    }

    /**
     * Write the multipart to the given outputstream.
     * 
     * @param out the output stream.
     * @param tempFile if not null, then use the specified file to stream the parts to first.
     */
    public void writeTo(OutputStream out, File tempFile) throws MessagingException, IOException {
        if (tempFile == null) {
            inMemoryWrite(out);
        } else {
            writeViaTempfile(out, tempFile);
        }
    }

    private void writeViaTempfile(OutputStream out, File tempFile) throws MessagingException, IOException {
        try (FileOutputStream fileOut = new FileOutputStream(tempFile);
                CountingOutputStream counter = new CountingOutputStream(fileOut)) {
            MimeMultipart multipart = new MimeMultipart();
            mimeHeader.setHeader(HEADER_CONTENT_TYPE, multipart.getContentType());
            // Write the part out to the stream first.
            for (KeyedBodyPart kbp : parts) {
                multipart.addBodyPart(kbp.getData());
            }
            multipart.writeTo(counter);
            counter.flush();
            mimeHeader.setHeader(HEADER_CONTENT_LENGTH, String.valueOf(counter.count()));
        }
        writeHeaders(mimeHeader, out);
        try (InputStream in = new FileInputStream(tempFile)) {
            IOUtils.copy(in, out);
        }
    }

    private void inMemoryWrite(OutputStream out) throws MessagingException, IOException {
        MimeMultipart multipart = new MimeMultipart();
        ByteArrayOutputStream partOut = new ByteArrayOutputStream();
        mimeHeader.setHeader(HEADER_CONTENT_TYPE, multipart.getContentType());
        // Write the part out to the stream first.
        for (KeyedBodyPart kbp : parts) {
            multipart.addBodyPart(kbp.getData());
        }
        multipart.writeTo(partOut);
        mimeHeader.setHeader(HEADER_CONTENT_LENGTH, String.valueOf(partOut.size()));
        writeHeaders(mimeHeader, out);
        out.write(partOut.toByteArray());
    }

    /**
     * Set an arbitary header to the headers prefixed to the start of the multipart.
     * <p>
     * The Content-Type and Content-Length will always be overridden with the content-type and length of the mime multipart
     * </p>
     * 
     * @param key the key
     * @param value the value.
     * @see javax.mail.internet.InternetHeaders
     */
    public void setHeader(String key, String value) {
        mimeHeader.setHeader(key, value);
    }

    /**
     * Return the underlying mime header that will be used to write the headers.
     * 
     * @return the internet header.
     */
    public InternetHeaders getMimeHeader() {
        return mimeHeader;
    }

    private byte[] toByteArray() throws MessagingException, IOException {
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        writeTo(out);
        return out.toByteArray();
    }

    /**
     * Encode the data.
     */
    private static byte[] encodeData(byte[] data, String encoding, InternetHeaders header)
            throws MessagingException, IOException {

        byte[] toEncode = (data == null) ? new byte[0] : data;
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        try (OutputStream encodedOut = wrap(out, encoding, header)) {
            encodedOut.write(toEncode);
        }
        return out.toByteArray();
    }

    private static byte[] encodeData(String data, String encoding, InternetHeaders header)
            throws MessagingException, IOException {
        String toEncode = defaultIfEmpty(data, "");
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        try (PrintStream print = new PrintStream(wrap(out, encoding, header))) {
            print.print(toEncode);
        }
        return out.toByteArray();
    }

    private static OutputStream wrap(OutputStream original, String encoding, InternetHeaders hdrs)
            throws MessagingException {
        OutputStream encodedOut = original;
        if (encoding != null) {
            encodedOut = MimeUtility.encode(original, encoding);
            hdrs.setHeader(HEADER_CONTENT_ENCODING, encoding);
        }
        return encodedOut;
    }

    /**
     * Write the internet headers out to the supplied outputstream
     */
    private static void writeHeaders(InternetHeaders header, OutputStream out)
            throws IOException, MessagingException {

        Enumeration e = header.getAllHeaderLines();
        PrintStream p = new PrintStream(out);
        while (e.hasMoreElements()) {
            p.println(e.nextElement().toString());
        }
        p.println("");
        p.flush();
    }

    private class KeyedBodyPart {

        private String id;
        private MimeBodyPart part;

        KeyedBodyPart(String contentId, MimeBodyPart bodypart) {
            this.id = contentId;
            this.part = bodypart;
        }

        MimeBodyPart getData() {
            return part;
        }

        String getKey() {
            return id;
        }

    }

    // We override every method becuase of INTERLOK-1926
    private class CountingOutputStream extends FilterOutputStream {
        private long count = 0;

        public CountingOutputStream(OutputStream out) {
            super(out);
        }

        public void write(int b) throws IOException {
            out.write(b);
            count += 1;
        }

        public void write(byte b[]) throws IOException {
            out.write(b, 0, b.length);
            count += b.length;
        }

        public void write(byte b[], int off, int len) throws IOException {
            if ((off | len | (b.length - (len + off)) | (off + len)) < 0)
                throw new IndexOutOfBoundsException();
            out.write(b, off, len);
            count += len;
        }

        long count() {
            return count;
        }
    }
}