package org.apache.velocity.tools.view;

/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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.StringWriter;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.velocity.Template;
import org.apache.velocity.context.Context;
import org.apache.velocity.exception.MethodInvocationException;

/**
 * Extension of the VelocityViewServlet to perform "two-pass"
 * layout rendering and allow for a customized error screen.
 *
 * @author Nathan Bubna
 * @version $Id: VelocityLayoutServlet.java 823236 2009-10-08 17:24:48Z nbubna $
 */
public class VelocityLayoutServlet extends VelocityViewServlet
{

    /** serial version id */
    private static final long serialVersionUID = -4521817395157483487L;

    /**
     * The velocity.properties key for specifying the
     * servlet's error template.
     */
    public static final String PROPERTY_ERROR_TEMPLATE =
        "tools.view.servlet.error.template";

    /**
     * The velocity.properties key for specifying the
     * relative directory holding layout templates.
     */
    public static final String PROPERTY_LAYOUT_DIR =
        "tools.view.servlet.layout.directory";

    /**
     * The velocity.properties key for specifying the
     * servlet's default layout template's filename.
     */
    public static final String PROPERTY_DEFAULT_LAYOUT =
        "tools.view.servlet.layout.default.template";


    /**
     * The default error template's filename.
     */
    public static final String DEFAULT_ERROR_TEMPLATE = "Error.vm";

    /**
     * The default layout directory
     */
    public static final String DEFAULT_LAYOUT_DIR = "layout/";

    /**
     * The default filename for the servlet's default layout
     */
    public static final String DEFAULT_DEFAULT_LAYOUT = "Default.vm";


    /**
     * The context key that will hold the content of the screen.
     *
     * This key ($screen_content) must be present in the layout
     * template for the current screen to be rendered.
     */
    public static final String KEY_SCREEN_CONTENT = "screen_content";

    /**
     * The context/parameter key used to specify an alternate
     * layout to be used for a request instead of the default layout.
     */
    public static final String KEY_LAYOUT = "layout";


    /**
     * The context key that holds the {@link Throwable} that
     * broke the rendering of the requested screen.
     */
    public static final String KEY_ERROR_CAUSE = "error_cause";

    /**
     * The context key that holds the stack trace of the error that
     * broke the rendering of the requested screen.
     */
    public static final String KEY_ERROR_STACKTRACE = "stack_trace";

    /**
     * The context key that holds the {@link MethodInvocationException}
     * that broke the rendering of the requested screen.
     *
     * If this value is placed in the context, then $error_cause
     * will hold the error that this invocation exception is wrapping.
     */
    public static final String KEY_ERROR_INVOCATION_EXCEPTION = "invocation_exception";


    protected String errorTemplate;
    protected String layoutDir;
    protected String defaultLayout;

    /**
     * Initializes Velocity, the view servlet and checks for changes to
     * the initial layout configuration.
     *
     * @param config servlet configuration parameters
     */
    public void init(ServletConfig config) throws ServletException
    {
        // first do VVS' init()
        super.init(config);

        // check for default template path overrides
        errorTemplate =
            getVelocityProperty(PROPERTY_ERROR_TEMPLATE, DEFAULT_ERROR_TEMPLATE);
        layoutDir =
            getVelocityProperty(PROPERTY_LAYOUT_DIR, DEFAULT_LAYOUT_DIR);
        defaultLayout =
            getVelocityProperty(PROPERTY_DEFAULT_LAYOUT, DEFAULT_DEFAULT_LAYOUT);

        // preventive error checking! directory must end in /
        if (!layoutDir.endsWith("/"))
        {
            layoutDir += '/';
        }

        // log the current settings
        getLog().info("VelocityLayoutServlet: Error screen is '"+errorTemplate+"'");
        getLog().info("VelocityLayoutServlet: Layout directory is '"+layoutDir+"'");
        getLog().info("VelocityLayoutServlet: Default layout template is '"+defaultLayout+"'");

        // for efficiency's sake, make defaultLayout a full path now
        defaultLayout = layoutDir + defaultLayout;
    }


    /**
     * Overrides VelocityViewServlet to check the request for
     * an alternate layout
     *
     * @param ctx context for this request
     * @param request client request
     */
    protected void fillContext(Context ctx, HttpServletRequest request)
    {
        String layout = findLayout(request);
        if (layout != null)
        {
            // let the template know what its new layout is
            ctx.put(KEY_LAYOUT, layout);
        }
    }

    /**
     * Searches for a non-default layout to be used for this request.
     * This implementation checks the request parameters and attributes.
     */
    protected String findLayout(HttpServletRequest request)
    {
        // check if an alternate layout has been specified
        // by way of the request parameters
        String layout = request.getParameter(KEY_LAYOUT);
        // also look in the request attributes 
        if (layout == null) 
        { 
            layout = (String)request.getAttribute(KEY_LAYOUT); 
        }
        return layout;
    }

    /**
     * Overrides VelocityViewServlet.mergeTemplate to do a two-pass
     * render for handling layouts
     */
    protected void mergeTemplate(Template template, Context context,
                                 HttpServletResponse response)
        throws IOException
    {
        //
        // this section is based on Tim Colson's "two pass render"
        //
        // Render the screen content
        StringWriter sw = new StringWriter();
        template.merge(context, sw);
        // Add the resulting content to the context
        context.put(KEY_SCREEN_CONTENT, sw.toString());

        // Check for an alternate layout
        //
        // we check after merging the screen template so the screen
        // can overrule any layout set in the request parameters
        // by doing #set( $layout = "MyLayout.vm" )
        Object obj = context.get(KEY_LAYOUT);
        String layout = (obj == null) ? null : obj.toString();
        if (layout == null)
        {
            // no alternate, use default
            layout = defaultLayout;
        }
        else
        {
            // make it a full(er) path
            layout = layoutDir + layout;
        }

        try
        {
            //load the layout template
            template = getTemplate(layout);
        }
        catch (Exception e)
        {
            getLog().error("Can't load layout \"" + layout + "\"", e);

            // if it was an alternate layout we couldn't get...
            if (!layout.equals(defaultLayout))
            {
                // try to get the default layout
                // if this also fails, let the exception go
                template = getTemplate(defaultLayout);
            }
        }

        // Render the layout template into the response
        super.mergeTemplate(template, context, response);
    }


    /**
     * Overrides VelocityViewServlet to display user's custom error template
     */
    protected void error(HttpServletRequest request,
                         HttpServletResponse response,
                         Throwable e)
    {
        try
        {
            // get a velocity context
            Context ctx = createContext(request, response);

            Throwable cause = e;

            // if it's an MIE, i want the real cause and stack trace!
            if (cause instanceof MethodInvocationException)
            {
                // put the invocation exception in the context
                ctx.put(KEY_ERROR_INVOCATION_EXCEPTION, e);
                // get the real cause
                cause = ((MethodInvocationException)e).getWrappedThrowable();
            }

            // add the cause to the context
            ctx.put(KEY_ERROR_CAUSE, cause);

            // grab the cause's stack trace and put it in the context
            StringWriter sw = new StringWriter();
            cause.printStackTrace(new java.io.PrintWriter(sw));
            ctx.put(KEY_ERROR_STACKTRACE, sw.toString());

            // retrieve and render the error template
            Template et = getTemplate(errorTemplate);
            mergeTemplate(et, ctx, response);

        }
        catch (Exception e2)
        {
            // d'oh! log this
            getLog().error("Error during error template rendering", e2);
            // then punt the original to a higher authority
            super.error(request, response, e);
        }
    }


}
