/*
 * Copyright (c) 2005 - 2007 Aduna.
 * All rights reserved.
 * 
 * Licensed under the Open Software License version 3.0.
 */
package org.semanticdesktop.aperture.crawler.mail;

import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;

import javax.mail.Address;
import javax.mail.BodyPart;
import javax.mail.Message;
import javax.mail.MessagingException;
import javax.mail.Multipart;
import javax.mail.Part;
import javax.mail.Message.RecipientType;
import javax.mail.internet.ContentType;
import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeMessage;
import javax.mail.internet.MimeUtility;

import org.ontoware.rdf2go.exception.ModelRuntimeException;
import org.ontoware.rdf2go.model.node.URI;
import org.ontoware.rdf2go.model.node.impl.URIImpl;
import org.ontoware.rdf2go.vocabulary.RDF;
import org.semanticdesktop.aperture.accessor.DataObject;
import org.semanticdesktop.aperture.accessor.RDFContainerFactory;
import org.semanticdesktop.aperture.accessor.base.DataObjectBase;
import org.semanticdesktop.aperture.accessor.base.FileDataObjectBase;
import org.semanticdesktop.aperture.datasource.DataSource;
import org.semanticdesktop.aperture.rdf.RDFContainer;
import org.semanticdesktop.aperture.vocabulary.NFO;
import org.semanticdesktop.aperture.vocabulary.NIE;
import org.semanticdesktop.aperture.vocabulary.NMO;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Creates a set of DataObjects from a MimeMessage.
 * 
 * <p>
 * DataObjectFactory interprets the structure of a MimeMessage and creates a tree of DataObjects that model
 * its contents in a way that is most natural to users. Practically this means that the DataObject tree should
 * be as similar as possible to how mail readers present the mail.
 * 
 * <p>
 * For example, a multipart/alternative message may have a rather complex object structure (a Part with a
 * MultiPart content, on its turn containing two BodyParts), but this is translated to a single DataObject
 * holding all the mail metadata (sender, receiver, etc) as well as an InputStream accessing the simplest of
 * the two body parts (typically the text/plain part).
 */
@SuppressWarnings("unchecked")
public class DataObjectFactory {

    /** Obtains InputStreams from {@link Part} instances. */
    public static interface PartStreamFactory {
        /**
         * Returns an input stream with the part content. It's conceptually a wrapper around the 
         * {@link Part#getInputStream()} method, designed to allow for customization of the returned
         * input stream.
         * @param part
         * @return an InputStream with the content of the part
         * @throws MessagingException
         * @throws IOException
         */
        public InputStream getPartStream(Part part) throws MessagingException, IOException;
    }
    
    // TODO: we could use the URL format specified in RFC 2192 to construct a proper IMAP4 URL.
    // Right now we use something home-grown for representing attachments rather than isections.
    // To investigate: does JavaMail provide us with enough information for constructing proper
    // URLs for attachments? Perhaps we can create them ourselves by carefully counting BodyParts?

    private Logger logger = LoggerFactory.getLogger(getClass());

    /**
     * Key used to store a DataObject's URI in the intermediate HashMap representation.
     */
    private static final String ID_KEY = "id";

    /**
     * Key used to store a DataObject's children in the intermediate HashMap representation.
     */
    private static final String CHILDREN_KEY = "children";

    /**
     * Key used to store a DataObject's content (an InputStream) in the intermediate HashMap representation.
     */
    private static final String CONTENTS_KEY = "contents";

    /**
     * The DataSource that the generated DataObjects will report as source.
     */
    private DataSource source;

    /**
     * The RDFContainerFactory delivering the RDFContainer to be used in the DataObjects.
     */
    private RDFContainerFactory containerFactory;
    
    /**
     * The PartStreamFactory used to obtain inputStreams.
     */
    private PartStreamFactory streamFactory;

    /**
     * Returns a list of DataObjects that have been created based on the contents of the specified
     * MimeMessage. The order of the DataObjects reflects the order of the message parts they represent, i.e.
     * the first DataObject represents the entire message, subsequent DataObjects represent attachments in a
     * depth first order several layers of attachments can exist when forwarding messages containing
     * attachments).
     * 
     * @param message The MimeMessage to interpret.
     * @param messageUri The URI to use to identify the specified MimeMessage. URIs of child DataObjects must
     *            be derived from this URI.
     * @param folderUri The URI of the Folder from which these MimeMessages were obtained. The root DataObject
     *            will have this URI as parent.
     * @param dataSource The DataSource that the DataObjects will return as source.
     * @param factory An RDFContainerFactory that can deliver RDFContainer to be used in the returned
     *            DataObjects.
     * @param lStreamFactory will be used to obtain InputStreams from messages
     * @return A List of DataObjects derived from the specified MimeMessage. The order of the DataObjects
     *         reflects the order of the message parts they represent.
     * @throws MessagingException Thrown when accessing the mail contents.
     * @throws IOException Thrown when accessing the mail contents.
     */
    public List createDataObjects(MimeMessage message, String messageUri, URI folderUri, DataSource dataSource,
            RDFContainerFactory factory, PartStreamFactory lStreamFactory) throws MessagingException, IOException {
        // initialize variables
        this.source = dataSource;
        this.containerFactory = factory;
        this.streamFactory = lStreamFactory;

        /*
         * create a HashMap representation of this message and all its nested parts
         * The hashmap representation of a message contains a set of name-value pairs for
         * each metadata property of a message and a special pair named with the CHILDREN_KEY, whose
         * value is an ArrayList of further HashMaps for the children. This makes the
         * HashMap representation equivalent to the tree-like structure of the MimeMessage.
         * (Which is non-obvious at the first glance - Antoni 06.11.2007)
         */ 
        HashMap map = handleMailPart(message, new URIImpl(messageUri), MailUtil
                .getDate(message));

        // convert the HashMap representation to a DataObject representation
        ArrayList result = new ArrayList();
        createDataObjects(map, folderUri, result);

        // The first object is the Message itself, add RDF type to it
        RDFContainer msgObject = ((DataObject) result.get(0)).getMetadata();
        msgObject.add(RDF.type, NMO.Email);
        // Apart from being a message, it is also a MailboxDataObject
        msgObject.add(RDF.type, NMO.MailboxDataObject);

        String messageID = message.getMessageID();
        if (messageID != null) {
            msgObject.add(NMO.messageId, messageID);
        }

        return result;
    }

    /* ----------------------------- Methods for MIME interpretation ----------------------------- */

    private HashMap handleMailPart(Part mailPart, URI uri, Date date) throws MessagingException, IOException {
        // determine the primary type of this Part
        ContentType contentType = null;
        String primaryType = null;

        String contentTypeStr = mailPart.getContentType();
        if (contentTypeStr != null) {
            contentType = new ContentType(contentTypeStr);
            primaryType = normalizeString(contentType.getPrimaryType());
        }

        // make an exception for multipart mails
        if ("multipart".equals(primaryType)) {
            Object content = mailPart.getContent();
            if (content instanceof Multipart) {
                // content is a container for multiple other parts
                return handleMultipart((Multipart) content, contentType, uri, date);
            }
            else {
                logger.warn("multipart '" + uri + "' does not contain a Multipart object: "
                        + (content == null ? null : content.getClass()));
                return null;
            }
        }
        else {
            return handleSinglePart(mailPart, contentType, uri, date, false);
        }
    }

    private HashMap handleSinglePart(Part mailPart, ContentType contentType, URI uri, Date date,
            boolean emptyContent) throws MessagingException, IOException {
        // determine the content type properties of this mail
        String mimeType = null;
        String charsetStr = null;

        if (contentType != null) {
            mimeType = normalizeString(contentType.getBaseType());
            charsetStr = normalizeString(contentType.getParameter("charset"));
        }

        // if some are unspecified, set the defaults according to RFC 2045
        if (mimeType == null) {
            mimeType = "text/plain";
        }
        if (charsetStr == null && "text/plain".equals(mimeType)) {
            charsetStr = "us-ascii";
        }

        charsetStr = normalizeCharset(charsetStr);

        // extract the mail's contents
        HashMap result = null;

        if (emptyContent) {
            // create a data object without content, as explicitly requested
            result = new HashMap();
            result.put(ID_KEY, uri);
        }
        else if ("message/rfc822".equals(mimeType)) {
            Object content = mailPart.getContent();
            if (content instanceof Message) {
                // this part contains a nested message (typically a forwarded message): ignore this part
                // and only model the contents of the nested message, as the parent message will have
                // been generated already and this specific Part contains no additional useful
                // information
                Message nestedMessage = (Message) content;
                return handleMailPart(nestedMessage, uri, MailUtil.getDate(nestedMessage));
            }
            else {
                logger.warn("message/rfc822 part with unknown content class: "
                        + (content == null ? null : content.getClass()));
                return null;
            }
        }
        else {
            // create a data object embedding the data stream of the mail part
            result = new HashMap();
            result.put(ID_KEY, uri);
            result.put(CONTENTS_KEY, streamFactory.getPartStream(mailPart));

            if (charsetStr != null) {
                result.put(NIE.characterSet, charsetStr);
            }

            String fileName = mailPart.getFileName();
            if (fileName != null) {
                try {
                    fileName = MimeUtility.decodeWord(fileName);
                }
                catch (MessagingException e) {
                    // happens on unencoded file names! so just ignore it and leave the file name as it is
                }
                result.put(NFO.fileName, fileName);
                // everything that has a file name is an attachment
                result.put(RDF.type, NFO.Attachment);
            }
        }

        // set some generally applicable metadata properties
        int size = mailPart.getSize();
        if (size >= 0) {
            result.put(NIE.byteSize, new Integer(size));
        }

        result.put(NIE.contentCreated, date);

        // Differentiate between Messages and other types of mail parts. We don't use the Part.getHeader
        // method as they don't decode non-ASCII 'encoded words' (see RFC 2047).
        if (mailPart instanceof Message) {
            // the data object's primary mimetype is message/rfc822. The MIME type of the InputStream
            // (most often text/plain or text/html) is modeled as a secondary MIME type
            result.put(NIE.mimeType, "message/rfc822");
            result.put(NMO.contentMimeType, mimeType);

            // add message metadata
            Message message = (Message) mailPart;
            addObjectIfNotNull(NMO.messageSubject, message.getSubject(), result);
            addContactArrayIfNotNull(NMO.from, message.getFrom(), result);
            addContactArrayIfNotNull(NMO.to, message.getRecipients(RecipientType.TO), result);
            addContactArrayIfNotNull(NMO.cc, message.getRecipients(RecipientType.CC), result);
            addContactArrayIfNotNull(NMO.bcc, message.getRecipients(RecipientType.BCC), result);
            result.put(RDF.type, NMO.Email);

            if (message instanceof MimeMessage) {
                MimeMessage mimeMessage = (MimeMessage) message;
                addObjectIfNotNull(NMO.sender, mimeMessage.getSender(), result);
            }
        }
        else {
            // this is most likely an attachment: set the InputStream's mime type as the data object's
            // primary MIME type
            result.put(NIE.mimeType, mimeType);
            // originally this line treated all parts of a multipart message as attachments, this is wrong
            // that's why i (Antoni Mylka) commented this line out on 27.02.2008, giving attachments an
            // rdf:type of nmo:MimeEntity is clearly correct for message maprts and for attachments,
            // the entire idea of creating a hashmap from a message part is insufficient in this respect
            // this issue will need to be resolved when this class is rewritten to allow for a mail part
            // to have multiple types
            //result.put(RDF.type, NFO.Attachment);
            if (result.get(RDF.type) == null) {
                // the part may already have a type, e.g. the attachments are marked as attachments
                // if the part has a fileName, see above
                result.put(RDF.type, NMO.MimeEntity);
            }
        }

        // done!
        return result;
    }

    private HashMap handleMultipart(Multipart part, ContentType contentType, URI uri, Date date)
            throws MessagingException, IOException {
        // fetch the content subtype
        String subType = normalizeString(contentType.getSubType());

        // handle the part according to its subtype
        if ("mixed".equals(subType)) {
            return handleMixedPart(part, contentType, uri, date);
        }
        else if ("alternative".equals(subType)) {
            return handleAlternativePart(part, contentType, uri, date);
        }
        else if ("digest".equals(subType)) {
            return handleDigestPart(part, contentType, uri, date);
        }
        else if ("related".equals(subType)) {
            return handleRelatedPart(part, contentType, uri, date);
        }
        else if ("signed".equals(subType)) {
            return handleSignedPart(part, contentType, uri, date);
        }
        else if ("encrypted".equals(subType)) {
            return handleEncryptedPart(part, contentType, uri, date);
        }
        else if ("report".equals(subType)) {
            return handleReportPart(part, contentType, uri, date);
        }
        else if ("parallel".equals(subType)) {
            return handleParallelPart(part, contentType, uri, date);
        }
        else {
            return handleUnknownTypePart(part, contentType, uri, date);
        }
    }

    private HashMap handleMixedPart(Multipart part, ContentType contentType, URI uri, Date date)
            throws MessagingException, IOException {
        // interpret the parent part, reflecting subject and address metadata, and skip if there is none
        Part parentPart = part.getParent();
        if (parentPart == null) {
            return null;
        }

        HashMap parent = handleSinglePart(parentPart, contentType, uri, date, true);
        if (parent == null) {
            return null;
        }

        // determine the uri prefix that all attachments parts should have
        String uriPrefix = getBodyPartURIPrefix(uri);

        // interpret every nested part
        int nrParts = part.getCount();
        ArrayList children = new ArrayList(nrParts);

        for (int i = 0; i < nrParts; i++) {
            BodyPart bodyPart = part.getBodyPart(i);
            if (bodyPart == null) {
                continue;
            }

            URI bodyURI = new URIImpl(uriPrefix + i);
            HashMap childResult = handleMailPart(bodyPart, bodyURI, date);

            if (childResult != null) {
                children.add(childResult);
            }
        }

        // the first child with type text/plain or text/html is promoted to become the body text
        // of the parent object
        int nrChildren = children.size();
        for (int i = 0; i < nrChildren; i++) {
            HashMap child = (HashMap) children.get(i);
            Object bodyMimeType = child.get(NIE.mimeType);

            if ("text/plain".equals(bodyMimeType) || "text/html".equals(bodyMimeType)) {
                children.remove(i);
                transferInfo(child, parent);
                break;
            }
        }

        // all remaining data objects are registered as the parent object's children
        parent.put(CHILDREN_KEY, children);

        // return the parent as the result of this operation
        return parent;
    }

    private HashMap handleAlternativePart(Multipart part, ContentType contentType, URI uri, Date date)
            throws MessagingException, IOException {
        // nothing to return when there are no parts
        int count = part.getCount();
        if (count == 0) {
            return null;
        }

        // try to fetch the text/plain alternative
        int index = getPartWithMimeType(part, "text/plain");

        // if not available, try to fetch the text/html alternative
        if (index < 0) {
            index = getPartWithMimeType(part, "text/html");
        }

        // if still not found, simply take the first available part;
        if (index < 0) {
            index = 0;
        }

        // interpret the selected alternative part
        HashMap child = handleMailPart(part.getBodyPart(index), uri, date);

        // If this part was nested in a message, we should merge the obtained info with a data object
        // modeling all message info, in all other cases (e.g. when the multipart/alternative was
        // nested in a multipart/mixed), we can simply return the child data object.
        // We can use the same uri as for the child object, as only one of these objects will actually
        // be returned.
        Part parentPart = part.getParent();
        if (parentPart instanceof Message) {
            HashMap parent = handleSinglePart(parentPart, contentType, uri, date, true);

            if (parent == null) {
                return child;
            }
            else {
                transferInfo(child, parent);
                return parent;
            }
        }
        else {
            return child;
        }
    }

    private HashMap handleDigestPart(Multipart part, ContentType contentType, URI uri, Date date)
            throws MessagingException, IOException {
        // interpret the parent part
        Part parentPart = part.getParent();
        if (parentPart == null) {
            return null;
        }

        HashMap parent = handleSinglePart(parentPart, contentType, uri, date, true);
        if (parent == null) {
            return null;
        }

        // create the URI prefix for all children
        String bodyURIPrefix = getBodyPartURIPrefix(uri);

        // interpret every body part in the digest multipart
        ArrayList children = new ArrayList();

        int nrParts = part.getCount();
        for (int i = 0; i < nrParts; i++) {
            // fetch the body part
            Part bodyPart = part.getBodyPart(i);
            if (bodyPart == null) {
                continue;
            }

            // derive a URI
            URI bodyURI = new URIImpl(bodyURIPrefix + i);

            // interpret this part
            HashMap child = handleMailPart(bodyPart, bodyURI, date);
            if (child != null) {
                children.add(child);
            }
        }

        // all interpreted body parts become children of the parent
        parent.put(CHILDREN_KEY, children);

        return parent;
    }

    private HashMap handleRelatedPart(Multipart part, ContentType contentType, URI uri, Date date)
            throws MessagingException, IOException {
        // interpret the parent part
        Part parentPart = part.getParent();
        if (parentPart == null) {
            return null;
        }

        HashMap parent = handleSinglePart(parentPart, contentType, uri, date, true);
        if (parent == null) {
            return null;
        }

        // determine the prefix for all children
        String bodyURIPrefix = getBodyPartURIPrefix(uri);

        // find the index of the root part, if specified (defaults to 0)
        int rootPartIndex = 0;
        int nrBodyParts = part.getCount();

        String rootPartString = contentType.getParameter("start");
        if (rootPartString != null) {
            rootPartString = rootPartString.trim();
            if (rootPartString.length() > 0) {
                for (int i = 0; i < nrBodyParts; i++) {
                    BodyPart bodyPart = part.getBodyPart(i);
                    String bodyID = getHeader(bodyPart, "Content-ID");
                    if (rootPartString.equals(bodyID)) {
                        rootPartIndex = i;
                        break;
                    }
                }
            }
        }

        // interpret each body part, giving special treatment to the root part
        ArrayList children = new ArrayList();

        for (int i = 0; i < nrBodyParts; i++) {
            // fetch the body part
            BodyPart bodyPart = part.getBodyPart(i);

            // interpret this body part
            URI bodyURI = new URIImpl(bodyURIPrefix + i);
            HashMap child = handleMailPart(bodyPart, bodyURI, date);

            // append it to the part object in the appropriate way
            if (child != null) {
                if (i == rootPartIndex) {
                    transferInfo(child, parent);
                }
                else {
                    children.add(child);
                }
            }
        }

        // all interpreted body parts become children of the parent part, except for the root part, whose
        // properties have already been shifted to the parent
        parent.put(CHILDREN_KEY, children);

        return parent;
    }

    private HashMap handleSignedPart(Multipart part, ContentType contentType, URI uri, Date date)
            throws MessagingException, IOException {
        return handleProtectedPart(part, 0, contentType, uri, date);
    }

    private HashMap handleEncryptedPart(Multipart part, ContentType contentType, URI uri, Date date)
            throws MessagingException, IOException {
        return handleProtectedPart(part, 1, contentType, uri, date);
    }

    private HashMap handleProtectedPart(Multipart part, int partIndex, ContentType contentType, URI uri,
            Date date) throws MessagingException, IOException {
        // interpret the first body part, which contains the actual content
        HashMap child = null;
        if (part.getCount() >= 2) {
            child = handleMailPart(part.getBodyPart(partIndex), uri, date);
        }
        else {
            logger.warn("multipart/signed or multipart/encrypted without enough body parts, uri = " + uri);
        }

        // if this part was nested in a message, we should merge the obtained info with the message info,
        // else we simply return the child
        Part parentPart = part.getParent();
        if (parentPart instanceof Message) {
            HashMap parent = handleSinglePart(parentPart, contentType, uri, date, true);
            if (parent == null) {
                return child;
            }
            else {
                if (child != null) {
                    transferInfo(child, parent);
                }
                return parent;
            }
        }
        else {
            return child;
        }
    }

    private HashMap handleReportPart(Multipart part, ContentType contentType, URI uri, Date date)
            throws MessagingException, IOException {
        // interpret for the parent message
        Part parentPart = part.getParent();
        if (parentPart == null) {
            return null;
        }

        HashMap parent = handleSinglePart(parentPart, contentType, uri, date, true);
        if (parent == null) {
            return null;
        }

        // the first part contains a human-readable error message and will be treated as the mail body
        int count = part.getCount();
        if (count > 0) {
            HashMap errorPart = handleMailPart(part.getBodyPart(0), uri, date);
            if (errorPart != null) {
                transferInfo(errorPart, parent);
            }
        }

        // the optional third part contains the (partial) returned message and will become an attachment
        if (count > 2) {
            URI nestedURI = new URIImpl(getBodyPartURIPrefix(uri) + "0");
            HashMap returnedMessage = handleMailPart(part.getBodyPart(2), nestedURI, date);
            if (returnedMessage != null) {
                ArrayList children = new ArrayList();
                children.add(returnedMessage);
                parent.put(CHILDREN_KEY, children);
            }
        }

        return parent;
    }

    private HashMap handleParallelPart(Multipart part, ContentType contentType, URI uri, Date date)
            throws MessagingException, IOException {
        // treat this as multipart/mixed
        return handleMixedPart(part, contentType, uri, date);
    }

    private HashMap handleUnknownTypePart(Multipart part, ContentType contentType, URI uri, Date date)
            throws MessagingException, IOException {
        // treat this as multipart/mixed, as imposed by RFC 2046
        logger.warn("Unknown multipart MIME type: \"" + contentType.getBaseType()
                + "\", treating as multipart/mixed");
        return handleMixedPart(part, contentType, uri, date);
    }

    /* ------- Methods for transforming a HashMap to a list of DataObjects ------- */

    private void createDataObjects(HashMap dataObjectHashMap, URI parentUri, ArrayList resultDataObjectList) {
        // fetch the minimal set of properties needed to create a DataObject
        URI dataObjectId = (URI) dataObjectHashMap.get(ID_KEY);
        InputStream content = (InputStream) dataObjectHashMap.get(CONTENTS_KEY);
        RDFContainer metadata = containerFactory.getRDFContainer(dataObjectId);

        if (content != null && !content.markSupported()) {
            content = new BufferedInputStream(content, 16384);
        }

        // create the DataObject
        DataObject dataObject = 
            (content == null) ? 
             new DataObjectBase(dataObjectId, source, metadata) : 
             new FileDataObjectBase(dataObjectId, source, metadata, content);
             
        resultDataObjectList.add(dataObject);

        // extend metadata with additional properties
        if (parentUri != null) {
            metadata.add(NIE.isPartOf, parentUri);
        }

        copyString(NIE.characterSet, dataObjectHashMap, metadata);
        copyString(NIE.mimeType, dataObjectHashMap, metadata);
        copyString(NMO.contentMimeType, dataObjectHashMap, metadata);
        copyString(NMO.messageSubject, dataObjectHashMap, metadata);
        copyString(NFO.fileName, dataObjectHashMap, metadata);

        copyInt(NIE.byteSize, dataObjectHashMap, metadata);

        copyDate(NIE.contentCreated, dataObjectHashMap, metadata);

        copyAddresses(NMO.from, dataObjectHashMap, metadata);
        copyAddresses(NMO.sender, dataObjectHashMap, metadata);
        copyAddresses(NMO.to, dataObjectHashMap, metadata);
        copyAddresses(NMO.cc, dataObjectHashMap, metadata);
        copyAddresses(NMO.bcc, dataObjectHashMap, metadata);
        
        copyUri(RDF.type, dataObjectHashMap, metadata);
        
        // a really crappy workaround, the hashmap allows the mail types to have only one type
        // this means, that attachments can be marked as attachments, but thay can't be marked
        // as MimeEntities, therefore we always add the RDF.type NMO.MimeEntity at this point
        metadata.add(RDF.type, NMO.MimeEntity);

        // repeat recursively on children
        ArrayList children = (ArrayList) dataObjectHashMap.get(CHILDREN_KEY);
        if (children != null) {
            int nrChildren = children.size();
            for (int i = 0; i < nrChildren; i++) {
                HashMap childHashMap = (HashMap) children.get(i);

                // also register the child in the parent's metadata
                URI childID = (URI) childHashMap.get(ID_KEY);
                metadata.getModel().addStatement(childID, NIE.isPartOf, dataObjectId);

                createDataObjects(childHashMap, dataObjectId, resultDataObjectList);
            }
        }
    }

    private void copyString(URI predicate, HashMap map, RDFContainer metadata) {
        String value = (String) map.get(predicate);
        if (value != null) {
            metadata.add(predicate, value);
        }
    }

    private void copyInt(URI predicate, HashMap map, RDFContainer metadata) {
        Integer value = (Integer) map.get(predicate);
        if (value != null) {
            metadata.add(predicate, value.intValue());
        }
    }

    private void copyDate(URI predicate, HashMap map, RDFContainer metadata) {
        Date value = (Date) map.get(predicate);
        if (value != null) {
            metadata.add(predicate, value);
        }
    }
    
    private void copyUri(URI predicate, HashMap map, RDFContainer metadata) {
        URI uri = (URI) map.get(predicate);
        if (uri != null) {
            metadata.add(predicate,uri);
        }
    }

    private void copyAddresses(URI predicate, HashMap map, RDFContainer metadata) {
        Object value = map.get(predicate);

        try {
            if (value instanceof InternetAddress) {
                MailUtil.addAddressMetadata((InternetAddress) value, predicate, metadata);
            }
            else if (value instanceof InternetAddress[]) {
                InternetAddress[] array = (InternetAddress[]) value;
                for (int i = 0; i < array.length; i++) {
                    MailUtil.addAddressMetadata(array[i], predicate, metadata);
                }
            }
            else if (value != null) {
                logger.warn("Unknown address class: " + value.getClass().getName());
            }
        }
        catch (ModelRuntimeException e) {
            logger.error("ModelException while handling address metadata", e);
        }
    }

    /* ----------------------------- Utility methods ----------------------------- */

    private String normalizeString(String string) {
        if (string != null) {
            string = string.trim().toLowerCase();
        }
        return string;
    }

    private String normalizeCharset(String charsetStr) {
        if (charsetStr == null || charsetStr.length() == 0) {
            charsetStr = MimeUtility.getDefaultJavaCharset();
        }
        else {
            charsetStr = MimeUtility.javaCharset(charsetStr);
        }

        // note: even MimeUtility.javaCharset may return different casings of the same charset
        charsetStr = charsetStr.toLowerCase();

        return charsetStr;
    }

    private void addContactArrayIfNotNull(URI predicate, Address[] addresses, HashMap result) {
        if (addresses != null) {
            result.put(predicate, addresses);
        }
    }

    private String getBodyPartURIPrefix(URI parentURI) {
        String prefix = parentURI.toString();
        return prefix + (prefix.indexOf('#') < 0 ? "#" : "-");
    }

    /**
     * Transfer all properties from one interpreted mail part to another, taking care to merge information
     * rather than overwrite it when appropriate.
     */
    private void transferInfo(HashMap fromObject, HashMap toObject) throws IOException {
        // transfer content stream if applicable
        Object content = fromObject.get(CONTENTS_KEY);
        if (content != null) {
            toObject.put(CONTENTS_KEY, content);
        }

        // transfer mime type, carefully placing it as mime type or content mime type
        Object fromType = fromObject.get(NIE.mimeType);
        if (fromType != null) {
            Object toType = toObject.get(NIE.mimeType);
            URI predicate = "message/rfc822".equals(toType) ? NMO.contentMimeType : NIE.mimeType;
            toObject.put(predicate, fromType);
        }

        // transfer the first object's children to the second object relationships
        ArrayList fromChildren = (ArrayList) fromObject.get(CHILDREN_KEY);

        if (fromChildren != null && !fromChildren.isEmpty()) {
            ArrayList toChildren = (ArrayList) toObject.get(CHILDREN_KEY);
            if (toChildren == null) {
                toChildren = new ArrayList();
                toObject.put(CHILDREN_KEY, toChildren);
            }

            toChildren.addAll(fromChildren);
        }
    }

    private int getPartWithMimeType(Multipart multipart, String mimeType) throws MessagingException {
        int count = multipart.getCount();
        for (int i = 0; i < count; i++) {
            BodyPart bodyPart = multipart.getBodyPart(i);
            String partType = getMimeType(bodyPart);
            if (mimeType.equalsIgnoreCase(partType)) {
                return i;
            }
        }

        return -1;
    }

    private String getMimeType(Part mailPart) throws MessagingException {
        String contentType = mailPart.getContentType();

        if (contentType != null) {
            ContentType ct = new ContentType(contentType);
            return ct.getBaseType();
        }

        return null;
    }

    private String getHeader(Part mailPart, String headerName) throws MessagingException {
        String[] headerValues = mailPart.getHeader(headerName);
        return (headerValues != null && headerValues.length > 0) ? headerValues[0] : null;
    }
    
    private void addObjectIfNotNull(URI predicate, Object value, HashMap map) {
        if (value != null) {
            map.put(predicate, value);
        }
    }
}