/*
 * Copyright (c) 2005, 2006, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle in the LICENSE file that accompanied this code.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */


package com.sun.xml.internal.messaging.saaj.soap;

import java.io.*;
import java.util.*;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.activation.DataHandler;
import javax.activation.DataSource;
import javax.xml.soap.*;
import javax.xml.transform.Source;
import javax.xml.transform.stream.StreamSource;

import com.sun.xml.internal.messaging.saaj.packaging.mime.Header;
import com.sun.xml.internal.messaging.saaj.packaging.mime.internet.*;
import com.sun.xml.internal.messaging.saaj.packaging.mime.util.*;
import com.sun.xml.internal.messaging.saaj.packaging.mime.MessagingException;

import com.sun.xml.internal.messaging.saaj.SOAPExceptionImpl;
import com.sun.xml.internal.messaging.saaj.soap.impl.EnvelopeImpl;
import com.sun.xml.internal.messaging.saaj.util.*;
import com.sun.xml.internal.org.jvnet.mimepull.MIMEPart;

/**
 * The message implementation for SOAP messages with
 * attachments. Messages for specific profiles will likely extend this
 * MessageImpl class and add more value for that particular profile.
 *
 * @author Anil Vijendran (akv@eng.sun.com)
 * @author Rajiv Mordani (rajiv.mordani@sun.com)
 * @author Manveen Kaur (manveen.kaur@sun.com)
 */

public abstract class MessageImpl
    extends SOAPMessage
    implements SOAPConstants {


    public static final String CONTENT_ID             = "Content-ID";
    public static final String CONTENT_LOCATION       = "Content-Location";

    protected static final Logger log =
        Logger.getLogger(LogDomainConstants.SOAP_DOMAIN,
                         "com.sun.xml.internal.messaging.saaj.soap.LocalStrings");

    protected static final int PLAIN_XML_FLAG      = 1;      // 00001
    protected static final int MIME_MULTIPART_FLAG = 2;      // 00010
    protected static final int SOAP1_1_FLAG = 4;             // 00100
    protected static final int SOAP1_2_FLAG = 8;             // 01000
    //protected static final int MIME_MULTIPART_XOP_FLAG = 14; // 01110
    protected static final int MIME_MULTIPART_XOP_SOAP1_1_FLAG = 6;  // 00110
    protected static final int MIME_MULTIPART_XOP_SOAP1_2_FLAG = 10; // 01010
    protected static final int XOP_FLAG = 13;                // 01101
    protected static final int FI_ENCODED_FLAG     = 16;     // 10000
    
    protected MimeHeaders headers;
    protected ContentType contentType;
    protected SOAPPartImpl soapPartImpl;
    protected FinalArrayList attachments;
    protected boolean saved = false;
    protected byte[] messageBytes;
    protected int messageByteCount;
    protected HashMap properties = new HashMap();

    // used for lazy attachment initialization
    protected MimeMultipart multiPart = null;
    protected boolean attachmentsInitialized = false;

    /**
     * True if this part is encoded using Fast Infoset.
     * MIME -> application/fastinfoset
     */
    protected boolean isFastInfoset = false;  
    
    /**
     * True if the Accept header of this message includes
     * application/fastinfoset
     */
    protected boolean acceptFastInfoset = false;
    
    protected MimeMultipart mmp = null;
    
    // if attachments are present, don't read the entire message in byte stream in saveTo()
    private boolean optimizeAttachmentProcessing = true;

    private InputStream inputStreamAfterSaveChanges = null;

    // switch back to old MimeMultipart incase of problem
    private static boolean switchOffBM = false;
    private static boolean switchOffLazyAttachment = false;
    private static boolean useMimePull = false;

    static {
        try {
            String s = System.getProperty("saaj.mime.optimization");
            if ((s != null) && s.equals("false")) {
                switchOffBM = true;
            }
            s = System.getProperty("saaj.lazy.mime.optimization");
            if ((s != null) && s.equals("false")) {
                switchOffLazyAttachment = true;
            }
            useMimePull = Boolean.getBoolean("saaj.use.mimepull");
        } catch (SecurityException ex) {
            // ignore it
        }
    }

    //property to indicate optimized serialization for lazy attachments
    private boolean lazyAttachments = false;

    // most of the times, Content-Types are already all lower cased.
    // String.toLowerCase() works faster in this case, so even if you
    // are only doing one comparison, it pays off to use String.toLowerCase()
    // than String.equalsIgnoreCase(). When you do more than one comparison,
    // the benefits of String.toLowerCase() dominates.
    //
    //
    // for FI,
    //   use application/fastinfoset for SOAP 1.1
    //   use application/soap+fastinfoset for SOAP 1.2
    // to speed up comparisons, test methods always use lower cases.

    /**
     * @param primary
     *      must be all lower case
     * @param sub
     *      must be all lower case
     */
    private static boolean isSoap1_1Type(String primary, String sub) {
        return primary.equalsIgnoreCase("text") && sub.equalsIgnoreCase("xml")
            || primary.equals("application")
               && sub.equals("fastinfoset");
    }

    /**
     * @param type
     *      must be all lower case
     */
    private static boolean isEqualToSoap1_1Type(String type) {
        return type.startsWith("text/xml") ||
               type.startsWith("application/fastinfoset");
    }

    /**
     * @param primary
     *      must be all lower case
     * @param sub
     *      must be all lower case
     */
    private static boolean isSoap1_2Type(String primary, String sub) {
        return primary.equals("application")
               && (sub.equals("soap+xml")
                   || sub.equals("soap+fastinfoset"));
    }

    /**
     * @param type
     *      must be all lower case
     */
    private static boolean isEqualToSoap1_2Type(String type) {
        return type.startsWith("application/soap+xml") ||
               type.startsWith("application/soap+fastinfoset");
    }

    /**
      * Construct a new message. This will be invoked before message
      * sends.
      */
    protected MessageImpl() {
        this(false, false);
        attachmentsInitialized = true;
    }
    
    /**
      * Construct a new message. This will be invoked before message
      * sends.
      */
    protected MessageImpl(boolean isFastInfoset, boolean acceptFastInfoset) {
        this.isFastInfoset = isFastInfoset;
        this.acceptFastInfoset = acceptFastInfoset;
        
        headers = new MimeHeaders();
        headers.setHeader("Accept", getExpectedAcceptHeader());
        contentType = new ContentType();
    }

    /**
     * Shallow copy.
     */
    protected MessageImpl(SOAPMessage msg) {
        if (!(msg instanceof MessageImpl)) {
            // don't know how to handle this.
        }
        MessageImpl src = (MessageImpl) msg;
        this.headers = src.headers;
        this.soapPartImpl = src.soapPartImpl;
        this.attachments = src.attachments;
        this.saved = src.saved;
        this.messageBytes = src.messageBytes;
        this.messageByteCount = src.messageByteCount;
        this.properties = src.properties;
        this.contentType = src.contentType;
    }

    /**
     * @param stat
     *      the mask value obtained from {@link #identifyContentType(ContentType)}
     */
    protected static boolean isSoap1_1Content(int stat) {
        return (stat & SOAP1_1_FLAG) != 0;
    }

    /**
     * @param stat
     *      the mask value obtained from {@link #identifyContentType(ContentType)}
     */
    protected static boolean isSoap1_2Content(int stat) {
        return (stat & SOAP1_2_FLAG) != 0;
    }

     private static boolean isMimeMultipartXOPSoap1_2Package(ContentType contentType) {
        String type = contentType.getParameter("type");
        if (type == null) {
            return false;
        }
        type = type.toLowerCase();
        if (!type.startsWith("application/xop+xml")) {
            return false;
        }
        String startinfo = contentType.getParameter("start-info");
        if (startinfo == null) {
            return false;
        }
        startinfo = startinfo.toLowerCase();
        return isEqualToSoap1_2Type(startinfo);
    }


     //private static boolean isMimeMultipartXOPPackage(ContentType contentType) {
     private static boolean isMimeMultipartXOPSoap1_1Package(ContentType contentType) {
        String type = contentType.getParameter("type");
        if(type==null)
            return false;
                                                                                                                             
        type = type.toLowerCase();
        if(!type.startsWith("application/xop+xml"))
            return false;
                                                                                                                             
        String startinfo = contentType.getParameter("start-info");
        if(startinfo == null)
            return false;
        startinfo = startinfo.toLowerCase();
        return isEqualToSoap1_1Type(startinfo);
    }
 
    private static boolean isSOAPBodyXOPPackage(ContentType contentType){
        String primary = contentType.getPrimaryType();
        String sub = contentType.getSubType();

        if (primary.equalsIgnoreCase("application")) {
            if (sub.equalsIgnoreCase("xop+xml")) {
                String type = getTypeParameter(contentType);
                return isEqualToSoap1_2Type(type) || isEqualToSoap1_1Type(type);
            }
        }
        return false;
    }
    
    /**
     * Construct a message from an input stream. When messages are
     * received, there's two parts -- the transport headers and the
     * message content in a transport specific stream.
     */
    protected MessageImpl(MimeHeaders headers, final InputStream in)
        throws SOAPExceptionImpl {
        contentType = parseContentType(headers);
        init(headers,identifyContentType(contentType),contentType,in);
    }

    private static ContentType parseContentType(MimeHeaders headers) throws SOAPExceptionImpl {
        final String ct;
        if (headers != null)
            ct = getContentType(headers);
        else {
            log.severe("SAAJ0550.soap.null.headers");
            throw new SOAPExceptionImpl("Cannot create message: " +
                                        "Headers can't be null");
        }

        if (ct == null) {
            log.severe("SAAJ0532.soap.no.Content-Type");
            throw new SOAPExceptionImpl("Absent Content-Type");
        }
        try {
            return new ContentType(ct);
        } catch (Throwable ex) {
            log.severe("SAAJ0535.soap.cannot.internalize.message");
            throw new SOAPExceptionImpl("Unable to internalize message", ex);
        }
    }

    /**
     * Construct a message from an input stream. When messages are
     * received, there's two parts -- the transport headers and the
     * message content in a transport specific stream.
     *
     * @param contentType
     *      The parsed content type header from the headers variable.
     *      This is redundant parameter, but it avoids reparsing this header again.
     * @param stat
     *      The result of {@link #identifyContentType(ContentType)} over
     *      the contentType parameter. This redundant parameter, but it avoids
     *      recomputing this information again.
     */
    protected MessageImpl(MimeHeaders headers, final ContentType contentType, int stat, final InputStream in) throws SOAPExceptionImpl {
        init(headers, stat, contentType, in);

    }

    private void init(MimeHeaders headers, int stat, final ContentType contentType, final InputStream in) throws SOAPExceptionImpl {
        this.headers = headers;

        try {

            // Set isFastInfoset/acceptFastInfoset flag based on MIME type
            if ((stat & FI_ENCODED_FLAG) > 0) {
                isFastInfoset = acceptFastInfoset = true;
            }

            // If necessary, inspect Accept header to set acceptFastInfoset
            if (!isFastInfoset) {
                String[] values = headers.getHeader("Accept");
                if (values != null) {
                    for (int i = 0; i < values.length; i++) {
                        StringTokenizer st = new StringTokenizer(values[i], ",");
                        while (st.hasMoreTokens()) {
                            final String token = st.nextToken().trim();
                            if (token.equalsIgnoreCase("application/fastinfoset") ||
                                token.equalsIgnoreCase("application/soap+fastinfoset")) {
                                acceptFastInfoset = true;
                                break;
                            }
                        }
                    }
                }
            }

            if (!isCorrectSoapVersion(stat)) {
                log.log(
                    Level.SEVERE,
                    "SAAJ0533.soap.incorrect.Content-Type",
                    new String[] {
                        contentType.toString(),
                        getExpectedContentType()});
                throw new SOAPVersionMismatchException(
                    "Cannot create message: incorrect content-type for SOAP version. Got: "
                        + contentType
                        + " Expected: "
                        + getExpectedContentType());
            }

            if ((stat & PLAIN_XML_FLAG) != 0) {
                if (isFastInfoset) {
                    getSOAPPart().setContent(
                        FastInfosetReflection.FastInfosetSource_new(in));
                } else {
                    initCharsetProperty(contentType);
                    getSOAPPart().setContent(new StreamSource(in));
                }
            }
            else if ((stat & MIME_MULTIPART_FLAG) != 0) {
                DataSource ds = new DataSource() {
                    public InputStream getInputStream() {
                        return in;
                    }

                    public OutputStream getOutputStream() {
                        return null;
                    }

                    public String getContentType() {
                        return contentType.toString();
                    }

                    public String getName() {
                        return "";
                    }
                };

                multiPart = null;
                if (useMimePull) {
                    multiPart = new MimePullMultipart(ds,contentType);
                } else if (switchOffBM) {
                    multiPart = new MimeMultipart(ds,contentType);
                } else {
                    multiPart = new BMMimeMultipart(ds,contentType);
                }

                String startParam = contentType.getParameter("start");
                MimeBodyPart soapMessagePart = null;
                InputStream soapPartInputStream = null;
                String contentID = null;
                if (switchOffBM || switchOffLazyAttachment) {
                    if(startParam == null) {
                        soapMessagePart = multiPart.getBodyPart(0);
                        for (int i = 1; i < multiPart.getCount(); i++) {
                            initializeAttachment(multiPart, i);
                        }
                    } else {
                        soapMessagePart = multiPart.getBodyPart(startParam);
                        for (int i = 0; i < multiPart.getCount(); i++) {
                            contentID = multiPart.getBodyPart(i).getContentID();
                            if(!contentID.equals(startParam))
                                initializeAttachment(multiPart, i);
                        }
                    }
                } else {
                    if (useMimePull) {
                        MimePullMultipart mpMultipart = (MimePullMultipart)multiPart;
                        MIMEPart sp = mpMultipart.readAndReturnSOAPPart();
                        soapMessagePart = new MimeBodyPart(sp);
                        soapPartInputStream = sp.readOnce();
                    } else {
                        BMMimeMultipart bmMultipart =
                                (BMMimeMultipart) multiPart;
                        InputStream stream = bmMultipart.initStream();

                        SharedInputStream sin = null;
                        if (stream instanceof SharedInputStream) {
                            sin = (SharedInputStream) stream;
                        }

                        String boundary = "--" +
                                contentType.getParameter("boundary");
                        byte[] bndbytes = ASCIIUtility.getBytes(boundary);
                        if (startParam == null) {
                            soapMessagePart =
                                    bmMultipart.getNextPart(stream, bndbytes, sin);
                            bmMultipart.removeBodyPart(soapMessagePart);
                        } else {
                            MimeBodyPart bp = null;
                            try {
                                while (!startParam.equals(contentID)) {
                                    bp = bmMultipart.getNextPart(
                                            stream, bndbytes, sin);
                                    contentID = bp.getContentID();
                                }
                                soapMessagePart = bp;
                                bmMultipart.removeBodyPart(bp);
                            } catch (Exception e) {
                                throw new SOAPExceptionImpl(e);
                            }
                        }
                    }
                }

                if (soapPartInputStream == null && soapMessagePart != null) {
                    soapPartInputStream = soapMessagePart.getInputStream();
                }

                ContentType soapPartCType = new ContentType(
                                            soapMessagePart.getContentType());
                initCharsetProperty(soapPartCType);
                String baseType = soapPartCType.getBaseType().toLowerCase();
                if(!(isEqualToSoap1_1Type(baseType)
                  || isEqualToSoap1_2Type(baseType)
                  || isSOAPBodyXOPPackage(soapPartCType))) {
                    log.log(Level.SEVERE,
                            "SAAJ0549.soap.part.invalid.Content-Type",
                            new Object[] {baseType});
                    throw new SOAPExceptionImpl(
                            "Bad Content-Type for SOAP Part : " +
                            baseType);
                }

                SOAPPart soapPart = getSOAPPart();
                setMimeHeaders(soapPart, soapMessagePart);
                soapPart.setContent(isFastInfoset ?
                     (Source) FastInfosetReflection.FastInfosetSource_new(
                         soapPartInputStream) :
                     (Source) new StreamSource(soapPartInputStream));
            } else {
                log.severe("SAAJ0534.soap.unknown.Content-Type");
                throw new SOAPExceptionImpl("Unrecognized Content-Type");
            }
        } catch (Throwable ex) {
            log.severe("SAAJ0535.soap.cannot.internalize.message");
            throw new SOAPExceptionImpl("Unable to internalize message", ex);
        }
        needsSave();
    }

    public boolean isFastInfoset() {
        return isFastInfoset;
    }

    public boolean acceptFastInfoset() {
        return acceptFastInfoset;
    }
    
    public void setIsFastInfoset(boolean value) {
        if (value != isFastInfoset) {
            isFastInfoset = value;
            if (isFastInfoset) {
                acceptFastInfoset = true;
            }
            saved = false;      // ensure transcoding if necessary
        }
    }
    
    public Object getProperty(String property) {
        return (String) properties.get(property);
    }

    public void setProperty(String property, Object value) {
        verify(property, value);
        properties.put(property, value);
    }

    private void verify(String property, Object value) {
        if (property.equalsIgnoreCase(SOAPMessage.WRITE_XML_DECLARATION)) {
            if (!("true".equals(value) || "false".equals(value)))
                throw new RuntimeException(
                    property + " must have value false or true");

            try {
                EnvelopeImpl env = (EnvelopeImpl) getSOAPPart().getEnvelope();
                if ("true".equalsIgnoreCase((String)value)) {
                    env.setOmitXmlDecl("no");
                } else if ("false".equalsIgnoreCase((String)value)) {
                    env.setOmitXmlDecl("yes");
                }
            } catch (Exception e) {
                log.log(Level.SEVERE, "SAAJ0591.soap.exception.in.set.property", 
                    new Object[] {e.getMessage(), "javax.xml.soap.write-xml-declaration"});
                throw new RuntimeException(e);
            }
            return;
        }

        if (property.equalsIgnoreCase(SOAPMessage.CHARACTER_SET_ENCODING)) {
            try {
                ((EnvelopeImpl) getSOAPPart().getEnvelope()).setCharsetEncoding((String)value);
            } catch (Exception e) {
                log.log(Level.SEVERE, "SAAJ0591.soap.exception.in.set.property", 
                    new Object[] {e.getMessage(), "javax.xml.soap.character-set-encoding"});
                throw new RuntimeException(e);
            }
        }
    }

    protected abstract boolean isCorrectSoapVersion(int contentTypeId);
    
    protected abstract String getExpectedContentType();
    protected abstract String getExpectedAcceptHeader();

    /**
     * Sniffs the Content-Type header so that we can determine how to process.
     *
     * <p>
     * In the absence of type attribute we assume it to be text/xml.
     * That would mean we're easy on accepting the message and
     * generate the correct thing (as the SWA spec also specifies
     * that the type parameter should always be text/xml)
     *
     * @return
     *      combination of flags, such as PLAIN_XML_CODE and MIME_MULTIPART_CODE.
     */
    // SOAP1.2 allow SOAP1.2 content type
    static int identifyContentType(ContentType ct)
        throws SOAPExceptionImpl {
        // TBD
        //    Is there anything else we need to verify here?

        String primary = ct.getPrimaryType().toLowerCase();
        String sub = ct.getSubType().toLowerCase();

        if (primary.equals("multipart")) {
            if (sub.equals("related")) {
                String type = getTypeParameter(ct);
                if (isEqualToSoap1_1Type(type)) {
                    return (type.equals("application/fastinfoset") ?
                           FI_ENCODED_FLAG : 0) | MIME_MULTIPART_FLAG | SOAP1_1_FLAG;
                } 
                else if (isEqualToSoap1_2Type(type)) {
                    return (type.equals("application/soap+fastinfoset") ?
                           FI_ENCODED_FLAG : 0) | MIME_MULTIPART_FLAG | SOAP1_2_FLAG;
                /*} else if (isMimeMultipartXOPPackage(ct)) {
                    return MIME_MULTIPART_XOP_FLAG;*/
                } else if (isMimeMultipartXOPSoap1_1Package(ct)) {
                    return MIME_MULTIPART_XOP_SOAP1_1_FLAG;
                } else if (isMimeMultipartXOPSoap1_2Package(ct)) {
                    return MIME_MULTIPART_XOP_SOAP1_2_FLAG;
                } else {
                    log.severe("SAAJ0536.soap.content-type.mustbe.multipart");
                    throw new SOAPExceptionImpl(
                        "Content-Type needs to be Multipart/Related "
                            + "and with \"type=text/xml\" "
                            + "or \"type=application/soap+xml\"");
                }
            } else {
                log.severe("SAAJ0537.soap.invalid.content-type");
                throw new SOAPExceptionImpl(
                    "Invalid Content-Type: " + primary + '/' + sub);
            }
        } 
        else if (isSoap1_1Type(primary, sub)) {
            return (primary.equalsIgnoreCase("application")
                    && sub.equalsIgnoreCase("fastinfoset") ?
                        FI_ENCODED_FLAG : 0) 
                   | PLAIN_XML_FLAG | SOAP1_1_FLAG;
        } 
        else if (isSoap1_2Type(primary, sub)) {
            return (primary.equalsIgnoreCase("application")
                    && sub.equalsIgnoreCase("soap+fastinfoset") ?
                        FI_ENCODED_FLAG : 0) 
                   | PLAIN_XML_FLAG | SOAP1_2_FLAG;
        } else if(isSOAPBodyXOPPackage(ct)){
            return XOP_FLAG;
        } else {
            log.severe("SAAJ0537.soap.invalid.content-type");
            throw new SOAPExceptionImpl(
                "Invalid Content-Type:"
                    + primary
                    + '/'
                    + sub
                    + ". Is this an error message instead of a SOAP response?");
        }
    }

    /**
     * Obtains the type parameter of the Content-Type header. Defaults to "text/xml".
     */
    private static String getTypeParameter(ContentType contentType) {
        String p = contentType.getParameter("type");
        if(p!=null)
            return p.toLowerCase();
        else
            return "text/xml";
    }

    public MimeHeaders getMimeHeaders() {
        return this.headers;
    }

    final static String getContentType(MimeHeaders headers) {
        String[] values = headers.getHeader("Content-Type");
        if (values == null)
            return null;
        else
            return values[0];
    }
    
    /*
     * Get the complete ContentType value along with optional parameters.
     */
    public String getContentType() {
        return getContentType(this.headers);
    }

    public void setContentType(String type) {
        headers.setHeader("Content-Type", type);
        needsSave();
    }

    private ContentType contentType() {
        ContentType ct = null;
        try {
            String currentContent = getContentType();
            if (currentContent == null) {
                return this.contentType;
            }
            ct = new ContentType(currentContent);
        } catch (Exception e) {
            // what to do here?
        }
        return ct;
    }

    /*
     * Return the MIME type string, without the parameters.
     */
    public String getBaseType() {
        return contentType().getBaseType();
    }

    public void setBaseType(String type) {
        ContentType ct = contentType();
        ct.setParameter("type", type);
        headers.setHeader("Content-Type", ct.toString());
        needsSave();
    }

    public String getAction() {
        return contentType().getParameter("action");
    }

    public void setAction(String action) {
        ContentType ct = contentType();
        ct.setParameter("action", action);
        headers.setHeader("Content-Type", ct.toString());
        needsSave();
    }

    public String getCharset() {
        return contentType().getParameter("charset");
    }

    public void setCharset(String charset) {
        ContentType ct = contentType();
        ct.setParameter("charset", charset);
        headers.setHeader("Content-Type", ct.toString());
        needsSave();
    }

    /**
     * All write methods (i.e setters) should call this method in
     * order to make sure that a save is necessary since the state
     * has been modified.
     */
    private final void needsSave() {
        saved = false;
    }

    public  boolean saveRequired() {
        return saved != true;
    }

    public String getContentDescription() {
        String[] values = headers.getHeader("Content-Description");
        if (values != null && values.length > 0)
            return values[0];
        return null;
    }

    public void setContentDescription(String description) {
        headers.setHeader("Content-Description", description);
        needsSave();
    }

    public abstract SOAPPart getSOAPPart();

    public void removeAllAttachments() {
        try {
            initializeAllAttachments();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }

        if (attachments != null) {
            attachments.clear();
            needsSave();
        }
    }

    public int countAttachments() {
        try {
            initializeAllAttachments();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
        if (attachments != null)
            return attachments.size();
        return 0;
    }

    public void addAttachmentPart(AttachmentPart attachment) {
        try {
            initializeAllAttachments();
            this.optimizeAttachmentProcessing = true;
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
        if (attachments == null)
            attachments = new FinalArrayList();

        attachments.add(attachment);

        needsSave();
    }

    static private final Iterator nullIter = Collections.EMPTY_LIST.iterator();

    public Iterator getAttachments() {
        try {
            initializeAllAttachments();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
        if (attachments == null)
            return nullIter;
        return attachments.iterator();
    }

    private void setFinalContentType(String charset) {
        ContentType ct = contentType();
        if (ct == null) {
            ct = new ContentType();
        }
        String[] split = getExpectedContentType().split("/");
        ct.setPrimaryType(split[0]);
        ct.setSubType(split[1]);
        ct.setParameter("charset", charset);
        headers.setHeader("Content-Type", ct.toString());
    }

    private class MimeMatchingIterator implements Iterator {
        public MimeMatchingIterator(MimeHeaders headers) {
            this.headers = headers;
            this.iter = attachments.iterator();
        }

        private Iterator iter;
        private MimeHeaders headers;
        private Object nextAttachment;

        public boolean hasNext() {
            if (nextAttachment == null)
                nextAttachment = nextMatch();
            return nextAttachment != null;
        }

        public Object next() {
            if (nextAttachment != null) {
                Object ret = nextAttachment;
                nextAttachment = null;
                return ret;
            }

            if (hasNext())
                return nextAttachment;

            return null;
        }

        Object nextMatch() {
            while (iter.hasNext()) {
                AttachmentPartImpl ap = (AttachmentPartImpl) iter.next();
                if (ap.hasAllHeaders(headers))
                    return ap;
            }
            return null;
        }

        public void remove() {
            iter.remove();
        }
    }

    public Iterator getAttachments(MimeHeaders headers) {
        try {
            initializeAllAttachments();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
        if (attachments == null)
            return nullIter;

        return new MimeMatchingIterator(headers);
    }

    public void removeAttachments(MimeHeaders headers) {
        try {
            initializeAllAttachments();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
        if (attachments == null)
            return ;

        Iterator it =  new MimeMatchingIterator(headers);
        while (it.hasNext()) {
            int index = attachments.indexOf(it.next());
            attachments.set(index, null);
        }
        FinalArrayList f = new FinalArrayList();
        for (int i = 0; i < attachments.size(); i++) {
            if (attachments.get(i) != null) {
                f.add(attachments.get(i));
            }
        }
        attachments = f;
       // needsSave();
    }

    public AttachmentPart createAttachmentPart() {
        return new AttachmentPartImpl();
    }

    public  AttachmentPart getAttachment(SOAPElement element) 
        throws SOAPException {
        try {
            initializeAllAttachments();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
        String uri;
        String hrefAttr = element.getAttribute("href");
        if ("".equals(hrefAttr)) {
            Node node = getValueNodeStrict(element);
            String swaRef = null;
            if (node != null) {
                swaRef = node.getValue();
            }
            if (swaRef == null || "".equals(swaRef)) {
                return null;
            } else {
                uri = swaRef;
            }
        } else {
            uri = hrefAttr;
        }
        return getAttachmentPart(uri);
    }

    private Node getValueNodeStrict(SOAPElement element) {
        Node node = (Node)element.getFirstChild();
        if (node != null) {
            if (node.getNextSibling() == null
                && node.getNodeType() == org.w3c.dom.Node.TEXT_NODE) {
                return node;
            } else {
                return null;
            }
        }
        return null;
    }

    
    private AttachmentPart getAttachmentPart(String uri) throws SOAPException {
        AttachmentPart _part;
        try {
            if (uri.startsWith("cid:")) {
                // rfc2392
                uri = '<'+uri.substring("cid:".length())+'>';
                
                MimeHeaders headersToMatch = new MimeHeaders();
                headersToMatch.addHeader(CONTENT_ID, uri);
                
                Iterator i = this.getAttachments(headersToMatch);
                _part = (i == null) ? null : (AttachmentPart)i.next();
            } else {
                // try content-location
                MimeHeaders headersToMatch = new MimeHeaders();
                headersToMatch.addHeader(CONTENT_LOCATION, uri);
                   
                Iterator i = this.getAttachments(headersToMatch);
                _part = (i == null) ? null : (AttachmentPart)i.next();
            }

            // try  auto-generated JAXRPC CID
            if (_part == null) {
                Iterator j = this.getAttachments();
                    
                while (j.hasNext()) {
                    AttachmentPart p = (AttachmentPart)j.next();
                    String cl = p.getContentId();
                    if (cl != null) {    
                        // obtain the partname
                        int eqIndex = cl.indexOf("=");
                        if (eqIndex > -1) {
                            cl = cl.substring(1, eqIndex);
                            if (cl.equalsIgnoreCase(uri)) {
                                _part = p;
                                 break;
                            }
                        }
                    } 
                }
            }
            
        } catch (Exception se) {
            log.log(Level.SEVERE, "SAAJ0590.soap.unable.to.locate.attachment", new Object[] {uri});
            throw new SOAPExceptionImpl(se);
        }
        return _part;
    }
    
    private final InputStream getHeaderBytes()
        throws IOException {
        SOAPPartImpl sp = (SOAPPartImpl) getSOAPPart();
        return sp.getContentAsStream();
    }

    private String convertToSingleLine(String contentType) {
        StringBuffer buffer = new StringBuffer();
        for (int i = 0; i < contentType.length(); i ++) {
            char c = contentType.charAt(i);
            if (c != '\r' && c != '\n' && c != '\t')
                buffer.append(c);
        }
        return buffer.toString();
    }

    private MimeMultipart getMimeMessage() throws SOAPException {
        try {
            SOAPPartImpl soapPart = (SOAPPartImpl) getSOAPPart();
            MimeBodyPart mimeSoapPart = soapPart.getMimePart();
            
            /*
             * Get content type from this message instead of soapPart
             * to ensure agreement if soapPart is transcoded (XML <-> FI)
             */
            ContentType soapPartCtype = new ContentType(getExpectedContentType());          
            
            if (!isFastInfoset) {
                soapPartCtype.setParameter("charset", initCharset());
            }
            mimeSoapPart.setHeader("Content-Type", soapPartCtype.toString());

            MimeMultipart headerAndBody = null;

            if (!switchOffBM && !switchOffLazyAttachment && 
                   (multiPart != null) && !attachmentsInitialized) {
                headerAndBody = new BMMimeMultipart();
                headerAndBody.addBodyPart(mimeSoapPart);
                if (attachments != null) { 
                    for (Iterator eachAttachment = attachments.iterator();
                         eachAttachment.hasNext();) {
                        headerAndBody.addBodyPart(
                            ((AttachmentPartImpl) eachAttachment.next())
                                .getMimePart());
                    }
                }
                InputStream in = ((BMMimeMultipart)multiPart).getInputStream();
                if (!((BMMimeMultipart)multiPart).lastBodyPartFound() &&
                    !((BMMimeMultipart)multiPart).isEndOfStream()) {
                    ((BMMimeMultipart)headerAndBody).setInputStream(in);
                    ((BMMimeMultipart)headerAndBody).setBoundary(
                        ((BMMimeMultipart)multiPart).getBoundary());
                    ((BMMimeMultipart)headerAndBody).
                        setLazyAttachments(lazyAttachments);
                }

            } else { 
                headerAndBody = new MimeMultipart();
                headerAndBody.addBodyPart(mimeSoapPart);

                for (Iterator eachAttachement = getAttachments();
                    eachAttachement.hasNext();
                    ) {
                    headerAndBody.addBodyPart(
                        ((AttachmentPartImpl) eachAttachement.next())
                            .getMimePart());
                }
            }

            ContentType contentType = headerAndBody.getContentType();

            ParameterList l = contentType.getParameterList();

            // set content type depending on SOAP version
            l.set("type", getExpectedContentType());
            l.set("boundary", contentType.getParameter("boundary"));
            ContentType nct = new ContentType("multipart", "related", l);

            headers.setHeader(
                "Content-Type",
                convertToSingleLine(nct.toString()));
          // TBD
          //    Set content length MIME header here.

            return headerAndBody;
        } catch (SOAPException ex) {
            throw ex;
        } catch (Throwable ex) {
            log.severe("SAAJ0538.soap.cannot.convert.msg.to.multipart.obj");
            throw new SOAPExceptionImpl(
                "Unable to convert SOAP message into "
                    + "a MimeMultipart object",
                ex);
        }
    }

    private String initCharset() {

        String charset = null;

        String[] cts = getMimeHeaders().getHeader("Content-Type");
        if ((cts != null) && (cts[0] != null)) {
            charset = getCharsetString(cts[0]);
        }

        if (charset == null) {
            charset = (String) getProperty(CHARACTER_SET_ENCODING);
        }

        if (charset != null) {
            return charset;
        }

        return "utf-8";
    }

    private String getCharsetString(String s) {
        try {
            int index = s.indexOf(";");
            if(index < 0)
                return null;
            ParameterList pl = new ParameterList(s.substring(index));
            return pl.get("charset");
        } catch(Exception e) {
            return null;
        }
    }

    public void saveChanges() throws SOAPException {

        // suck in all the data from the attachments and have it
        // ready for writing/sending etc.

        String charset = initCharset();

        /*if (countAttachments() == 0) {*/
        int attachmentCount = (attachments == null) ? 0 : attachments.size();
        if (attachmentCount == 0) {
            if (!switchOffBM && !switchOffLazyAttachment && 
                !attachmentsInitialized && (multiPart != null)) {
                // so there might be attachments
                attachmentCount = 1;
            }
        }

        try {
            if ((attachmentCount == 0) && !hasXOPContent()) {
                InputStream in;
                try{
                /*
                 * Not sure why this is called getHeaderBytes(), but it actually
                 * returns the whole message as a byte stream. This stream could
                 * be either XML of Fast depending on the mode.
                 */
                    in = getHeaderBytes();
                    // no attachments, hence this property can be false
                    this.optimizeAttachmentProcessing = false;
                    if (SOAPPartImpl.lazyContentLength) {
                        inputStreamAfterSaveChanges = in;
                    }
                } catch (IOException ex) {
                    log.severe("SAAJ0539.soap.cannot.get.header.stream");
                    throw new SOAPExceptionImpl(
                            "Unable to get header stream in saveChanges: ",
                            ex);
                }

                if (in instanceof ByteInputStream) {
                    ByteInputStream bIn = (ByteInputStream)in;
                    messageBytes = bIn.getBytes();
                    messageByteCount = bIn.getCount();
                }

                setFinalContentType(charset);
                /*
                headers.setHeader(
                        "Content-Type",
                        getExpectedContentType() +
                        (isFastInfoset ? "" : "; charset=" + charset));*/
                if (messageByteCount > 0) {
                    headers.setHeader(
                            "Content-Length",
                            Integer.toString(messageByteCount));
                }
            } else {
                if(hasXOPContent())
                    mmp = getXOPMessage();
                else
                    mmp = getMimeMessage();
            }
        } catch (Throwable ex) {
            log.severe("SAAJ0540.soap.err.saving.multipart.msg");
            throw new SOAPExceptionImpl(
                    "Error during saving a multipart message",
                    ex);
        }

        // FIX ME -- SOAP Action replaced by Content-Type optional parameter action
        /*
        if(isCorrectSoapVersion(SOAP1_1_FLAG)) {

            String[] soapAction = headers.getHeader("SOAPAction");

            if (soapAction == null || soapAction.length == 0)
                headers.setHeader("SOAPAction", "\"\"");

        } 
        */

        saved = true;
    }

    private MimeMultipart getXOPMessage() throws SOAPException {
        try {
            MimeMultipart headerAndBody = new MimeMultipart();
            SOAPPartImpl soapPart =  (SOAPPartImpl)getSOAPPart();
            MimeBodyPart mimeSoapPart = soapPart.getMimePart();
            ContentType soapPartCtype =
                new ContentType("application/xop+xml");
            soapPartCtype.setParameter("type", getExpectedContentType());
            String charset = initCharset();
            soapPartCtype.setParameter("charset", charset);
            mimeSoapPart.setHeader("Content-Type", soapPartCtype.toString());
            headerAndBody.addBodyPart(mimeSoapPart);
                                                                                                                                
            for (Iterator eachAttachement = getAttachments();
                eachAttachement.hasNext();
                ) {
                headerAndBody.addBodyPart(
                    ((AttachmentPartImpl) eachAttachement.next())
                        .getMimePart());
            }
                                                                                                                                
            ContentType contentType = headerAndBody.getContentType();
                                                                                                                                
            ParameterList l = contentType.getParameterList();
                                                                                                                                
            //lets not write start-info for now till we get servlet fix done
            l.set("start-info", getExpectedContentType());//+";charset="+initCharset());
                                                                                                                                
            // set content type depending on SOAP version
            l.set("type", "application/xop+xml");

            if (isCorrectSoapVersion(SOAP1_2_FLAG)) { 
                 String action = getAction();
                 if(action != null)
                     l.set("action", action);
            }
       
            l.set("boundary", contentType.getParameter("boundary"));
            ContentType nct = new ContentType("Multipart", "Related", l);
            headers.setHeader(
                "Content-Type",
                convertToSingleLine(nct.toString()));
            // TBD
            //    Set content length MIME header here.
                                                                                                                                
            return headerAndBody;
        } catch (SOAPException ex) {
            throw ex;
        } catch (Throwable ex) {
            log.severe("SAAJ0538.soap.cannot.convert.msg.to.multipart.obj");
            throw new SOAPExceptionImpl(
                "Unable to convert SOAP message into "
                    + "a MimeMultipart object",
                ex);
        }
                                                                                                                                
    }

    private boolean hasXOPContent() throws ParseException {
        String type = getContentType();
        if(type == null)
            return false;
        ContentType ct = new ContentType(type);
        //return isMimeMultipartXOPPackage(ct) || isSOAPBodyXOPPackage(ct);
        return isMimeMultipartXOPSoap1_1Package(ct) ||
            isMimeMultipartXOPSoap1_2Package(ct) || isSOAPBodyXOPPackage(ct);

    }

    public void writeTo(OutputStream out) throws SOAPException, IOException {
        if (saveRequired()){
            this.optimizeAttachmentProcessing = true;
            saveChanges();
        }

        if(!optimizeAttachmentProcessing){
            if (SOAPPartImpl.lazyContentLength && messageByteCount <= 0) {
                byte[] buf = new byte[1024];

                int length = 0;
                while( (length = inputStreamAfterSaveChanges.read(buf)) != -1) {
                    out.write(buf,0, length);
                    messageByteCount += length;
                }
                if (messageByteCount > 0) {
                    headers.setHeader(
                            "Content-Length",
                            Integer.toString(messageByteCount));
                }
            } else {
                out.write(messageBytes, 0, messageByteCount);
            }
        }
        else{
            try{
                if(hasXOPContent()){
                    mmp.writeTo(out);
                }else{
                    mmp.writeTo(out);
                    if (!switchOffBM && !switchOffLazyAttachment &&
                            (multiPart != null) && !attachmentsInitialized) {
                        ((BMMimeMultipart)multiPart).setInputStream(
                                ((BMMimeMultipart)mmp).getInputStream());
                    }
                } 
            } catch(Exception ex){
                log.severe("SAAJ0540.soap.err.saving.multipart.msg");
                throw new SOAPExceptionImpl(
                        "Error during saving a multipart message",
                        ex);                
            }
        }

        if(isCorrectSoapVersion(SOAP1_1_FLAG)) {

            String[] soapAction = headers.getHeader("SOAPAction");

            if (soapAction == null || soapAction.length == 0)
                headers.setHeader("SOAPAction", "\"\"");

        } 
        
        messageBytes = null;
        needsSave();
    }
    
    public SOAPBody getSOAPBody() throws SOAPException {
        SOAPBody body = getSOAPPart().getEnvelope().getBody();
        /*if (body == null) {
             throw new SOAPException("No SOAP Body was found in the SOAP Message");
        }*/
        return body;
    }

    public SOAPHeader getSOAPHeader() throws SOAPException {
        SOAPHeader hdr = getSOAPPart().getEnvelope().getHeader();
        /*if (hdr == null) {
            throw new SOAPException("No SOAP Header was found in the SOAP Message");
        }*/
        return hdr;
    }

    private void initializeAllAttachments () 
        throws MessagingException, SOAPException {
        if (switchOffBM || switchOffLazyAttachment) {
            return;
        }

        if (attachmentsInitialized || (multiPart == null)) {
            return;
        }
                                                                                
        if (attachments == null)
            attachments = new FinalArrayList();
                                                                                
        int count = multiPart.getCount();
        for (int i=0; i < count; i++ ) {
            initializeAttachment(multiPart.getBodyPart(i));
        }
        attachmentsInitialized = true;
        //multiPart = null;
        needsSave();
     }
                                                                                
    private void initializeAttachment(MimeBodyPart mbp) throws SOAPException {
        AttachmentPartImpl attachmentPart = new AttachmentPartImpl();
        DataHandler attachmentHandler = mbp.getDataHandler();
        attachmentPart.setDataHandler(attachmentHandler);
                                                                                
        AttachmentPartImpl.copyMimeHeaders(mbp, attachmentPart);
        attachments.add(attachmentPart);
    }

    private void initializeAttachment(MimeMultipart multiPart, int i)
        throws Exception {
        MimeBodyPart currentBodyPart = multiPart.getBodyPart(i);
        AttachmentPartImpl attachmentPart = new AttachmentPartImpl();

        DataHandler attachmentHandler = currentBodyPart.getDataHandler();
        attachmentPart.setDataHandler(attachmentHandler);

        AttachmentPartImpl.copyMimeHeaders(currentBodyPart, attachmentPart);
        addAttachmentPart(attachmentPart);
    }

    private void setMimeHeaders(SOAPPart soapPart,
            MimeBodyPart soapMessagePart) throws Exception {

        // first remove the existing content-type
        soapPart.removeAllMimeHeaders();
        // add everything present in soapMessagePart
        List headers = soapMessagePart.getAllHeaders();
        int sz = headers.size();
        for( int i=0; i<sz; i++ ) {
            Header h = (Header) headers.get(i);
            soapPart.addMimeHeader(h.getName(), h.getValue());
        }
    }

    private void initCharsetProperty(ContentType contentType) {
        String charset = contentType.getParameter("charset");
        if (charset != null) {
            ((SOAPPartImpl) getSOAPPart()).setSourceCharsetEncoding(charset);
            if(!charset.equalsIgnoreCase("utf-8"))
                setProperty(CHARACTER_SET_ENCODING, charset);
        }
    }

    public void setLazyAttachments(boolean flag) {
        lazyAttachments = flag;
    }

}
