/*
 * Copyright 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.
 */
package org.apache.portals.bridges.jsf;

import java.io.IOException;

import javax.faces.FacesException;
import javax.faces.FactoryFinder;
import javax.faces.application.Application;
import javax.faces.component.UIViewRoot;
import javax.faces.context.FacesContext;
import javax.faces.context.FacesContextFactory;
import javax.faces.lifecycle.Lifecycle;
import javax.faces.lifecycle.LifecycleFactory;
import javax.faces.render.RenderKitFactory;
import javax.faces.webapp.FacesServlet;

import javax.portlet.ActionRequest;
import javax.portlet.ActionResponse;
import javax.portlet.PortletConfig;
import javax.portlet.PortletException;
import javax.portlet.PortletMode;
import javax.portlet.PortletRequest;
import javax.portlet.PortletResponse;
import javax.portlet.PortletSession;
import javax.portlet.RenderRequest;
import javax.portlet.RenderResponse;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.portals.bridges.common.GenericServletPortlet;

/**
 * <p>
 * FacesPortlet utilizes Java Server Faces to create the user interface in a
 * portlet environment.
 * </p>
 * 
 * @author <a href="mailto:dlestrat@yahoo.com">David Le Strat</a>
 * @author <a href="mailto:taylor@apache.org">David Sean Taylor</a>
 */
public class FacesPortlet extends GenericServletPortlet
{

    /** The Log instance for this class. */
    private static final Log log = LogFactory.getLog(FacesPortlet.class);

    /** The VIEW_ROOT used to keep track of action between the action request and the render request. */
    public static final String VIEW_ROOT = "org.apache.portals.bridges.jsf.VIEW_ROOT";
    
    /** 
     * The REQUEST_SERVLET_PATH used for externalContext.getRequestServletPath(). externalContext.getRequestServletPath()
     * should return null but this is a work around an issue with MyFaces JspViewHandler implementation getServletMapping().
     */
    public static final String REQUEST_SERVLET_PATH = "org.apache.portals.bridges.jsf.REQUEST_SERVLET_PATH";

    /** The JSF_VIEW_ID used to maintain the state of the view action. */
    public static final String JSF_VIEW_ID = "jsf_viewid";
    public static final String JSF_EDIT_ID = "jsf_editid";
    public static final String JSF_HELP_ID = "jsf_helpid";
    public static final String JSF_CUSTOM_ID = "jsf_customid";
    
    /** Name of portlet preference for Action page. */
    public static final String PARAM_ACTION_PAGE = "ActionPage";

    /** Name of portlet preference for Custom page. */
    public static final String PARAM_CUSTOM_PAGE = "CustomPage";

    /** Name of portlet preference for Edit page. */
    public static final String PARAM_EDIT_PAGE = "EditPage";

    /** Name of portlet preference for Edit page */
    public static final String PARAM_HELP_PAGE = "HelpPage";

    /** Name of portlet preference for View page */
    public static final String PARAM_VIEW_PAGE = "ViewPage";

    /** Action request. */
    public static final String ACTION_REQUEST = "ACTION";

    /** View request. */
    public static final String VIEW_REQUEST = "VIEW";

    /** Custom request. */
    public static final String CUSTOM_REQUEST = "CUSTOM";

    /** Edit request. */
    public static final String EDIT_REQUEST = "EDIT";

    /** Help request. */
    public static final String HELP_REQUEST = "HELP";

    /** Default URL for the action page. */
    private String defaultActionPage = null;

    /** Default URL for the custom page. */
    private String defaultCustomPage = null;

    /** Default URL for the edit page. */
    private String defaultEditPage = null;

    /** Default URL for the help page. */
    private String defaultHelpPage = null;

    /** Default URL for the view page. */
    private String defaultViewPage = null;

    /**
     * <p>
     * Context initialization parameter name for the lifecycle identifier of the
     * {@link Lifecycle}instance to be utilized.
     * </p>
     */
    private static final String LIFECYCLE_ID_ATTR = FacesServlet.LIFECYCLE_ID_ATTR;

    /**
     * <p>
     * The {@link Application}instance for this web application.
     * </p>
     */
    private Application application = null;

    /**
     * <p>
     * Factory for {@link FacesContext}instances.
     * </p>
     */
    private FacesContextFactory facesContextFactory = null;

    /**
     * <p>
     * The {@link Lifecycle}instance to use for request processing.
     * </p>
     */
    private Lifecycle lifecycle = null;

    /**
     * <p>
     * The <code>PortletConfig</code> instance for this portlet.
     * </p>
     */
    private PortletConfig portletConfig = null;

    /**
     * <p>
     * Release all resources acquired at startup time.
     * </p>
     */
    public void destroy()
    {
        if (log.isTraceEnabled())
        {
            log.trace("Begin FacesPortlet.destory() ");
        }
        application = null;
        facesContextFactory = null;
        lifecycle = null;
        portletConfig = null;
        if (log.isTraceEnabled())
        {
            log.trace("End FacesPortlet.destory() ");
        }

    }

    /**
     * <p>
     * Acquire the factory instance we will require.
     * </p>
     * 
     * @exception PortletException if, for any reason, the startp of this Faces
     *                application failed. This includes errors in the config
     *                file that is parsed before or during the processing of
     *                this <code>init()</code> method.
     */
    public void init(PortletConfig portletConfig) throws PortletException
    {

        if (log.isTraceEnabled())
        {
            log.trace("Begin FacesPortlet.init() ");
        }

        super.init(portletConfig);

        // Save our PortletConfig instance
        this.portletConfig = portletConfig;
        this.defaultViewPage = portletConfig.getInitParameter(PARAM_VIEW_PAGE);
        this.defaultEditPage = portletConfig.getInitParameter(PARAM_EDIT_PAGE);
        this.defaultHelpPage = portletConfig.getInitParameter(PARAM_HELP_PAGE);
        
        if (null == this.defaultViewPage)
        {
            // A Faces Portlet is required to have at least the
            // defaultViewPage
            // defined!
            throw new PortletException("Portlet " + portletConfig.getPortletName()
                    + " is incorrectly configured. No default View page is defined.");
        }
        if (null == this.defaultActionPage)
        {
            this.defaultActionPage = this.defaultViewPage;
        }
        if (null == this.defaultCustomPage)
        {
            this.defaultCustomPage = this.defaultViewPage;
        }
        if (null == this.defaultHelpPage)
        {
            this.defaultHelpPage = this.defaultViewPage;
        }
        if (null == this.defaultEditPage)
        {
            this.defaultEditPage = this.defaultViewPage;
        }
        if (log.isTraceEnabled())
        {
            log.trace("End FacesPortlet.init() ");
        }
    }

    /**
     * @see javax.portlet.GenericPortlet#doEdit(javax.portlet.RenderRequest,
     *      javax.portlet.RenderResponse)
     */
    public void doEdit(RenderRequest request, RenderResponse response) throws PortletException, IOException
    {
        process(request, response, defaultEditPage, FacesPortlet.EDIT_REQUEST, JSF_EDIT_ID);
    }

    /**
     * @see javax.portlet.GenericPortlet#doHelp(javax.portlet.RenderRequest,
     *      javax.portlet.RenderResponse)
     */
    public void doHelp(RenderRequest request, RenderResponse response) throws PortletException, IOException
    {
        if (this.defaultHelpPage != null && this.defaultHelpPage.endsWith(".html"))
        {
            super.doHelp(request, response);
        }
        else
        {
            process(request, response, defaultHelpPage, FacesPortlet.HELP_REQUEST, JSF_HELP_ID);
        }
    }

    /**
     * @param request The {@link RenderRequest}.
     * @param response The {@link RenderResponse}.
     * @throws PortletException Throws a {@link PortletException}.
     * @throws IOException Throws a {@link IOException}.
     */
    public void doCustom(RenderRequest request, RenderResponse response) throws PortletException, IOException
    {
        process(request, response, defaultCustomPage, FacesPortlet.CUSTOM_REQUEST, JSF_CUSTOM_ID);
    }

    /**
     * @see javax.portlet.GenericPortlet#doView(javax.portlet.RenderRequest,
     *      javax.portlet.RenderResponse)
     */
    public void doView(RenderRequest request, RenderResponse response) throws PortletException, IOException
    {
        process(request, response, defaultViewPage, FacesPortlet.VIEW_REQUEST, JSF_VIEW_ID);
    }

    /**
     * @see javax.portlet.Portlet#processAction(javax.portlet.ActionRequest,
     *      javax.portlet.ActionResponse)
     */
    public void processAction(ActionRequest request, ActionResponse response) throws PortletException, IOException
    {
        String viewId = JSF_CUSTOM_ID;
        if (request.getPortletMode().equals(PortletMode.VIEW))
        {
            viewId = JSF_VIEW_ID;            
        }
        else if (request.getPortletMode().equals(PortletMode.EDIT))
        {
            viewId = JSF_EDIT_ID;                        
        }
        else if (request.getPortletMode().equals(PortletMode.HELP))
        {
            viewId = JSF_HELP_ID;                        
        }
        process(request, response, defaultActionPage, FacesPortlet.ACTION_REQUEST, viewId);
    }

    /**
     * <p>
     * Gets the {@link FacesContextFactory}.
     * </p>
     * 
     * @return The {@link FacesContextFactory}.
     * @throws PortletException Throws a {@link PortletException}.
     */
    public FacesContextFactory getFacesContextFactory() throws PortletException
    {
        if (facesContextFactory != null)
        {
            return facesContextFactory;
        }
        try
        {
            facesContextFactory = (FacesContextFactory) FactoryFinder.getFactory(FactoryFinder.FACES_CONTEXT_FACTORY);
            if (log.isTraceEnabled())
            {
                log.trace("Retrieved facesContextFactory " + facesContextFactory);
            }
        }
        catch (FacesException e)
        {
            Throwable rootCause = e.getCause();
            if (rootCause == null)
            {
                throw e;
            }
            else
            {
                throw new PortletException(e.getMessage(), rootCause);
            }
        }
        return facesContextFactory;
    }

    /**
     * <p>
     * Get the faces life cycle.
     * </p>
     * 
     * @return The {@link Lifecycle}.
     * @throws PortletException Throws a {@link PortletException}.
     */
    public Lifecycle getLifecycle() throws PortletException
    {
        if (lifecycle != null)
        {
            return lifecycle;
        }
        try
        {
            LifecycleFactory lifecycleFactory = (LifecycleFactory) FactoryFinder
                    .getFactory(FactoryFinder.LIFECYCLE_FACTORY);
            if (log.isTraceEnabled())
            {
                log.trace("Retrieved lifecycleFactory " + lifecycleFactory);
            }
            String lifecycleId = portletConfig.getPortletContext().getInitParameter(LIFECYCLE_ID_ATTR);
            if (log.isDebugEnabled())
            {
                log.debug("lifecycleId " + lifecycleId);
            }
            if (lifecycleId == null)
            {
                lifecycleId = LifecycleFactory.DEFAULT_LIFECYCLE;
            }
            lifecycle = lifecycleFactory.getLifecycle(lifecycleId);
            if (log.isTraceEnabled())
            {
                log.trace("Retrieved lifecycle from lifecycleFactory " + lifecycle);
            }
        }
        catch (FacesException e)
        {
            Throwable rootCause = e.getCause();
            if (rootCause == null)
            {
                throw e;
            }
            else
            {
                throw new PortletException(e.getMessage(), rootCause);
            }
        }
        return lifecycle;
    }

    /**
     * <p>
     * Processes the request.
     * </p>
     * 
     * @param request The {@link PortletRequest}.
     * @param response The {@link PortletResponse}.
     * @param defaultPage The default page.
     * @param requestType The request type.
     * @throws PortletException Throws a {@link PortletException}.
     * @throws IOException Throws an {@link IOException}.
     */
    private void process(PortletRequest request, PortletResponse response, String defaultPage, String requestType, String viewId)
            throws PortletException, IOException
    {
        boolean actionRequest = (request instanceof ActionRequest);
        boolean renderRequest = (request instanceof RenderRequest);
        
        String defaultView = defaultPage;
        
        if (actionRequest)
        {
            log.trace("Begin FacesPortlet.processAction()");
        }

        
        // Acquire the FacesContext instance for this request
        FacesContext context = getFacesContextFactory().getFacesContext(
                portletConfig, 
                request, response, getLifecycle());

        // Restore view if available.
        setDefaultView(context, defaultPage, viewId);
        if (log.isTraceEnabled())
        {
            log.trace("Begin Executing phases");
        }

        preProcessFaces(context);
        
        // Execute the pre-render request processing lifecycle for this request
        try
        {
            if (actionRequest)
            {
                getLifecycle().execute(context);
                if (log.isTraceEnabled())
                {
                    log.trace("End Executing phases");
                }
                // The view should have been restore.
                // Pass it to the render request. 
                                
                request.getPortletSession().setAttribute(createViewRootKey(context, defaultPage, viewId), context.getViewRoot());
                ActionResponse actionResponse = (ActionResponse)response;
                                
                // actionResponse.setRenderParameter(viewId, context.getViewRoot().getViewId()); // get the navigation change
            }
            else if (renderRequest)
            {
                //    getLifecycle().execute(context);
                String vi = context.getViewRoot().getViewId();
                context.getApplication().getViewHandler().restoreView(context, vi);
                
                getLifecycle().render(context);
                if (log.isTraceEnabled())
                {
                    log.trace("End executing RenderResponse phase ");
                }
            }
            else
            {
                throw new PortletException("Request must be of type ActionRequest or RenderRequest");
            }            
            request.getPortletSession().setAttribute(viewId, context.getViewRoot().getViewId(), PortletSession.PORTLET_SCOPE);            
            
        }
        catch (FacesException e)
        {
            Throwable t = ((FacesException) e).getCause();
            if (t == null)
            {
                throw new PortletException(e.getMessage(), e);
            }
            else
            {
                if (t instanceof PortletException)
                {
                    throw ((PortletException) t);
                }
                else if (t instanceof IOException)
                {
                    throw ((IOException) t);
                }
                else
                {
                    throw new PortletException(t.getMessage(), t);
                }
            }
        }
        finally
        {
            // Release the FacesContext instance for this request
            context.release();
        }
        if (log.isTraceEnabled())
        {
            log.trace("End FacesPortlet.process()");
        }
    }

    protected void preProcessFaces(FacesContext context)
    {        
    }
    
    
    private String createViewRootKey(FacesContext context, String defaultView, String viewId)
    {
        PortletRequest portletRequest = (PortletRequest) context.getExternalContext().getRequest();
        // String view = portletRequest.getParameter(viewId);
        String view = (String)portletRequest.getPortletSession().getAttribute(viewId, PortletSession.PORTLET_SCOPE);
        
        if (view == null)
        {
            view = defaultView;
        }
        String key = VIEW_ROOT + ":" + getPortletName();
        UIViewRoot root = context.getViewRoot();
        if (root != null)
        {
           key = key + ":" + root.getViewId();
        }
        else
        {
            key = key + ":" + view;
        }
        return key;
    }
    
    /**
     * <p>
     * Set the view identifier to the view for the page to be rendered.
     * </p>
     * 
     * @param context The {@link FacesContext}for the current request.
     * @param defaultView The default view identifier.
     * @return The default view.
     */
    private void setDefaultView(FacesContext facesContext, String defaultView, String viewId)
    {
        // Need to be able to transport viewId between actionRequest and
        // renderRequest.
        PortletRequest portletRequest = (PortletRequest) facesContext.getExternalContext().getRequest();
        if (portletRequest instanceof ActionRequest)
        {
            String view = (String)portletRequest.getPortletSession().getAttribute(viewId, PortletSession.PORTLET_SCOPE);
            
            if ((null != facesContext.getViewRoot()) && (null != facesContext.getViewRoot().getViewId()))
            {
                defaultView = facesContext.getViewRoot().getViewId();
            }
            //else if (null != portletRequest.getParameter(viewId))
            else if (null != view)
            {
                //defaultView = portletRequest.getParameter(viewId);
                defaultView = view;
            }
            
            UIViewRoot viewRoot = (UIViewRoot)portletRequest.
                                    getPortletSession().
                                    getAttribute(createViewRootKey(facesContext, defaultView, viewId));
            if (viewRoot != null)
            {
                facesContext.setViewRoot(viewRoot);
                defaultView = facesContext.getViewRoot().getViewId();
            }
            
            portletRequest.setAttribute(REQUEST_SERVLET_PATH, defaultView.replaceAll("[.]jsp", ".jsf"));
        }
        else if (portletRequest instanceof RenderRequest)
        {
            // String view = portletRequest.getParameter(viewId);
            String view = (String)portletRequest.getPortletSession().getAttribute(viewId, PortletSession.PORTLET_SCOPE);
            
            if (null == facesContext.getViewRoot())
            {                
                if (view == null)
                {
                    view = defaultView;
                }
                UIViewRoot viewRoot = (UIViewRoot)portletRequest.
                                        getPortletSession().
                                        getAttribute(createViewRootKey(facesContext, view, viewId));
                if (null != viewRoot)
                {
                    facesContext.setViewRoot(viewRoot);
                    defaultView = facesContext.getViewRoot().getViewId();
                }
                else
                {
                    facesContext.setViewRoot(new UIViewRoot());
                    facesContext.getViewRoot().setViewId(view);
                    facesContext.getViewRoot().setRenderKitId(RenderKitFactory.HTML_BASIC_RENDER_KIT);
                    portletRequest.getPortletSession().setAttribute(createViewRootKey(facesContext, view, viewId), viewRoot);
                }                   
            }
            portletRequest.setAttribute(REQUEST_SERVLET_PATH, view.replaceAll(".jsp", ".jsf"));
        }
        
    }
}