/***********************************************************************\
//                                                                      *
//      MaybeUploadServlet.java                                        	*
//                                                                      *
//      Purpose: An HTTP servlet supporting file upload			*
//                                                                      *
//      Author:    Simon Brooke                                         *
//      Created:   27th December 2000                                   *
//	$Revision: 1.12 $; $Date: 2001/04/24 15:55:58 $			*
//                                                                      *
//***********************************************************************/

package uk.co.weft.maybeupload;

import java.io.*;
import java.util.ResourceBundle;
import javax.servlet.*;
import javax.servlet.http.*;

/** A superclass for Servlets which may need to handle file
 *  upload. Sun's Servlet class does not transparently handle
 *  multipart/form-data requests and, indeed, makes it extremely
 *  difficult for application-layer classes to handle them. This class
 *  is written as a wrapper round Sun's Servlet class which makes
 *  multipart/form-data handling transparent. </p>
 *
 *  <p>This class (and consequently all servlets which are subclasses
 *  of this class) know about the following configuration parameters:
 *  <dl>
 *    <dt>allow_overwrite</dt>
 *    <dd>Boolean: whether or not to allow uploaded files to overwrite 
 *      previously uploaded files with the same name. Default is we don't.</dd>
 *    <dt>max_upload</dt>
 *    <dd>Integer: maximum size in bytes of file to upload. Optional. 
 *      Default is 524288.</dd>
 *    <dt>save_uploaded</dt>
 *    <dd>Boolean:  whether or not to save uploaded files directly to 
 *      disk. Optional. Default is we do. If we don't, the uploaded 
 *      file will be returned as a java.io.ByteArrayInputStream by a 
 *      call to MaybeUploadRequestWrapper.get()</dd>
 *    <dt>silently_rename</dt>
 *    <dd>Boolean: whether or not to rename files whose names collide with
 *      existing files in the upload directory. Default is we do. Note that
 *      if both <code>allow_overwrite</code> and <code>silently_rename</code>
 *      are <samp>false</samp> and a name collision occurs, we will throw an
 *      (@see UploadException).</dd>
 *    <dt>upload_dir_path</dt>
 *    <dd>String: A path to a directory in the server-side local 
 *      file-system in which uploaded files can be saved. Optional. 
 *      Defaults to <samp>/tmp</samp></dd>
 *    <dt>upload_dir_url</dt>
 *    <dd>String: the path to my upload directory (work directory) within
 *      the document root of the web server, if it <em>is</em> within
 *      the document root of the web server, else null. For example,
 *      if <code>uploadDirPath</code> was 
 *      <samp>"/home/httpd/htdocs/upload"</samp>, and the document 
 *      root of the Web server was <samp>/home/httpd/htdocs</samp>, 
 *      then it would make sense to have <code>uploadDirURL</code> set 
 *      to <samp>"/upload/"</samp>A Optional. No Default.</dd>
 *  </dl>
 *  All parameters may be supplied either as context-params or as 
 *  init-params; if both are supplied the servlet-specific init-param 
 *  will override the context-param.
 *  </p>
 *
 *  <p>NOTE: This code relies heavily on the Tomcat reference
 *  implementation and is thus in part <em>Copyright (c) 1999 The Apache
 *  Software Foundation.  All rights reserved.</em></p>
 *
 *  <blockquote>"This product includes software developed by the 
 *        Apache Software Foundation (http://www.apache.org/)."</blockquote>
 *
 *
 *   @author Simon Brooke (simon@jasmine.org.uk)
 *   @version $Revision: 1.12 $
 *   This revision: $Author: simon $
 *   <pre>
 *   $Log: MaybeUploadServlet.java,v $
 *   Revision 1.12  2001/04/24 15:55:58  simon
 *   Patch release incorporating Aaron Dunlop's ByteArrayInputStream stuff.
 *
 *   Revision 1.11  2001/04/09 11:24:24  simon
 *   Made maxUpload a configurable parameter ('max_upload')
 *
 *   Revision 1.10  2001/03/22 10:49:53  simon
 *   Allow parameters to be set from context-params, as well as from
 *   init-params
 *
 *   Revision 1.9  2001/02/22 11:16:31  simon
 *   Corrected deprecated use of UnavailableException, seeing we're no
 *   longer going for Servlet 2.1 spec backwards compatibility.
 *
 *   Revision 1.8  2001/02/22 11:11:22  simon
 *   Moved project version symbol from Makefile to Make-local-dependencies;
 *   backed out changes in MaybeUploadServlet and
 *   uk/co/weft/maybeupload/Makefile made whilst investigating possibility
 *   of Servlet 2.1 spec compatibility.
 *
 *   Revision 1.7  2001/01/23 19:12:17  simon
 *   A number of bugfixes, plus an important new feature: you can decide
 *   whether to allow name collisions in the upload directory to result
 *   in overwriting, renaming of the new file, or an exception.
 *
 *   Revision 1.6  2001/01/22 15:09:53  simon
 *   Cache file object for upload directory at load time; provided public
 *   methods to access upload directory and upload directory URL
 *
 *   Revision 1.5  2001/01/20 15:15:47  simon
 *   Added new configuration parameter, upload_dir_url, for use where the
 *   upload directory is visible within the HTTP space and may be linked
 *   to. Obviously this is optional; there may be many situations where you
 *   explicitly don't want anyone to be able to access uploaded files via
 *   HTTP!
 *
 *   Revision 1.4  2001/01/17 16:14:50  simon
 *   Some tidying up of indentation whilst trying to track down bug - which
 *   turned out to be in htform.Servlet, not here.
 *
 *   Revision 1.3  2001/01/09 12:14:12  simon
 *   Now tested with:
 *   	Netscape Communicator 4.76/Linux 2.2
 *   	Konqueror 1.9.8/Linux 2.2
 *   	Microsoft Internet Explorer 5.00.2014.0216IC
 *   File upload (including binary file upload) works. Remaining known bug:
 *   all fields must have data...
 *
 *   Revision 1.2  2001/01/08 12:40:10  simon
 *   Now working; still tidying up
 *
 *   Revision 1.1.1.1  2001/01/05 14:58:09  simon
 *   First cut - not yet tested
 *
 *   </pre> */

public abstract class MaybeUploadServlet extends javax.servlet.http.HttpServlet
{
    /** file system local path to where I unpack files I have uploaded */
    protected String uploadDirPath = "/tmp";

    /** the path to my upload directory (work directory) within the
     *  document root of the web server, if it is within the document
     *  root of the web server, else null. For example, if
     *  <code>uploadDirPath</code> was
     *  <samp>"/home/httpd/htdocs/upload"</samp>, and the document
     *  root of the Web server was <samp>/home/httpd/htdocs</samp>,
     *  then it would make sense to have <code>uploadDirURL</code> set
     *  to <samp>"/upload/"</samp> */
    protected String uploadDirURL = null;

    /** whether to allow uploaded files to be overwritten when new
     *  files are uploaded; default is we don't */
    protected boolean allowOverwrite = false;

    /** whether or not to rename uploaded files to prevent name
     *  collisions; default is we do */
    protected boolean silentlyRename = true;

    /** the maximum upload size: by default, half a megabyte. */
    protected int maxUpload = 524288;

    /** the actual upload directory as a file object */
    protected File uploadDir;

    /** whether or not to save uploads directly to disk; default is we do */
    protected boolean saveUploadedFilesToDisk = true;

    /* private static final stuff copied from HttpServlet - shame it
       wasn't 'protected' */
    private static final String METHOD_DELETE = "DELETE";
    private static final String METHOD_HEAD = "HEAD";
    private static final String METHOD_GET = "GET";
    private static final String METHOD_OPTIONS = "OPTIONS";
    private static final String METHOD_POST = "POST";
    private static final String METHOD_PUT = "PUT";
    private static final String METHOD_TRACE = "TRACE";

    private static final String HEADER_IFMODSINCE = "If-Modified-Since";
    private static final String HEADER_LASTMOD = "Last-Modified";
    
    public void init( ServletConfig config) throws ServletException
    {
	super.init( config);	// superclass initialisation *first*

	uploadDirPath = getStringParameterValue( "upload_dir_path", config, 
						 uploadDirPath);

 	uploadDirURL = getStringParameterValue( "upload_dir_url", config, 
						uploadDirURL);
	
 	allowOverwrite = getBooleanParameterValue( "allow_overwrite", config, 
						   allowOverwrite);
	
	silentlyRename = getBooleanParameterValue( "silently_rename", config,
						   silentlyRename);

	saveUploadedFilesToDisk = 
	    getBooleanParameterValue( "save_uploaded", config, 
				      saveUploadedFilesToDisk);

	maxUpload = getIntParameterValue( "max_upload", config,
						   maxUpload);

	uploadDir = new File( uploadDirPath);

	if ( ! uploadDir.isDirectory() || ! uploadDir.canWrite())
	    throw new 
		UnavailableException( "Cannot write to upload directory " + 
				      uploadDirPath);
    }


    /** Unpack a named parameter from the servlet config and return
     *  the value as a String. Context-wide values are less closely
     *  binding than servlet specific values.
     *
     *  @param name the name of the parameter to fetch
     *  @param config the configuration to fetch it from
     *  @param default the value to return if no parameter found
     */
    private String getStringParameterValue( String name, ServletConfig config,
					    String dflt)
    {
	String result = dflt;

	String v = config.getServletContext().getInitParameter( name);
	if ( v != null)
	    result = v;		// context overrides hard-coded default

	v = config.getInitParameter( name);
	if ( v != null)
	    result = v;		// servlet specific value overrides context

	return result;
    }

    /** Unpack a named parameter from the servlet config and return
     *  the value as a boolean. Context-wide values are less closely
     *  binding than servlet specific values.
     *
     *  @param name the name of the parameter to fetch
     *  @param config the configuration to fetch it from
     *  @param default the value to return if no parameter found
     */
    private boolean getBooleanParameterValue( String name, 
					      ServletConfig config, 
					      boolean dflt)
    {
	boolean result = dflt;

	String v = config.getServletContext().getInitParameter( name);
	if ( v != null)
	    result = Boolean.valueOf( v).booleanValue();
				// context overrides hard-coded default

	v = config.getInitParameter( name);
	if ( v != null)
	    result = Boolean.valueOf( v).booleanValue();
				// servlet specific value overrides context

	return result;
    }
   
    /** Unpack a named parameter from the servlet config and return
     *  the value as an int. Context-wide values are less closely
     *  binding than servlet specific values.
     *
     *  @param name the name of the parameter to fetch
     *  @param config the configuration to fetch it from
     *  @param default the value to return if no parameter found
     */
    private int getIntParameterValue( String name, 
				      ServletConfig config, 
				      int dflt)
    {
	int result = dflt;

	String v = config.getServletContext().getInitParameter( name);
	if ( v != null)
	    result = Integer.parseInt( v);
				// context overrides hard-coded default

	v = config.getInitParameter( name);
	if ( v != null)
	    result = Integer.parseInt( v);
				// servlet specific value overrides context

	return result;
    }


    /** Service a request. This method heavily dependent on the
     *  reference implementation.
     *
     *  @param req the request to be serviced
     *  @param resp the response being constructed to this request
     */
    public void service( HttpServletRequest req, HttpServletResponse resp)
	throws ServletException, IOException
    {
	MaybeUploadRequestWrapper wrapper = new 
	    MaybeUploadRequestWrapper( req, saveUploadedFilesToDisk,
				       uploadDir, allowOverwrite, 
				       silentlyRename, maxUpload);

	String method = req.getMethod();

	if ( method.equals( METHOD_GET)) 
	    {
		long lastModified = getLastModified(req);
		if (lastModified == -1) 
		    {
				// servlet doesn't support
				// if-modified-since, no reason to go
				// through further expensive logic
			doGet(wrapper, resp);
		    } 
		else 
		    {
			long ifModifiedSince = 
			    req.getDateHeader(HEADER_IFMODSINCE);

			if ( ifModifiedSince < ( lastModified / 1000 * 1000)) 
			    {
				// If the servlet mod time is later,
				// call doGet( ) Round down to the
				// nearest second for a proper compare
				// A ifModifiedSince of -1 will always
				// be less
				maybeSetLastModified( resp, lastModified);
				doGet( wrapper, resp);
			    } 
			else 
			    {
				resp.setStatus( 
				       HttpServletResponse.SC_NOT_MODIFIED);
			    }
		    }

	    } 
	else if ( method.equals( METHOD_HEAD)) 
	    {
		long lastModified = getLastModified( req);
		maybeSetLastModified( resp, lastModified);
		doHead( wrapper, resp);

	    } 
	else if ( method.equals( METHOD_POST)) 
	    {
		doPost( wrapper, resp);
	    } 
	else if ( method.equals( METHOD_PUT)) 
	    {
		doPut( wrapper, resp);		    
	    } 
	else if ( method.equals( METHOD_DELETE)) 
	    {
		doDelete( wrapper, resp);
	    } 
	else if ( method.equals( METHOD_OPTIONS)) 
	    {
		doOptions( wrapper,resp);
	    } 
	else if ( method.equals( METHOD_TRACE)) 
	    {
		doTrace( wrapper,resp);
	    } 
	else 
	    {
		resp.sendError( HttpServletResponse.SC_NOT_IMPLEMENTED, 
				new String( "HTTP Method [" + method + 
					    "] is not implemented"));
	    }
    }


    /** Sets the Last-Modified entity header field, if it has not
     *  already been set and if the value is meaningful.  Called before
     *  doGet, to ensure that headers are set before response data is
     *  written.  A subclass might have set this header already, so we
     *  check. Copied in it's entirety from the refernce implementation.
     */

    private void maybeSetLastModified( HttpServletResponse resp,
				       long lastModified) 
    {
	if ( resp.containsHeader( HEADER_LASTMOD))
	    return;
	if ( lastModified >= 0)
	    resp.setDateHeader( HEADER_LASTMOD, lastModified);
    }
   

    /** Simple wrapper round HttpServlet.doGet( ), so that you can
     *  depend on having a MaybeUploadRequestWrapper in your code.
     *
     *  @param req a request wrapper which know how to handle upload
     *  @param resp a standard servlet response
     */
    protected void doGet( MaybeUploadRequestWrapper req, 
			  HttpServletResponse resp)
	throws ServletException, IOException
    {
	super.doGet( req, resp);
    }



    /** Simple wrapper round HttpServlet.doPost( ), so that you can
     *  depend on having a MaybeUploadRequestWrapper in your code.
     *
     *  @param req a request wrapper which know how to handle upload
     *  @param resp a standard servlet response
     */
    protected void doPost( MaybeUploadRequestWrapper req, 
			  HttpServletResponse resp)
	throws ServletException, IOException
    {
	super.doPost( req, resp);
    }



    /** Simple wrapper round HttpServlet.doDelete( ), so that you can
     *  depend on having a MaybeUploadRequestWrapper in your code.
     *
     *  @param req a request wrapper which know how to handle upload
     *  @param resp a standard servlet response
     */
    protected void doDelete( MaybeUploadRequestWrapper req, 
			  HttpServletResponse resp)
	throws ServletException, IOException
    {
	super.doDelete( req, resp);
    }



    /** Simple wrapper round HttpServlet.doPut( ), so that you can
     *  depend on having a MaybeUploadRequestWrapper in your code.
     *
     *  @param req a request wrapper which know how to handle upload
     *  @param resp a standard servlet response
     */
    protected void doPut( MaybeUploadRequestWrapper req, 
			  HttpServletResponse resp)
	throws ServletException, IOException
    {
	super.doPut( req, resp);
    }



    /** Simple wrapper round HttpServlet.doOptions( ), so that you can
     *  depend on having a MaybeUploadRequestWrapper in your code.
     *
     *  @param req a request wrapper which know how to handle upload
     *  @param resp a standard servlet response
     */
    protected void doOptions( MaybeUploadRequestWrapper req, 
			  HttpServletResponse resp)
	throws ServletException, IOException
    {
	super.doOptions( req, resp);
    }



    /** Simple wrapper round HttpServlet.doTrace( ), so that you can
     *  depend on having a MaybeUploadRequestWrapper in your code.
     *
     *  @param req a request wrapper which know how to handle upload
     *  @param resp a standard servlet response
     */
    protected void doTrace( MaybeUploadRequestWrapper req, 
			  HttpServletResponse resp)
	throws ServletException, IOException
    {
	super.doTrace( req, resp);
    }

    /** doHead is a bit more tricky. not really handled in this
     *  version, although I'll happily integrate code from anyone else
     *  who is sufficiently confident to write an
     *  implementation. Could just copy the HttpServlet version,
     *  but...
     *
     *  @param req a request wrapper which know how to handle upload
     *  @param resp a standard servlet response */
    protected void doHead( MaybeUploadRequestWrapper req, 
			  HttpServletResponse resp)
	throws ServletException, IOException
    {
	String protocol = req.getProtocol( );
	String msg = "HTTP method HEAD not supported";
	if ( protocol.endsWith( "1.1")) {
	    resp.sendError( HttpServletResponse.SC_METHOD_NOT_ALLOWED, msg);
	} else {
	    resp.sendError( HttpServletResponse.SC_BAD_REQUEST, msg);
	}
	
    }

    /** @return my upload directory. Note that the upload directory
     *  cannot be set dynamically at run-time for security reasons */
    public File getUploadDir()
    {
	return uploadDir;
    }

    /** @return as a String, the base URL for my upload directory if
     *  any. If returned, may be an absolute URL or a URL fragment
     *  relative to the document root of the HTTP server hosting the
     *  Servlet engine. In null, means that you cannot access the
     *  upload directory via HTTP */
    public String getUploadURL()
    {
	return uploadDirURL;
    }
}
