package org.apache.velocity.servlet;

/*
 * Copyright 2000-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.
 */

import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.io.OutputStreamWriter;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.UnsupportedEncodingException;

import java.util.Properties;

import javax.servlet.ServletConfig;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;

import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.velocity.Template;
import org.apache.velocity.runtime.RuntimeConstants;
import org.apache.velocity.runtime.RuntimeSingleton;
import org.apache.velocity.io.VelocityWriter;
import org.apache.velocity.util.SimplePool;

import org.apache.velocity.context.Context;
import org.apache.velocity.VelocityContext;

import org.apache.velocity.app.Velocity;

import org.apache.velocity.exception.ResourceNotFoundException;
import org.apache.velocity.exception.ParseErrorException;
import org.apache.velocity.exception.MethodInvocationException;

/**
 * Base class which simplifies the use of Velocity with Servlets.
 * Extend this class, implement the <code>handleRequest()</code> method, 
 * and add your data to the context.  Then call 
 * <code>getTemplate("myTemplate.wm")</code>.
 * 
 * This class puts some things into the context object that you should
 * be aware of:
 * <pre>
 * "req" - The HttpServletRequest object
 * "res" - The HttpServletResponse object
 * </pre>
 *
 * There are other methods you can override to access, alter or control
 * any part of the request processing chain.  Please see the javadocs for
 * more information on :
 * <ul>
 * <li> loadConfiguration() : for setting up the Velocity runtime
 * <li> createContext() : for creating and loading the Context
 * <li> setContentType() : for changing the content type on a request
 *                         by request basis
 * <li> handleRequest() : you <b>must</b> implement this 
 * <li> mergeTemplate()  : the template rendering process
 * <li> requestCleanup() : post rendering resource or other cleanup
 * <li> error() : error handling
 * </ul>
 * <br>
 * If you put a contentType object into the context within either your
 * serlvet or within your template, then that will be used to override
 * the default content type specified in the properties file.
 *
 * "contentType" - The value for the Content-Type: header
 *
 * @author Dave Bryson
 * @author <a href="mailto:jon@latchkey.com">Jon S. Stevens</a>
 * @author <a href="mailto:geirm@optonline.net">Geir Magnusson Jr.</a>
 * @author <a href="kjohnson@transparent.com">Kent Johnson</a>
 * @author <a href="dlr@finemaltcoding.com">Daniel Rall</a>
 * $Id: VelocityServlet.java,v 1.52.4.1 2004/03/03 23:23:03 geirm Exp $
 */
public abstract class VelocityServlet extends HttpServlet
{
    /**
     * The context key for the HTTP request object.
     */
    public static final String REQUEST = "req";

    /**
     * The context key for the HTTP response object.
     */
    public static final String RESPONSE = "res";

    /**
     * The HTTP content type context key.
     */
    public static final String CONTENT_TYPE = "default.contentType";

    /**
     *  The default content type for the response
     */
    public static final String DEFAULT_CONTENT_TYPE = "text/html";
    
  
    /**
     *  Encoding for the output stream
     */
    public static final String DEFAULT_OUTPUT_ENCODING = "ISO-8859-1";

    /**
     * The default content type, itself defaulting to {@link
     * #DEFAULT_CONTENT_TYPE} if not configured.
     */
    private static String defaultContentType;

    /**
     * This is the string that is looked for when getInitParameter is
     * called (<code>org.apache.velocity.properties</code>).
     */
    protected static final String INIT_PROPS_KEY =
        "org.apache.velocity.properties";

    /**
     * Use of this properties key has been deprecated, and will be
     * removed in Velocity version 1.5.
     */
    private static final String OLD_INIT_PROPS_KEY = "properties";

    /**
     * Cache of writers
     */
   
    private static SimplePool writerPool = new SimplePool(40);
   
    /** 
     * Performs initialization of this servlet.  Called by the servlet 
     * container on loading.
     *
     * @param config The servlet configuration to apply.
     *
     * @exception ServletException
     */
    public void init( ServletConfig config )
        throws ServletException
    {
        super.init( config );

        /*
         *  do whatever we have to do to init Velocity
         */                
        initVelocity( config );

        /*
         *  Now that Velocity is initialized, cache some config.
         */
        defaultContentType = RuntimeSingleton.getString(CONTENT_TYPE,
                                                        DEFAULT_CONTENT_TYPE);
    }

    /**
     *  Initializes the Velocity runtime, first calling 
     *  loadConfiguration(ServletConvig) to get a 
     *  java.util.Properties of configuration information
     *  and then calling Velocity.init().  Override this
     *  to do anything to the environment before the 
     *  initialization of the singelton takes place, or to 
     *  initialize the singleton in other ways.
     */
    protected void initVelocity( ServletConfig config )
         throws ServletException
    {
        try
        {
            /*
             *  call the overridable method to allow the 
             *  derived classes a shot at altering the configuration
             *  before initializing Runtime
             */

            Properties props = loadConfiguration( config );
  
            Velocity.init( props );
        }
        catch( Exception e )
        {
            throw new ServletException("Error initializing Velocity: " + e, e);
        }   
    }    
     
    /**
     *  Loads the configuration information and returns that 
     *  information as a Properties, which will be used to 
     *  initialize the Velocity runtime.
     *  <br><br>
     *  Currently, this method gets the initialization parameter
     *  VelocityServlet.INIT_PROPS_KEY, which should be a file containing
     *  the configuration information.
     *  <br><br>
     *  To configure your Servlet Spec 2.2 compliant servlet runner to pass
     *  this to you, put the following in your WEB-INF/web.xml file
     *  <br>
     *  <pre>
     *    &lt;servlet&gt;
     *      &lt;servlet-name&gt; YourServlet &lt/servlet-name&gt;
     *      &lt;servlet-class&gt; your.package.YourServlet &lt;/servlet-class&gt;
     *      &lt;init-param&gt;
     *         &lt;param-name&gt; org.apache.velocity.properties &lt;/param-name&gt;
     *         &lt;param-value&gt; velocity.properties &lt;/param-value&gt;
     *      &lt;/init-param&gt;
     *    &lt;/servlet&gt;
     *   </pre>
     *
     * Alternately, if you wish to configure an entire context in this
     * fashion, you may use the following:
     *  <br>
     *  <pre>
     *    &lt;context-param&gt;
     *       &lt;param-name&gt; org.apache.velocity.properties &lt;/param-name&gt;
     *       &lt;param-value&gt; velocity.properties &lt;/param-value&gt;
     *       &lt;description&gt; Path to Velocity configuration &lt;/description&gt;
     *    &lt;/context-param&gt;
     *   </pre>
     * 
     *  Derived classes may do the same, or take advantage of this code to do the loading for them via :
     *   <pre>
     *      Properties p = super.loadConfiguration( config );
     *   </pre>
     *  and then add or modify the configuration values from the file.
     *  <br>
     *
     *  @param config ServletConfig passed to the servlets init() function
     *                Can be used to access the real path via ServletContext (hint)
     *  @return java.util.Properties loaded with configuration values to be used
     *          to initialize the Velocity runtime.
     *  @throws FileNotFoundException if a specified file is not found.
     *  @throws IOException I/O problem accessing the specified file, if specified.
     */
    protected Properties loadConfiguration(ServletConfig config)
        throws IOException, FileNotFoundException
    {
        // This is a little overly complex because of legacy support
        // for the initialization properties key "properties".
        // References to OLD_INIT_PROPS_KEY should be removed at
        // Velocity version 1.5.
        String propsFile = config.getInitParameter(INIT_PROPS_KEY);
        if (propsFile == null || propsFile.length() == 0)
        {
            ServletContext sc = config.getServletContext();
            propsFile = config.getInitParameter(OLD_INIT_PROPS_KEY);
            if (propsFile == null || propsFile.length() == 0)
            {
                propsFile = sc.getInitParameter(INIT_PROPS_KEY);
                if (propsFile == null || propsFile.length() == 0)
                {
                    propsFile = sc.getInitParameter(OLD_INIT_PROPS_KEY);
                    if (propsFile != null && propsFile.length() > 0)
                    {
                        sc.log("Use of the properties initialization " +
                               "parameter '" + OLD_INIT_PROPS_KEY + "' has " +
                               "been deprecated by '" + INIT_PROPS_KEY + '\'');
                    }
                }
            }
            else
            {
                sc.log("Use of the properties initialization parameter '" +
                       OLD_INIT_PROPS_KEY + "' has been deprecated by '" +
                       INIT_PROPS_KEY + '\'');
            }
        }
        
        /*
         * This will attempt to find the location of the properties
         * file from the relative path to the WAR archive (ie:
         * docroot). Since JServ returns null for getRealPath()
         * because it was never implemented correctly, then we know we
         * will not have an issue with using it this way. I don't know
         * if this will break other servlet engines, but it probably
         * shouldn't since WAR files are the future anyways.
         */

        Properties p = new Properties();
        
        if ( propsFile != null )
        {
            String realPath = getServletContext().getRealPath(propsFile);
        
            if ( realPath != null )
            {
                propsFile = realPath;
            }

            p.load( new FileInputStream(propsFile) );
        }

        return p;
    }
          
    /**
     * Handles HTTP <code>GET</code> requests by calling {@link
     * #doRequest()}.
     */
    public void doGet( HttpServletRequest request, HttpServletResponse response )
        throws ServletException, IOException
    {
        doRequest(request, response);
    }

    /**
     * Handles HTTP <code>POST</code> requests by calling {@link
     * #doRequest()}.
     */
    public void doPost( HttpServletRequest request, HttpServletResponse response )
        throws ServletException, IOException
    {
        doRequest(request, response);
    }

    /**
     *  Handles all requests (by default).
     *
     *  @param request  HttpServletRequest object containing client request
     *  @param response HttpServletResponse object for the response
     */
    protected void doRequest(HttpServletRequest request, HttpServletResponse response )
         throws ServletException, IOException
    {
        Context context = null;
        try
        {
            /*
             *  first, get a context
             */

            context = createContext( request, response );
            
            /*
             *   set the content type 
             */

            setContentType( request, response );

            /*
             *  let someone handle the request
             */

            Template template = handleRequest( request, response, context );        
            /*
             *  bail if we can't find the template
             */

            if ( template == null )
            {
                return;
            }

            /*
             *  now merge it
             */

            mergeTemplate( template, context, response );
        }
        catch (Exception e)
        {
            /*
             *  call the error handler to let the derived class
             *  do something useful with this failure.
             */

            error( request, response, e);
        }
        finally
        {
            /*
             *  call cleanup routine to let a derived class do some cleanup
             */

            requestCleanup( request, response, context );
        }
    }

    /**
     *  A cleanup routine which is called at the end of the {@link
     *  #doRequest(HttpServletRequest, HttpServletResponse)}
     *  processing sequence, allowing a derived class to do resource
     *  cleanup or other end of process cycle tasks.
     *
     *  @param request servlet request from client 
     *  @param response servlet reponse 
     *  @param context  context created by the createContext() method
     */
    protected void requestCleanup( HttpServletRequest request, HttpServletResponse response, Context context )
    {
        return;
    }

    /**
     *  merges the template with the context.  Only override this if you really, really
     *  really need to. (And don't call us with questions if it breaks :)
     *
     *  @param template template object returned by the handleRequest() method
     *  @param context  context created by the createContext() method
     *  @param response servlet reponse (use this to get the output stream or Writer
     */
    protected void mergeTemplate( Template template, Context context, HttpServletResponse response )
        throws ResourceNotFoundException, ParseErrorException, 
               MethodInvocationException, IOException, UnsupportedEncodingException, Exception
    {
        ServletOutputStream output = response.getOutputStream();
        VelocityWriter vw = null;
        // ASSUMPTION: response.setContentType() has been called.
        String encoding = response.getCharacterEncoding();
        
        try
        {
            vw = (VelocityWriter) writerPool.get();
            
            if (vw == null)
            {
                vw = new VelocityWriter(new OutputStreamWriter(output,
                                                               encoding),
                                        4 * 1024, true);
            }
            else
            {
                vw.recycle(new OutputStreamWriter(output, encoding));
            }
           
            template.merge(context, vw);
        }
        finally
        {
            try
            {
                if (vw != null)
                {
                    /*
                     *  flush and put back into the pool
                     *  don't close to allow us to play
                     *  nicely with others.
                     */
                    vw.flush();

                    /*
                     * Clear the VelocityWriter's reference to its
                     * internal OutputStreamWriter to allow the latter
                     * to be GC'd while vw is pooled.
                     */
                    vw.recycle(null);

                    writerPool.put(vw);
                }
            }
            catch (Exception e)
            {
                // do nothing
            }
        }
    }

    /**
     * Sets the content type of the response, defaulting to {@link
     * #defaultContentType} if not overriden.  Delegates to {@link
     * #chooseCharacterEncoding(HttpServletRequest)} to select the
     * appropriate character encoding.
     *
     * @param request The servlet request from the client.
     * @param response The servlet reponse to the client.
     */
    protected void setContentType(HttpServletRequest request,
                                  HttpServletResponse response)
    {
        String contentType = defaultContentType;
        int index = contentType.lastIndexOf(';') + 1;
        if (index <= 0 || (index < contentType.length() &&
                           contentType.indexOf("charset", index) == -1))
        {
            // Append the character encoding which we'd like to use.
            String encoding = chooseCharacterEncoding(request);
            //System.out.println("Chose output encoding of '" +
            //                   encoding + '\'');
            if (!DEFAULT_OUTPUT_ENCODING.equalsIgnoreCase(encoding))
            {
                contentType += "; charset=" + encoding;
            }
        }
        response.setContentType(contentType);
        //System.out.println("Response Content-Type set to '" +
        //                   contentType + '\'');
    }

    /**
     * Chooses the output character encoding to be used as the value
     * for the "charset=" portion of the HTTP Content-Type header (and
     * thus returned by <code>response.getCharacterEncoding()</code>).
     * Called by {@link #setContentType(HttpServletRequest,
     * HttpServletResponse)} if an encoding isn't already specified by
     * Content-Type.  By default, chooses the value of
     * RuntimeSingleton's <code>output.encoding</code> property.
     *
     * @param request The servlet request from the client.
     */
    protected String chooseCharacterEncoding(HttpServletRequest request)
    {
        return RuntimeSingleton.getString(RuntimeConstants.OUTPUT_ENCODING,
                                          DEFAULT_OUTPUT_ENCODING);
    }

    /**
     *  Returns a context suitable to pass to the handleRequest() method
     *  <br><br>
     *  Default implementation will create a VelocityContext object,
     *   put the HttpServletRequest and HttpServletResponse
     *  into the context accessable via the keys VelocityServlet.REQUEST and
     *  VelocityServlet.RESPONSE, respectively.
     *
     *  @param request servlet request from client
     *  @param response servlet reponse to client
     *
     *  @return context
     */
    protected Context createContext(HttpServletRequest request,  HttpServletResponse response )
    {
        /*
         *   create a new context
         */

        VelocityContext context = new VelocityContext();
        
        /*
         *   put the request/response objects into the context
         *   wrap the HttpServletRequest to solve the introspection
         *   problems 
         */
           
        context.put( REQUEST,  request );
        context.put( RESPONSE, response );

        return context;
    }

    /**
     * Retrieves the requested template.
     *
     * @param name The file name of the template to retrieve relative to the 
     *             template root.
     * @return     The requested template.
     * @throws ResourceNotFoundException if template not found
     *          from any available source.
     * @throws ParseErrorException if template cannot be parsed due
     *          to syntax (or other) error.
     * @throws Exception if an error occurs in template initialization
     */
    public Template getTemplate( String name )
        throws ResourceNotFoundException, ParseErrorException, Exception
    {
        return RuntimeSingleton.getTemplate(name);
    }
    
    /**
     * Retrieves the requested template with the specified
     * character encoding.
     *
     * @param name The file name of the template to retrieve relative to the 
     *             template root.
     * @param encoding the character encoding of the template
     *
     * @return     The requested template.
     * @throws ResourceNotFoundException if template not found
     *          from any available source.
     * @throws ParseErrorException if template cannot be parsed due
     *          to syntax (or other) error.
     * @throws Exception if an error occurs in template initialization
     *     
     *  @since Velocity v1.1
     */
    public Template getTemplate( String name, String encoding )
        throws ResourceNotFoundException, ParseErrorException, Exception
    {
        return RuntimeSingleton.getTemplate( name, encoding );
    }

    /**
     * Implement this method to add your application data to the context, 
     * calling the <code>getTemplate()</code> method to produce your return 
     * value.
     * <br><br>
     * In the event of a problem, you may handle the request directly
     * and return <code>null</code> or throw a more meaningful exception
     * for the error handler to catch.
     *
     *  @param request servlet request from client 
     *  @param response servlet reponse 
     *  @param ctx The context to add your data to.
     *  @return    The template to merge with your context or null, indicating
     *    that you handled the processing.
     *
     *  @since Velocity v1.1
     */
    protected Template handleRequest( HttpServletRequest request, HttpServletResponse response, Context ctx ) 
        throws Exception
    {
        /*
         * invoke handleRequest
         */

        Template t =  handleRequest( ctx );

        /*
         *  if it returns null, this is the 'old' deprecated 
         *  way, and we want to mimic the behavior for a little 
         *  while anyway
         */

        if (t == null)
        {
            throw new Exception ("handleRequest(Context) returned null - no template selected!" );
        }

        return t;
    }

    /**
     * Implement this method to add your application data to the context, 
     * calling the <code>getTemplate()</code> method to produce your return 
     * value.
     * <br><br>
     * In the event of a problem, you may simple return <code>null</code>
     * or throw a more meaningful exception.
     *
     * @deprecated Use
     * {@link #handleRequest( HttpServletRequest request, 
     * HttpServletResponse response, Context ctx )}
     *
     * @param ctx The context to add your data to.
     * @return    The template to merge with your context.
     */
    protected Template handleRequest( Context ctx ) 
        throws Exception
    {
        throw new Exception ("You must override VelocityServlet.handleRequest( Context) "
                             + " or VelocityServlet.handleRequest( HttpServletRequest, "
                             + " HttpServletResponse, Context)" );
    }
 
    /**
     * Invoked when there is an error thrown in any part of doRequest() processing.
     * <br><br>
     * Default will send a simple HTML response indicating there was a problem.
     * 
     * @param request original HttpServletRequest from servlet container.
     * @param response HttpServletResponse object from servlet container.
     * @param cause  Exception that was thrown by some other part of process.
     */
    protected void error( HttpServletRequest request, HttpServletResponse response, Exception cause )
        throws ServletException, IOException
    {
        StringBuffer html = new StringBuffer();
        html.append("<html>");
        html.append("<title>Error</title>");
        html.append("<body bgcolor=\"#ffffff\">");
        html.append("<h2>VelocityServlet: Error processing the template</h2>");
        html.append("<pre>");
        String why = cause.getMessage();
        if (why != null && why.trim().length() > 0)
        {
            html.append(why);
            html.append("<br>");
        }

        StringWriter sw = new StringWriter();
        cause.printStackTrace( new PrintWriter( sw ) );

        html.append( sw.toString()  );
        html.append("</pre>");
        html.append("</body>");
        html.append("</html>");
        response.getOutputStream().print( html.toString() );
    }
}
