/*
 * Copyright 2001-2004 The Apache Software Foundation.
 *
 * 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.
 */

/* @author Rob Jellinghaus (robj@unrealities.com) */
/* @author Rick Rineholt */
package org.jboss.axis.attachments;

import org.jboss.axis.AxisFault;
import org.jboss.axis.Message;
import org.jboss.axis.MessagePart;
import org.jboss.axis.Part;
import org.jboss.axis.utils.Messages;
import org.jboss.logging.Logger;

import javax.activation.DataHandler;
import java.io.InputStream;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.StringTokenizer;

/**
 * Implements the Attachment interface, via an actual Hashmap of actual
 * AttachmentParts.
 */
public class AttachmentsImpl implements Attachments
{
   private static Logger log = Logger.getLogger(AttachmentsImpl.class.getName());

   /**
    * Field attachments.
    */
   private HashMap attachments = new java.util.HashMap();

   /**
    * Field orderedAttachments.
    */
   private LinkedList orderedAttachments = new LinkedList();

   /**
    * Field soapPart.
    */
   protected MessagePart soapPart = null;

   /**
    * The actual stream to manage the multi-related input stream.
    */
   protected MultiPartInputStream mpartStream =
           null;
   /**
    * The form of the attachments, whether MIME or DIME.
    */
   protected int sendtype = Attachments.SEND_TYPE_NOTSET;

   /**
    * This is the content location as specified in SOAP with Attachments.
    * This maybe null if the message had no Content-Location specifed.
    */
   protected String contentLocation = null;

   /**
    * The HashMap for DataHandler Managements.
    */
   private HashMap stackDataHandler = new HashMap();

   /**
    * Construct one of these on a parent Message.
    * Should only ever be called by Message constructor!
    * <p/>
    * 1. mime-type: [unknown]
    * do nothing
    * <p/>
    * 2. mime-type: multipart/related
    * 2.1 create a MultiPartRelatedInputStream that does the parsing of the input stream
    * 2.2 create a SOAPPart from the MultiPartRelatedInputStream
    * <p/>
    * 3. mime-type: application/dime
    * 3.1 create a MultiPartDimeInputStream that does the parsing of the input stream
    * 3.2 create a SOAPPart from the MultiPartDimeInputStream
    *
    * @param intialContents  should be anything but today only a stream is
    *                        supported.
    * @param contentType     The mime content type of the stream for transports
    *                        that provide it.
    * @param contentLocation
    * @throws org.jboss.axis.AxisFault
    */
   public AttachmentsImpl(Object intialContents, String contentType, String contentLocation)
           throws org.jboss.axis.AxisFault
   {

      if (contentLocation != null)
      {
         contentLocation = contentLocation.trim();

         if (contentLocation.length() == 0)
         {
            contentLocation = null;
         }
      }

      this.contentLocation = contentLocation;

      if (contentType != null)
      {
         if (contentType.equals(Message.MIME_UNKNOWN))
         {
            log.warn("Unknown contentType");
         }
         else
         {
            StringTokenizer st = new StringTokenizer(contentType, " \t;");

            if (st.hasMoreTokens())
            {
               String mimetype = st.nextToken();

               if (mimetype.equalsIgnoreCase(Message.MIME_MULTIPART_RELATED))
               {
                  sendtype = SEND_TYPE_MIME;
                  mpartStream = new MultiPartRelatedInputStream(contentType, (InputStream)intialContents);

                  if (null == contentLocation)
                  {

                     // If the content location is not specified as
                     // of the main message use the SOAP content location.
                     contentLocation = mpartStream.getContentLocation();

                     if (contentLocation != null)
                     {
                        contentLocation = contentLocation.trim();

                        if (contentLocation.length() == 0)
                        {
                           contentLocation = null;
                        }
                     }
                  }

                  soapPart = new MessagePart(null, mpartStream, false);
               }
               else if (mimetype.equalsIgnoreCase(Message.MIME_APPLICATION_DIME))
               {
                  try
                  {
                     mpartStream = new MultiPartDimeInputStream((InputStream)intialContents);
                     soapPart = new MessagePart(null, mpartStream, false);
                  }
                  catch (Exception e)
                  {
                     throw AxisFault.makeFault(e);
                  }
                  sendtype = SEND_TYPE_DIME;
               }
            }
         }
      }
   }

   /**
    * Copies attachment references from the multipartStream to local list.
    * Done only once per object creation.
    *
    * @throws AxisFault
    */
   private void mergeinAttachments() throws AxisFault
   {

      if (mpartStream != null)
      {
         Collection atts = mpartStream.getAttachments();

         if (contentLocation == null)
            contentLocation = mpartStream.getContentLocation();

         mpartStream = null;

         setAttachmentParts(atts);
      }
   }

   /**
    * This method uses getAttacmentByReference() to look for attachment.
    * If attachment has been found, it will be removed from the list, and
    * returned to the user.
    *
    * @param reference The reference that referers to an attachment.
    * @return The part associated with the removed attachment, or null.
    * @throws org.jboss.axis.AxisFault
    */
   public Part removeAttachmentPart(String reference)
           throws org.jboss.axis.AxisFault
   {

      multipart = null;

      dimemultipart = null;

      mergeinAttachments();

      Part removedPart = getAttachmentByReference(reference);

      if (removedPart != null)
      {
         attachments.remove(removedPart.getContentId());
         attachments.remove(removedPart.getContentLocation());
         orderedAttachments.remove(removedPart);
      }

      return removedPart;
   }

   /**
    * Adds an existing attachment to this list.
    * Note: Passed part will be bound to this message.
    *
    * @param newPart new part to add
    * @return Part old attachment with the same Content-ID, or null.
    * @throws org.jboss.axis.AxisFault
    */
   public Part addAttachmentPart(Part newPart)
           throws org.jboss.axis.AxisFault
   {


      multipart = null;
      dimemultipart = null;

      mergeinAttachments();

      Part oldPart = (Part)attachments.put(newPart.getContentId(), newPart);

      if (oldPart != null)
      {
         orderedAttachments.remove(oldPart);
         attachments.remove(oldPart.getContentLocation());
      }

      orderedAttachments.add(newPart);

      if (newPart.getContentLocation() != null)
      {
         attachments.put(newPart.getContentLocation(), newPart);
      }

      return oldPart;
   }

   public Part createAttachmentPart(Object datahandler)
           throws org.jboss.axis.AxisFault
   {

      // Searching for the same attachements
      Integer key = new Integer(datahandler.hashCode());
      if (stackDataHandler.containsKey(key))
      {
         return (Part)stackDataHandler.get(key);
      }

      multipart = null;

      dimemultipart = null;

      mergeinAttachments();

      if (!(datahandler instanceof javax.activation.DataHandler))
      {
         throw new org.jboss.axis.AxisFault(Messages.getMessage("unsupportedAttach", datahandler.getClass().getName(),
                 javax.activation.DataHandler.class.getName()));
      }

      Part ret =
              new AttachmentPartImpl((javax.activation.DataHandler)datahandler);

      addAttachmentPart(ret);

      // Store the current DataHandler with its key
      stackDataHandler.put(key, ret);

      return ret;
   }

   /**
    * Add the collection of parts.
    *
    * @param parts
    * @throws org.jboss.axis.AxisFault
    */
   public void setAttachmentParts(java.util.Collection parts)
           throws org.jboss.axis.AxisFault
   {

      removeAllAttachments();

      if ((parts != null) && !parts.isEmpty())
      {
         for (java.util.Iterator i = parts.iterator(); i.hasNext();)
         {
            Object part = i.next();

            if (null != part)
            {
               if (part instanceof Part)
                  addAttachmentPart((Part)part);
               else
                  createAttachmentPart(part);
            }
         }
      }
   }

   /**
    * This method should look at a refernce and determine if it is a CID: or
    * url to look for attachment.
    * <br>
    * Note: if Content-Id or Content-Location headers have changed by outside
    * code, lookup will not return proper values. In order to change these
    * values attachment should be removed, then added again.
    *
    * @param reference The reference in the xml that referers to an attachment.
    * @return The part associated with the attachment.
    * @throws org.jboss.axis.AxisFault
    */
   public Part getAttachmentByReference(String reference)
           throws org.jboss.axis.AxisFault
   {

      if (null == reference)
      {
         return null;
      }

      reference = reference.trim();

      if (0 == reference.length())
      {
         return null;
      }

      mergeinAttachments();

      //This search will pickit up if its fully qualified location or if it's a content-id
      // that is not prefixed by the cid.

      Part ret = (Part)attachments.get(reference);
      if (null != ret) return ret;


      if (!reference.startsWith(Attachments.CIDprefix) && (null != contentLocation))
      {
         //Not a content-id check to see if its a relative location id.

         String fqreference = contentLocation;

         if (!fqreference.endsWith("/"))
         {
            fqreference += "/";
         }

         if (reference.startsWith("/"))
         {
            fqreference += reference.substring(1);
         }
         else
         {
            fqreference += reference;
         }

         // lets see if we can get it as Content-Location
         ret = (AttachmentPartImpl)attachments.get(fqreference);
      }

      if (null == ret && reference.startsWith(Attachments.CIDprefix))
      {
         //This is a content-id lets see if we have it.
         ret = (Part)attachments.get(reference.substring(4));
      }

      return ret;
   }

   /**
    * This method will return all attachments as a collection.
    *
    * @return A collection of attachments.
    * @throws org.jboss.axis.AxisFault
    */
   public java.util.Collection getAttachments()
           throws org.jboss.axis.AxisFault
   {

      mergeinAttachments();

      return new LinkedList(orderedAttachments);
   }

   /**
    * From the complex stream return the root part.
    * Today this is SOAP.
    *
    * @return the root <code>Part</code>
    */
   public Part getRootPart()
   {
      return soapPart;
   }

   public void setRootPart(Part newRoot)
   {

      try
      {
         this.soapPart = (MessagePart)newRoot;
         multipart = null;
         dimemultipart = null;
      }
      catch (ClassCastException e)
      {
         throw new ClassCastException(Messages.getMessage("onlySOAPParts"));
      }
   }

   /**
    * multipart , cached entries for the stream of attachment that are going to be sent.
    */
   javax.mail.internet.MimeMultipart multipart = null;
   DimeMultiPart dimemultipart = null;

   /**
    * Get the content length of the stream.
    *
    * @return the content length of the stream
    * @throws org.jboss.axis.AxisFault
    */
   public long getContentLength() throws org.jboss.axis.AxisFault
   {

      mergeinAttachments();

      int sendtype = this.sendtype == SEND_TYPE_NOTSET ? SEND_TYPE_DEFAULT : this.sendtype;

      try
      {
         if (sendtype == SEND_TYPE_MIME)
            return org.jboss.axis.attachments.MimeUtils.getContentLength(multipart != null ? multipart : (multipart = org.jboss.axis.attachments.MimeUtils.createMP(soapPart.getAsString(), orderedAttachments)));
         else if (sendtype == SEND_TYPE_DIME) return createDimeMessage().getTransmissionSize();
      }
      catch (Exception e)
      {
         throw AxisFault.makeFault(e);
      }
      return 0;
   }

   /**
    * Creates the DIME message
    *
    * @return a DIME part
    * @throws org.jboss.axis.AxisFault if the part could not be built
    */
   protected DimeMultiPart createDimeMessage() throws org.jboss.axis.AxisFault
   {
      int sendtype = this.sendtype == SEND_TYPE_NOTSET ? SEND_TYPE_DEFAULT : this.sendtype;
      if (sendtype == SEND_TYPE_DIME)
      {
         if (dimemultipart == null)
         {

            dimemultipart = new DimeMultiPart();
            dimemultipart.addBodyPart(new DimeBodyPart(soapPart.getAsBytes(), DimeTypeNameFormat.URI,
                    "http://schemas.xmlsoap.org/soap/envelope/",
                    "uuid:714C6C40-4531-442E-A498-3AC614200295"));

            for (java.util.Iterator i = orderedAttachments.iterator();
                 i.hasNext();)
            {
               AttachmentPartImpl part = (AttachmentPartImpl)i.next();
               DataHandler dh = AttachmentUtils.
                       getActivationDataHandler(part);
               dimemultipart.addBodyPart(new
                       DimeBodyPart(dh, part.getContentId()));
            }
         }
      }
      return dimemultipart;
   }

   /**
    * Write the content to the stream.
    *
    * @param os
    * @throws org.jboss.axis.AxisFault
    */
   public void writeContentToStream(java.io.OutputStream os)
           throws org.jboss.axis.AxisFault
   {
      int sendtype = this.sendtype == SEND_TYPE_NOTSET ?
              SEND_TYPE_DEFAULT : this.sendtype;
      try
      {

         mergeinAttachments();
         if (sendtype == SEND_TYPE_MIME)
         {
            org.jboss.axis.attachments.MimeUtils.writeToMultiPartStream(os,
                    (multipart != null) ? multipart : (multipart =
                    org.jboss.axis.attachments.MimeUtils.createMP(soapPart.getAsString(), orderedAttachments)));
         }
         else if (sendtype == SEND_TYPE_DIME) createDimeMessage().write(os);
      }
      catch (Exception e)
      {
         throw org.jboss.axis.AxisFault.makeFault(e);
      }
   }

   /**
    * Gets the content type for the whole stream.
    *
    * @return the content type for the whole stream
    * @throws org.jboss.axis.AxisFault
    */
   public String getContentType() throws org.jboss.axis.AxisFault
   {

      mergeinAttachments();

      int sendtype = this.sendtype == SEND_TYPE_NOTSET ? SEND_TYPE_DEFAULT :
              this.sendtype;
      if (sendtype == SEND_TYPE_MIME)
         return org.jboss.axis.attachments.MimeUtils.getContentType((multipart
                 != null)
                 ? multipart
                 : (multipart =
                 org.jboss.axis.attachments.MimeUtils.createMP(soapPart.getAsString(),
                         orderedAttachments)));
      else
         return org.jboss.axis.Message.MIME_APPLICATION_DIME;
   }

   /**
    * This is the number of attachments.
    *
    * @return the number of attachments
    */
   public int getAttachmentCount()
   {

      try
      {
         mergeinAttachments();

         // force a serialization of the message so that
         // any attachments will be added
         soapPart.getAsString();

         return orderedAttachments.size();
      }
      catch (AxisFault e)
      {
         log.warn(Messages.getMessage("exception00"), e);
      }

      return 0;
   }

   /**
    * Determine if an object is to be treated as an attchment.
    *
    * @param value the value that is to be determined if
    *              its an attachment.
    * @return True if value should be treated as an attchment.
    */
   public boolean isAttachment(Object value)
   {
      return AttachmentUtils.isAttachment(value);
   }

   /**
    * Removes all <CODE>AttachmentPart</CODE> objects that have
    * been added to this <CODE>SOAPMessage</CODE> object.
    * <p/>
    * <P>This method does not touch the SOAP part.</P>
    */
   public void removeAllAttachments()
   {
      try
      {
         multipart = null;
         dimemultipart = null;
         mergeinAttachments();
         attachments.clear();
         orderedAttachments.clear();
         stackDataHandler.clear();
      }
      catch (AxisFault af)
      {
         log.warn(Messages.getMessage("exception00"), af);
      }
   }

   /**
    * Retrieves all the <CODE>AttachmentPart</CODE> objects
    * that have header entries that match the specified headers.
    * Note that a returned attachment could have headers in
    * addition to those specified.
    *
    * @param headers a <CODE>MimeHeaders</CODE>
    *                object containing the MIME headers for which to
    *                search
    * @return an iterator over all attachments that have a header
    *         that matches one of the given headers
    */
   public java.util.Iterator getAttachments(javax.xml.soap.MimeHeaders headers)
   {
      java.util.Vector vecParts = new java.util.Vector();
      java.util.Iterator iterator = GetAttachmentsIterator();
      while (iterator.hasNext())
      {
         Part part = (Part)iterator.next();
         if (part instanceof AttachmentPartImpl)
         {
            if (((AttachmentPartImpl)part).matches(headers))
            {
               vecParts.add(part);
            }
         }
      }
      return vecParts.iterator();
   }

   /**
    * get an iterator over all attachments. This
    *
    * @return iterator of Part Objects; some of which may be
    *         AttachmentPart instances
    * @see org.jboss.axis.Part
    * @see AttachmentPartImpl
    */
   private java.util.Iterator GetAttachmentsIterator()
   {
      java.util.Iterator iterator = attachments.values().iterator();
      return iterator;
   }

   /**
    * Create a new attachment Part in this Message.
    * Will actually, and always, return an AttachmentPart.
    *
    * @return a new attachment Part
    * @throws org.jboss.axis.AxisFault
    */
   public Part createAttachmentPart() throws org.jboss.axis.AxisFault
   {
      return new AttachmentPartImpl();
   }

   public void setSendType(int sendtype)
   {
      if (sendtype < 1)
         throw new IllegalArgumentException("");
      if (sendtype > SEND_TYPE_MAX)
         throw new IllegalArgumentException("");
      this.sendtype = sendtype;
   }

   public int getSendType()
   {
      return sendtype;
   }

   /**
    * dispose of the attachments and their files; do not use the object
    * after making this call.
    */

   public void dispose()
   {
      java.util.Iterator iterator = GetAttachmentsIterator();
      while (iterator.hasNext())
      {
         Part part = (Part)iterator.next();
         if (part instanceof AttachmentPartImpl)
         {
            AttachmentPartImpl apart = (AttachmentPartImpl)part;
            apart.dispose();
         }
      }

   }

   // consider type-safe e-num here?
   /**
    * Determine how an object typically sent as attachments are to
    * be represented. Currently, MIME DIME and NONE are reccognised.
    *
    * @param value a String representing a sending type, treated in a
    *              case-insensetive manner
    * @return an <code>int</code> send type code
    */
   public static int getSendType(String value)
   {
      if (value.equalsIgnoreCase("MIME")) return SEND_TYPE_MIME;
      if (value.equalsIgnoreCase("DIME")) return SEND_TYPE_DIME;
      if (value.equalsIgnoreCase("NONE")) return SEND_TYPE_NONE;
      return SEND_TYPE_NOTSET;
   }

   /**
    * For a given sendType value, return a string representation.
    *
    * @param value a type code integer
    * @return a <code>String</code> representation of <code>value</code>
    */
   public static String getSendTypeString(int value)
   {
      if (value == SEND_TYPE_MIME)
      {
         return "MIME";
      }
      if (value == SEND_TYPE_DIME)
      {
         return "DIME";
      }
      if (value == SEND_TYPE_NONE)
      {
         return "NONE";
      }
      return null;
   }
}