/*
 * The contents of this file are subject to the Mozilla Public License Version 1.1
 * (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.mozilla.org/MPL/>.
 * 
 * Software distributed under the License is distributed on an "AS IS" basis, WITHOUT
 * WARRANTY OF ANY KIND, either express or implied. See the License for the specific
 * language governing rights and limitations under the License.
 * 
 * The Original Code is the Venice Web Communities System.
 * 
 * The Initial Developer of the Original Code is Eric J. Bowersox <erbo@silcom.com>,
 * for Silverwrist Design Studios.  Portions created by Eric J. Bowersox are
 * Copyright (C) 2001-2004 Eric J. Bowersox/Silverwrist Design Studios.  All Rights Reserved.
 * 
 * Contributor(s): 
 */
package com.silverwrist.util;

import java.io.*;
import java.sql.Blob;
import java.sql.SQLException;
import java.util.*;
import javax.activation.DataSource;
import javax.mail.*;
import javax.mail.internet.*;
import javax.servlet.*;
import org.apache.log4j.*;

// This class was built in a process I call "Java Junkyard Wars," in which I put together
// a bunch of APIs in ways that their designers would never have anticipated.  It's
// absolutely bodge-tastic!

/**
 * A class which parses servlet request data of the MIME type <CODE>multipart/form-data</CODE>.  This is
 * necessary because the standard Java Servlets API only handles the standard form data type of
 * <CODE>application/x-www-form-urlencoded</CODE>, yet the <CODE>multipart/form-data</CODE> form
 * data type is the only way to upload files using a standard Web browser (at least one that implements
 * RFC 1867, as most modern browsers do).<P>
 * When using this code, do <EM>not</EM> use the standard <CODE>ServletRequest.getParameter</CODE> API!
 * Instead, if the datatype returned by <CODE>ServletRequest.getContentType</CODE> is "multipart/form-data",
 * pass the request to the constructor of this class, and then use the methods of this class to extract both
 * file and non-file parameters.
 *
 * @author Eric J. Bowersox &lt;erbo@silcom.com&gt;
 * @version X
 * @see ServletMultipartException
 * @see javax.servlet.ServletRequest
 */
public final class ServletMultipartHandler
{
  /*--------------------------------------------------------------------------------
   * Internal wrapper around the ServletRequest that implements DataSource
   *--------------------------------------------------------------------------------
   */

  static class ServletDataSource implements DataSource
  {
    private ServletRequest request;
    private InputStream istm = null;

    public ServletDataSource(ServletRequest request)
    {
      this.request = request;

    } // end constructor

    public InputStream getInputStream() throws IOException
    {
      if (istm==null)
	istm = request.getInputStream();
      return istm;

    } // end getInputStream

    public OutputStream getOutputStream() throws IOException
    {
      throw new IOException("tried to get OutputStream on servlet input?!?!?");

    } // end getOutputStream

    public String getContentType()
    {
      return request.getContentType();

    } // end getContentType

    public String getName()
    {
      return "servlet";

    } // end getName

  } // end class ServletDataSource

  /*--------------------------------------------------------------------------------
   * Internal class representing a data value
   *--------------------------------------------------------------------------------
   */

  static class MultipartDataValue implements Blob
  {
    private byte[] actual_data;  // the actual data we contain

    public MultipartDataValue(MimeBodyPart part) throws MessagingException, IOException
    {
      if (logger.isDebugEnabled())
	logger.debug("creating new MultipartDataValue");

      // load actual data
      actual_data = IOUtil.load(part.getInputStream());

      if (logger.isDebugEnabled())
	logger.debug("finished copying, " + actual_data.length + " bytes transferred");

    } // end constructor

    public long length()
    {
      return actual_data.length;

    } // end length

    public byte[] getBytes(long pos, int length)
    {
      byte[] rc = new byte[length];
      System.arraycopy(actual_data,(int)pos,rc,0,length);
      return rc;

    } // end getBytes

    public InputStream getBinaryStream()
    {
      return new ByteArrayInputStream(actual_data);

    } // end getBinaryStream

    public long position(byte[] pattern, long start) throws SQLException
    {
      logger.warn("position() function is not implemented for MultipartDataValue");
      throw new SQLException("function not implemented");

    } // end position

    public long position(Blob pattern, long start) throws SQLException
    {
      return position(pattern.getBytes(0,(int)(pattern.length())),start);

    } // end position

    public int setBytes(long pos, byte[] bytes) throws SQLException
    {
      return setBytes(pos,bytes,0,bytes.length);

    } // end setBytes

    public int setBytes(long pos, byte[] bytes, int offset, int len) throws SQLException
    {
      logger.warn("setBytes() function is not implemented for MultipartDataValue");
      throw new SQLException("function not implemented");

    } // end setBytes

    public OutputStream setBinaryStream(long pos) throws SQLException
    {
      logger.warn("setBinaryStream() function is not implemented for MultipartDataValue");
      throw new SQLException("function not implemented");

    } // end setBinaryStream

    public void truncate(long len) throws SQLException
    {
      logger.warn("truncate() function is not implemented for MultipartDataValue");
      throw new SQLException("function not implemented");

    } // end truncate

  } // end class MultipartDataValue

  /*--------------------------------------------------------------------------------
   * Internal class representing a request parameter
   *--------------------------------------------------------------------------------
   */

  final class MultipartParameter
  {
    private MimeBodyPart part;   // the actual body part data
    private String name;         // the parameter name
    private String filename;     // the filename
    private MultipartDataValue cached_value = null;

    public MultipartParameter(MimeBodyPart part) throws MessagingException
    {
      this.part = part;   // save part reference

      // Parse the Content-Disposition header.
      String[] cdstr = part.getHeader("Content-Disposition");
      if (logger.isDebugEnabled())
	logger.debug("Content-Disposition is " + cdstr[0]);
      ContentDisposition cdisp = new ContentDisposition(cdstr[0]);
      name = cdisp.getParameter("name");
      filename = cdisp.getParameter("filename");

      if (filename!=null)
      { // EJB 4/4/2001 - Windows boxes pass the filename complete with the backslashed pathname on the
	// front, and, for some reason, ContentDisposition strips out the backslashes while leaving the
	// pathname components in place (including the drive letter!).  So we have to go to manual here
	// to extract the filename ourselves.
	int pos = cdstr[0].indexOf("filename");
	pos += 8;
	while ((cdstr[0].charAt(pos)==' ') || (cdstr[0].charAt(pos)=='\t'))
	  pos++;
	if (cdstr[0].charAt(pos)!='=')
	  throw new RuntimeException("must have = sign after filename");
	pos++;
	while ((cdstr[0].charAt(pos)==' ') || (cdstr[0].charAt(pos)=='\t'))
	  pos++;
	if ((cdstr[0].charAt(pos)=='\'') || (cdstr[0].charAt(pos)=='"'))
	{ // filename enclosed in quotes...
	  char match = cdstr[0].charAt(pos++);
	  filename = cdstr[0].substring(pos);
	  pos = filename.lastIndexOf(match);
	  if (pos<0)
	    throw new RuntimeException("must have closing quote");
	  filename = filename.substring(0,pos);

	} // end if
	else
	{ // no quotes, just take the rest of the line
	  filename = cdstr[0].substring(pos);
	  pos = filename.lastIndexOf(';');
	  if (pos>=0)
	    filename = filename.substring(0,pos);

	} // end else

	if (logger.isDebugEnabled())
	  logger.debug("Raw filename: " + filename);

	// Strip off everything but the base filename, if the browser happened to pass that.
	int sep = filename.lastIndexOf('\\');
	if (sep>=0)
	  filename = filename.substring(sep+1);
	sep = filename.lastIndexOf('/');
	if (sep>=0)
	  filename = filename.substring(sep+1);

      } // end if

      if (logger.isDebugEnabled())
      { // tell us what kind of parameter we have
	if (filename!=null)
	  logger.debug("new file parameter \"" + name + "\" defined (filename = " + filename + ")");
	else
	  logger.debug("new text parameter \"" + name + "\" defined");

      } // end if

    } // end constructor

    public final String getName()
    {
      return name;

    } // end getName

    public final boolean isFile()
    {
      return (filename!=null);

    } // end isFile

    public final String getValue()
    {
      if (filename!=null)
	return filename;  // "value" for file parts is the filename

      try
      { // Retrieve the part's actual content and convert it to a String.  (Since non-file
	// fields are of type text/plain, the Object "val" should actually be a String, in
	// which case the toString() call is actually a no-op.  But this is safe.)
	Object val = part.getContent();
	return val.toString();

      } // end try
      catch (Exception e)
      { // turn any exception returns here into null returns
	logger.warn("parameter getValue() method threw a " + e.getClass().getName(),e);
	return null;

      } // end catch

    } // end getValue

    public final String getContentType()
    {
      try
      { // pass through to the interior part
	return part.getContentType();

      } // end try
      catch (Exception e)
      { // just dump a null on error
	logger.warn("parameter getContentType() method threw a " + e.getClass().getName(),e);
	return null;

      } // end catch

    } // end getContentType

    public final int getSize()
    {
      try
      { // pass through to the interior part
	return part.getSize();

      } // end try
      catch (Exception e)
      { // just dump a -1 on error
	logger.warn("parameter getSize() method threw a " + e.getClass().getName(),e);
	return -1;

      } // end catch

    } // end getSize

    public final MultipartDataValue getContent() throws ServletMultipartException
    {
      if (filename==null)
	return null;  // not a file parameter

      if (cached_value==null)
      { // we don't have the value cached yet
	if (logger.isDebugEnabled())
	  logger.debug("getting MultipartDataValue for parameter \"" + name + "\"");

	try
	{ // extract the value
	  cached_value = new MultipartDataValue(part);

	} // end try
	catch (MessagingException me)
	{ // translate exception here
	  logger.error("MIME parse error getting data content: " + me.getMessage(),me);
	  throw new ServletMultipartException("Error getting data content: " + me.getMessage(),me);

	} // end catch
	catch (IOException ioe)
	{ // translate exception here
	  logger.error("IO error getting data: " + ioe.getMessage(),ioe);
	  throw new ServletMultipartException("Error getting data content: " + ioe.getMessage(),ioe);

	} // end catch

      } // end if

      return cached_value;

    } // end getContent

  } // end class MultipartParameter

  /*--------------------------------------------------------------------------------
   * Static data members
   *--------------------------------------------------------------------------------
   */

  private static Category logger = Category.getInstance(ServletMultipartHandler.class);

  /*--------------------------------------------------------------------------------
   * Attributes
   *--------------------------------------------------------------------------------
   */

  private MimeMultipart multipart;     // holds all the multipart data
  private HashMap param_byname;        // parameters by name
  private ArrayList param_order;       // parameters in order

  /*--------------------------------------------------------------------------------
   * Constructor
   *--------------------------------------------------------------------------------
   */

  /**
   * Constructs a new <CODE>ServletMultipartHandler</CODE>.
   *
   * @param request The <CODE>ServletRequest</CODE> which contains the request data of type
   *                <CODE>multipart/form-data</CODE>.
   * @exception com.silverwrist.util.ServletMultipartException The request could not be correctly parsed.
   */
  public ServletMultipartHandler(ServletRequest request) throws ServletMultipartException
  {
    try
    { // build the MimeMultipart based on the ServletDataSource
      logger.debug("about to create MimeMultipart");
      multipart = new MimeMultipart(new ServletDataSource(request));
      logger.debug("about to get request count");
      int count = multipart.getCount();
      if (logger.isDebugEnabled())
	logger.debug("retrieved " + count + " parameter(s) from request");
      
      // allocate the multipart parameters and slot them into our arrays
      param_byname = new HashMap();
      param_order = new ArrayList();
      for (int i=0; i<count; i++)
      { // create the MultipartParameter structures and stash them for later use
	MultipartParameter parm = new MultipartParameter((MimeBodyPart)(multipart.getBodyPart(i)));
	if (param_byname.get(parm.getName())==null)
	  param_byname.put(parm.getName(),parm);
	param_order.add(parm);

      } // end count

      if (logger.isDebugEnabled())
	logger.debug("ServletMultipartHandler created, " + param_order.size() + " params defined");

    } // end try
    catch (MessagingException me)
    { // translate a MessagingException into the nicer ServletMultipartException
      logger.error("multipart parsing failed: " + me.getMessage(),me);
      throw new ServletMultipartException("Error parsing request data: " + me.getMessage(),me);

    } // end catch

  } // end constructor

  /*--------------------------------------------------------------------------------
   * External static operations
   *--------------------------------------------------------------------------------
   */

  /**
   * Returns <CODE>true</CODE> if the given <CODE>ServletRequest</CODE> can be handled by
   * the <CODE>ServletMultipartHandler</CODE>, <CODE>false</CODE> if not.
   *
   * @param request The <CODE>ServletRequest</CODE> to be checked.
   * @return <CODE>true</CODE> if the given <CODE>ServletRequest</CODE> can be handled by
   *         the <CODE>ServletMultipartHandler</CODE>, <CODE>false</CODE> if not.
   */
  public final static boolean canHandle(ServletRequest request)
  {
    String ctype = request.getContentType();
    return (ctype.startsWith("multipart/form-data"));

  } // end canHandle

  /*--------------------------------------------------------------------------------
   * External operations
   *--------------------------------------------------------------------------------
   */

  /**
   * Returns an <CODE>Enumeration</CODE> of all parameter names in this request.
   *
   * @return See above.
   * @see #getParamNames()
   * @see java.util.Enumeration
   */
  public final Enumeration getNames()
  {
    ArrayList tmp_v = new ArrayList();
    Iterator it = param_order.iterator();
    while (it.hasNext())
    { // add each name to the temporary vector
      MultipartParameter parm = (MultipartParameter)(it.next());
      tmp_v.add(parm.getName());

    } // end while

    return Collections.enumeration(tmp_v); // and enumerate it

  } // end getNames

  /**
   * Returns an <CODE>Iterator</CODE> over all parameter names in this request.  This <CODE>Iterator</CODE>
   * must be considered read-only.
   *
   * @return See above.
   * @see #getNames()
   * @see java.util.Iterator
   */
  public final Iterator getParamNames()
  {
    ArrayList tmp_v = new ArrayList();
    Iterator it = param_order.iterator();
    while (it.hasNext())
    { // add each name to the temporary vector
      MultipartParameter parm = (MultipartParameter)(it.next());
      tmp_v.add(parm.getName());

    } // end while

    return Collections.unmodifiableList(tmp_v).iterator();
    
  } // end getParamNames

  public final boolean hasParameter(String name)
  {
    return param_byname.containsKey(name);

  } // end hasParameter

  /**
   * Returns whether or not the parameter with the specified name is a file parameter.
   *
   * @param name Name of the parameter to test.
   * @return <CODE>true</CODE> if the specified parameter is a file parameter, <CODE>false</CODE> of not.
   *         If no parameter by this name exists in the request, the method returns <CODE>false</CODE>.
   */
  public final boolean isFileParam(String name)
  {
    MultipartParameter parm = (MultipartParameter)(param_byname.get(name));
    if (parm==null)
    { // no such parameter!
      logger.warn("parameter \"" + name + "\" not found");
      return false;

    } // end if
    else
      return parm.isFile();

  } // end isFileParam

  /**
   * Returns the value of the parameter with the specified name.  In the case of file parameters, returns
   * the name of the file that was uploaded.<P>
   * If the parameter may have multiple values, you should use {@link getParamValues(java.lang.String)} to
   * return all possible values for this parameter.
   *
   * @param name Name of the parameter to return.
   * @return Value of the parameter, which is a filename if this parameter is a file.  If no parameter by
   *         this name exists in the request, the method returns <CODE>null</CODE>.
   */
  public final String getValue(String name)
  {
    MultipartParameter parm = (MultipartParameter)(param_byname.get(name));
    if (parm==null)
    { // no such parameter!
      logger.warn("parameter \"" + name + "\" not found");
      return null;

    } // end if
    else
      return parm.getValue();

  } // end getValue

  /**
   * Returns an array of <CODE>String</CODE> objects containing all of the values the given request
   * parameter has, or <CODE>null</CODE> if the parameter does not exist.  In the case of file parameters,
   * the value is ne name of the file that was uploaded.  If the parameter has a single value, the returned
   * array has a length of 1.
   *
   * @param name Name of the parameter to return.
   * @return Array containing all of the values associated with this parameter name.  If no values
   *         are associated with the parameter name, returns <CODE>null</CODE>.
   */
  public final String[] getParamValues(String name)
  {
    if (!(param_byname.containsKey(name)))
      return null;  // no parameters with this name

    // Collect all the parameter values with this name.
    ArrayList rc = new ArrayList(param_order.size());
    Iterator it = param_order.iterator();
    while (it.hasNext())
    { // find names that match, copy values
      MultipartParameter parm = (MultipartParameter)(it.next());
      if (parm.getName().equals(name))
	rc.add(parm.getValue());

    } // end while

    // return the results array
    return (String[])(rc.toArray(new String[0]));

  } // end getParamValues

  /**
   * Returns the MIME type of the file parameter with the specified name.  Ordinary text parameters return
   * the MIME type <CODE>text/plain</CODE>.
   *
   * @param name Name of the parameter to return.
   * @return Content type of the parameter.  If no parameter by this name exists in the request, the method
   *         returns <CODE>null</CODE>.
   */
  public final String getContentType(String name)
  {
    MultipartParameter parm = (MultipartParameter)(param_byname.get(name));
    if (parm==null)
    { // no such parameter!
      logger.warn("parameter \"" + name + "\" not found");
      return null;

    } // end if
    else
      return parm.getContentType();

  } // end getContentType

  /**
   * Returns the size in bytes of the file parameter with the specified name.  For ordinary text parameters,
   * the return value is meaningless.
   *
   * @param name Name of the parameter to return.
   * @return Size in bytes of the parameter.  If no parameter by this name exists in the request, the method
   *         returns -1.
   */
  public final int getContentSize(String name)
  {
    MultipartParameter parm = (MultipartParameter)(param_byname.get(name));
    if (parm==null)
    { // no such parameter!
      logger.warn("parameter \"" + name + "\" not found");
      return -1;

    } // end if
    else
      return parm.getSize();

  } // end getContentSize

  /**
   * Returns the contents of the specified file parameter expressed as a <CODE>Blob</CODE>.
   *
   * @param name Name of the parameter to return.
   * @return <CODE>Blob</CODE> containing the file parameter data.  If this parameter does not
   *         exist in the request or is not a file parameter, the method returns <CODE>null</CODE>.
   * @exception com.silverwrist.util.ServletMultipartException Unable to prepare the file parameter data.
   * @see #getFileContentStream()
   * @see java.sql.Blob
   */
  public final Blob getFileContentBlob(String name) throws ServletMultipartException
  {
    MultipartParameter parm = (MultipartParameter)(param_byname.get(name));
    if (parm==null)
    { // no such parameter!
      logger.warn("parameter \"" + name + "\" not found");
      return null;

    } // end if
    else
      return parm.getContent();

  } // end getFileContentBlob

  /**
   * Returns the contents of the specified file parameter expressed as an <CODE>InputStream</CODE>.
   *
   * @param name Name of the parameter to return.
   * @return <CODE>InputStream</CODE> containing the file parameter data.  If this parameter does not
   *         exist in the request or is not a file parameter, the method returns <CODE>null</CODE>.
   * @exception com.silverwrist.util.ServletMultipartException Unable to prepare the file parameter data.
   * @see #getFileContentBlob()
   * @see java.io.InputStream
   */
  public final InputStream getFileContentStream(String name) throws ServletMultipartException
  {
    MultipartParameter parm = (MultipartParameter)(param_byname.get(name));
    if (parm==null)
    { // no such parameter!
      logger.warn("parameter \"" + name + "\" not found");
      return null;

    } // end if

    MultipartDataValue val = parm.getContent();
    return ((val==null) ? null : val.getBinaryStream());

  } // end getFileContentStream

} // end class ServletMultipartHandler
