/*
 * MultiPartForm.java
 *
 * Copyright(c) 2000 by inetbasics,
 * 18636 N. 15th Street, Phoenix, Arizona, 85024, U.S.A.
 * All rights reserved.
 *
 * This software shall only be used in accordance with the
 * terms of the attached license agreement.
 *
 */

package com.inetbasics.iap.core.jsp;

import javax.servlet.*;
import java.io.*;
import java.util.*;

/** Utility class designed to deal with Multi-part encoded form
 * requests.  The typical use for these forms is to upload binary
 * information to the server.  The user must read all of the files
 * before access the request parameters.  The following is a code
 * exmaple of how to use this class:
 * 
 * <code>
 * <pre>
 * MultiPartForm newForm = new MultiPartForm(pageContext.getRequest());
 * 
 * String fileName = newForm.getNextFileName();
 * 
 * if (fileName != null)
 * {
 *    // read in the file...
 *    fileName += ".server";
 *    FileOutputStream output = new FileOutputStream(fileName);
 *    InputStream in = newForm.getNextFile();
 *    System.out.println("Writing file:  " +fileName);
 * 
 *    int count = 0;
 *    int read = in.read();
 * 
 *    while (read != -1)
 *    {
 *       System.out.print("-"+count);
 *       output.write(read);
 *       read = in.read();
 *       count++;
 *    }
 *    System.out.println("Read returned -1");
 *    output.close();
 *    fileName = newForm.getNextFileName();
 * }
 * // now it is safe to get the parameters
 * String value = newForm.getParameter("someParam");
 * </pre>
 * </code>
 * 
 * The preious example will read the input from the form and save
 * each file with a ".server" extension.
 */
public class MultiPartForm 
{
   /**     */
   private Hashtable requestParameters = new Hashtable();
   
   /**     */
   private ParamHeader lastHeader = null;
   /**     */
   private InputStream currentStream = null;
      
   /**     */
   private String delimiter = null;
   
   /**     */
   private byte[] buffer = new byte[1024]; // read buffer   
   /**     */
   private StringBuffer lineBuffer = new StringBuffer(1024);
   
   /** this constructor takes a pristine <code>javax.servlet.ServletRequest</code>
    * object as a parameter.  This request object must be pristine
    * (ie, no previous calls to <code>getParameter</code>).
    * 
    * @param request The request object from the client.
    * @exception IOException If the request input stream cannot
    * be read.
    */
   public MultiPartForm(ServletRequest request) throws IOException
   {
      fillFromRequest(request);
   }
   
   /** This is similar to the constructor with the same arguments
    * 
    * @exception IOException 
    * @param request 
    * @see #MultiPartForm(ServletRequest)
    */
   public void fillFromRequest(ServletRequest request) throws IOException
   {
      requestParameters.clear();
      
      boolean finished = false;
      
      currentStream = request.getInputStream();

      delimiter = readLine(currentStream); // first line is the delimiter....
      readParams();
   }

   /** Internal method for reading all params up to a binary file.
    * 
    * @exception IOException 
    */
   protected void readParams() throws IOException
   {      
      lastHeader = null;
      
      while (true)
      {
         ParamHeader header = readHeader();
         if (header == null)
         {
            // we are finished... no more headers
            return;
         }
         
         switch (header.type)
         {
         case ParamHeader.BINARY:
            lastHeader = header;
            requestParameters.put(header.name, header.fileName);
            return;
         case ParamHeader.TEXT:
            requestParameters.put(header.name, readString());
            break;
         default:
            throw new IOException("Corrupt stream.");
         }         
      }
      
   }
   
   /** Used by the user to get a parameter from the request.
    *  This method should not be called until all the files have been
    * completely read.
    * 
    * @param parameterName The name of the parameter
    * @return The string value of the parameter
    */
   public String getParameter(String parameterName)
   {
      Object returnObject = requestParameters.get(parameterName);
      
      if (returnObject instanceof String)
      {
         return (String)returnObject;
      }
      
      return null;      
   }

   /** This call gets the next file name in the request parameter.
    *  It can be used to iterate through the files being uploaded.
    *  The file must be completely read before this method is called again.
    * 
    * @return The name of the file being uploaded.
    */
   public String getNextFileName()
   {
      if (lastHeader != null)
      {
         return lastHeader.fileName;
      }
      return null;
   }
      
   /** This call gets the tag name of the next file being read.
    * 
    * @return The tag name of the next file being read.
    */
   public String getNextFileTagName()
   {
      if (lastHeader != null)
      {
         return lastHeader.name;
      }
      return null;
   }
   
   /** This call returns an input stream for the next file that
    * can be read.  The user must fully read this input stream.
    * 
    * @exception IOException The <code>InputStream</code> cannot
    * be created
    * @return The input stream containing the byte of the the
    * binary file    
    *     being uploaded.
    */
   public InputStream getNextFile() throws IOException
   {
      if (lastHeader == null)
      {
         return null;
      }
      
      return new MultiPartInputStream(currentStream, lastHeader.delimiter);
   }
         
   /** 
    * Internal method that reads the information about the next
    * available parameter.
    *  
    * @exception IOException 
    */
   protected ParamHeader readHeader() throws IOException
   {
      String line1 = readLine(currentStream);
      String line2 = readLine(currentStream);
      
      if (line1 == null || line2 == null)
      {
         return null;
      }
      
      int namePos = line1.indexOf("name=\"");
      if (namePos == -1)
      {
         throw new IOException("Corrupt stream.");
      }
      
      int nameEndPos = line1.indexOf("\"", namePos + 6);
      if (nameEndPos == -1)
      {
         throw new IOException("Corrupt stream.");
      }
      
      String fileName = null;
      int fileNamePos = line1.indexOf("filename=\"", nameEndPos);
      
      if (fileNamePos != -1)
      {
         int fileNameEndPos = line1.indexOf("\"", fileNamePos + 10);
         if (fileNameEndPos == -1)
         {
            throw new IOException("Corrupt stream.");
         }
         fileName = line1.substring(fileNamePos + 10, fileNameEndPos);
      }
            
      String name = line1.substring(namePos + 6, nameEndPos);
      
      return new ParamHeader((line2.length() == 0)?ParamHeader.TEXT:ParamHeader.BINARY, name, delimiter, fileName);      
   }
   
   /** Internal method for reading the next parameter that is
    * a string off the stream.
    * 
    * @exception IOException 
    * @return The next parameter that is a string
    */
   protected String readString() throws IOException
   {
      String returnString = readLine(currentStream);
      
      String nextLine = readLine(currentStream);
      
      while (!nextLine.startsWith(delimiter))
      {
         returnString += "\n" + nextLine;
         nextLine = readLine(currentStream);
      }
      
      return returnString;
   }
   
   /** Internal method for reading the next line off the stream.
    * 
    * @param is The stream to be read.
    * @exception IOException The stream cannot be read.
    * @return The next line off the stream
    */
   protected String readLine(InputStream is) throws IOException
   {
      StringBuffer lineBuffer = new StringBuffer(100);
      
      int read = is.read();
      
      while(read != -1)
      {
         if (read == 0x0d)
         {
            is.read(); // read the other one...
            return lineBuffer.toString();
         }
         
         lineBuffer.append((char)read);
         read = is.read();
      }
      
      return null;            
   }
   
   
   /** Internal class used for holding iformation about the next 
    * available parameter.
    */
   protected class ParamHeader
   {
      /**        */
      private static final int BINARY = 0;
      /**        */
      private static final int TEXT = 1;
      
      /**        */
      public int type = 0;
      /**        */
      public String name = "";
      /**        */
      public String delimiter = "";
      /**        */
      public String fileName = null;
      
      /**        * 
       * @param type 
       * @param name 
       * @param delimiter 
       * @param fileName 
       */
      public ParamHeader(int type, String name, String delimiter, String fileName)
      {
         this.type = type;
         this.name = name;
         this.delimiter = delimiter;
         this.fileName = fileName;
      }
   } 
   
   private class MultiPartInputStream extends InputStream
   {
      /**        */
      private byte[] delimiter = null;
      /**        */
      private byte[] readAheadBuffer = null;
      /**        */
      private int currentBytePos = 0;
      /**        */
      private InputStream reader = null;
      /**        */
      private boolean atEnd = false;

      /**        * 
       * @param reader 
       * @param delimiter 
       * @exception IOException 
       */
      public MultiPartInputStream(InputStream reader, String delimiter) throws IOException
      {
         this.delimiter = delimiter.getBytes();
         byte [] oldDelimiter = delimiter.getBytes();
         
         this.delimiter = new byte[oldDelimiter.length+2];
         this.delimiter[0] = (byte)0x0d;
         this.delimiter[1] = (byte)0x0a;
         System.arraycopy(oldDelimiter, 0, this.delimiter, 2, oldDelimiter.length);
         
         this.readAheadBuffer = new byte[this.delimiter.length];
         
         this.reader = reader;
         
         // read the delimiters
         reader.read();
         reader.read();
         
         fillReadAhead();
      }
      
      /**        * 
       * @exception IOException 
       */
      protected void fillReadAhead() throws IOException
      {
         int read = 0;
         while (read < readAheadBuffer.length)
         {
            int character = reader.read();
            if (character == -1)
            {
               throw new IOException("Corrupt stream.");
            }
            
            readAheadBuffer[read] = (byte)character;
            read++;
         }
         atEnd = equals(readAheadBuffer, currentBytePos, delimiter);
                  
      }
      
      /**        * 
       * @exception IOException 
       */
      public int available() throws IOException
      {
         return 1;
      }
      
      /**        * 
       * @exception IOException 
       */
      public int read() throws IOException
      {
         if (atEnd)
         {
            reader.read();
            reader.read();// burn off 0x0d and 0x0a
            readParams();
            return -1;
         }
         
         byte returnedByte = readAheadBuffer[currentBytePos];
         
         int i=reader.read();
         if (i == -1)
         {
            throw new IOException("Corrupt stream.");
         }
         
         readAheadBuffer[currentBytePos] = (byte)i;
         currentBytePos++;
         if (currentBytePos >= readAheadBuffer.length)
         {
            currentBytePos = 0;
         }
         
         atEnd = equals(readAheadBuffer, currentBytePos, delimiter);
         
         return returnedByte & 0xff;
      }
            
      /**        * 
       * @param src 
       * @param srcStart 
       * @param target 
       */
      protected boolean equals(byte [] src, int srcStart, byte[]target)
      {
         int srcPos = srcStart;
         
         for (int i=0;i<target.length;i++)
         {
            if (src[srcPos] != target[i])
            {
               return false;
            }
            srcPos++;
            if (srcPos >= src.length)
            {
               srcPos = 0;
            }            
         }

         return true;  
      }

   }

}


/**
 * MultiPartForm.java
 *
 * $Revision: 1.1.1.1 $, $Date: 2000/11/08 00:19:00 $
 * $Author: jdellis $
 *
 * $Log: MultiPartForm.java,v $
 * Revision 1.1.1.1  2000/11/08 00:19:00  jdellis
 * Initial Check-In
 *
 *
 */

