/*
 * 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 jp.sf.pal.myfaces.portlet;

import java.io.IOException;

import javax.faces.FactoryFinder;
import javax.faces.application.Application;
import javax.faces.application.ApplicationFactory;
import javax.faces.application.ViewHandler;
import javax.faces.component.UIViewRoot;
import javax.faces.context.ExternalContext;
import javax.faces.context.FacesContext;
import javax.faces.context.FacesContextFactory;
import javax.faces.lifecycle.Lifecycle;
import javax.faces.lifecycle.LifecycleFactory;
import javax.faces.webapp.FacesServlet;
import javax.portlet.ActionRequest;
import javax.portlet.ActionResponse;
import javax.portlet.GenericPortlet;
import javax.portlet.PortletContext;
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 javax.portlet.UnavailableException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import jp.sf.pal.myfaces.portlet.fileupload.PortletFileUpload;
import jp.sf.pal.myfaces.portlet.headerresource.HeaderResource;
import jp.sf.pal.myfaces.portlet.headerresource.HeaderResourceFactory;
import jp.sf.pal.myfaces.portlet.selector.DefaultViewSelector;
import jp.sf.pal.myfaces.portlet.wrapper.HttpServletResponseWrapper;
import jp.sf.pal.myfaces.portlet.wrapper.MultipartRequestWrapper;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.myfaces.component.html.util.AddResource;
import org.apache.myfaces.component.html.util.ExtensionsResponseWrapper;
import org.apache.myfaces.config.FacesConfigurator;
import org.apache.myfaces.context.ReleaseableExternalContext;
import org.apache.myfaces.context.portlet.PortletExternalContextImpl;
import org.apache.myfaces.context.servlet.ServletFacesContextImpl;
import org.apache.myfaces.portlet.PortletUtil;
import org.apache.myfaces.webapp.webxml.WebXml;

/**
 * This portlet initializes MyFaces and converts portlet requests into
 * JSF requests.
 *
 * @author  <a href="mailto:shinsuke@yahoo.co.jp">Shinsuke Sugaya</a>
 * 
 * Base Information:
 * @author  Stan Silvert (latest modification by Author: ssilvert )
 * @version Revision: 279594
 */
public class MyFacesGenericPortlet extends GenericPortlet
{
    private static final Log log = LogFactory.getLog(MyFacesGenericPortlet.class);

    private static final String UPLOAD_REPOSITORY_PATH = "uploadRepositoryPath";

    private static final String UPLOAD_THRESHOLD_SIZE = "uploadThresholdSize";

    private static final String UPLOAD_MAX_FILE_SIZE = "uploadMaxFileSize";

    private static final String CURRENT_PORTLET_MODE = "CurrentPortletMode";

    // PortletRequest parameter
    public static final String VIEW_ID = MyFacesGenericPortlet.class.getName() + ".VIEW_ID";

    // PortletSession attribute
    protected static final String CURRENT_FACES_CONTEXT = MyFacesGenericPortlet.class.getName()
            + ".CURRENT_FACES_CONTEXT";

    // portlet config parameter from portlet.xml
    protected static final String DEFAULT_VIEW = "default-view";

    // portlet config parameter from portlet.xml
    protected static final String DEFAULT_VIEW_SELECTOR = "default-view-selector";

    protected static final String FACES_INIT_DONE = MyFacesGenericPortlet.class.getName() + ".FACES_INIT_DONE";

    protected PortletContext portletContext;

    protected FacesContextFactory facesContextFactory;

    protected Lifecycle lifecycle;

    protected String defaultView;

    protected DefaultViewSelector defaultViewSelector;

    private int uploadMaxFileSize = 100 * 1024 * 1024; // 10 MB

    private int uploadThresholdSize = 1 * 1024 * 1024; // 1 MB

    private String uploadRepositoryPath = null; // standard temp directory

    private HeaderResource headerResource;

    /**
     * Creates a new instance of MyFacesPortlet
     */
    public MyFacesGenericPortlet()
    {
    }

    /**
     * Portlet lifecycle.
     */
    public void destroy()
    {
        super.destroy();
        FactoryFinder.releaseFactories();
    }

    /**
     * Portlet lifecycle.
     */
    public void init() throws PortletException, UnavailableException
    {
        this.portletContext = getPortletContext();
        setDefaultView();
        setDefaultViewSelector();
        initMyFaces();

        facesContextFactory = (FacesContextFactory) FactoryFinder.getFactory(FactoryFinder.FACES_CONTEXT_FACTORY);

        // Javadoc says: Lifecycle instance is shared across multiple simultaneous requests, it must be
        // implemented in a thread-safe manner.  So we can acquire it here once:
        LifecycleFactory lifecycleFactory = (LifecycleFactory) FactoryFinder
                .getFactory(FactoryFinder.LIFECYCLE_FACTORY);
        lifecycle = lifecycleFactory.getLifecycle(getLifecycleId());

        // HeaderResource
        headerResource = HeaderResourceFactory.getHeaderResource(getPortletContext());
        if (headerResource == null)
        {
            log.error("headerResource is null. This portlet cannot probably put some tags into <head> element.");
        }

        // for inputFileUpload
        String param = getPortletConfig().getInitParameter(UPLOAD_MAX_FILE_SIZE);

        uploadMaxFileSize = resolveSize(param, uploadMaxFileSize);

        param = getPortletConfig().getInitParameter(UPLOAD_THRESHOLD_SIZE);

        uploadThresholdSize = resolveSize(param, uploadThresholdSize);

        uploadRepositoryPath = getPortletConfig().getInitParameter(UPLOAD_REPOSITORY_PATH);
    }

    private int resolveSize(String param, int defaultValue)
    {
        int numberParam = defaultValue;

        if (param != null)
        {
            param = param.toLowerCase();
            int factor = 1;
            String number = param;

            if (param.endsWith("g"))
            {
                factor = 1024 * 1024 * 1024;
                number = param.substring(0, param.length() - 1);
            }
            else if (param.endsWith("m"))
            {
                factor = 1024 * 1024;
                number = param.substring(0, param.length() - 1);
            }
            else if (param.endsWith("k"))
            {
                factor = 1024;
                number = param.substring(0, param.length() - 1);
            }

            numberParam = Integer.parseInt(number) * factor;
        }
        return numberParam;
    }

    protected void setDefaultView() throws UnavailableException
    {
        this.defaultView = getPortletConfig().getInitParameter(DEFAULT_VIEW);
        if (defaultView == null)
        {
            String msg = "Fatal: must specify a JSF view id as the default view in portlet.xml";
            throw new UnavailableException(msg);
        }
    }

    protected void setDefaultViewSelector() throws UnavailableException
    {
        String selectorClass = getPortletConfig().getInitParameter(DEFAULT_VIEW_SELECTOR);
        if (selectorClass == null)
            return;

        try
        {
            this.defaultViewSelector = (DefaultViewSelector) Class.forName(selectorClass).newInstance();
            this.defaultViewSelector.setPortletContext(getPortletContext());
            this.defaultViewSelector.setPortletConfig(getPortletConfig());
        }
        catch (Exception e)
        {
            log.error("Failed to load " + DEFAULT_VIEW_SELECTOR, e);
            throw new UnavailableException(e.getMessage());
        }
    }

    protected void setContentType(RenderRequest request, RenderResponse response)
    {

        if (response.getContentType() == null)
        {
            String portalPreferredContentType = request.getResponseContentType();
            if (portalPreferredContentType != null)
            {
                response.setContentType(portalPreferredContentType);
            }
            else
            {
                response.setContentType("text/html");
            }
        }
    }

    protected String getLifecycleId()
    {
        String lifecycleId = getPortletConfig().getInitParameter(FacesServlet.LIFECYCLE_ID_ATTR);
        return lifecycleId != null ? lifecycleId : LifecycleFactory.DEFAULT_LIFECYCLE;
    }

    protected void initMyFaces()
    {
        try
        {
            Boolean b = (Boolean) portletContext.getAttribute(FACES_INIT_DONE);

            if (b == null || b.booleanValue() == false)
            {
                log.trace("Initializing MyFaces");

                //Load the configuration
                ExternalContext externalContext = new PortletExternalContextImpl(portletContext, null, null);

                //And configure everything
                new FacesConfigurator(externalContext).configure();

                // parse web.xml - not sure if this is needed for portlet
                WebXml.init(externalContext);

                portletContext.setAttribute(FACES_INIT_DONE, Boolean.TRUE);
            }
            else
            {
                log.info("MyFaces already initialized");
            }
        }
        catch (Exception ex)
        {
            log.error("Error initializing MyFacesGenericPortlet", ex);
        }

        log.info("PortletContext '" + portletContext.getRealPath("/") + "' initialized.");
    }

    /**
     * Called by the portlet container to allow the portlet to process an action request.
     */
    public void processAction(ActionRequest request, ActionResponse response) throws PortletException, IOException
    {
        if (log.isTraceEnabled())
            log.trace("called processAction");

        if (sessionTimedOut(request))
            return;

        // Set PortletConfig
        request.setAttribute(MyFacesPortletParameters.PORTLET_CONFIG, getPortletConfig());

        // Check multipart/form-data
        if (PortletFileUpload.isMultipartContent(request))
        {
            log.info("PortletRequest is multipart content.");
            request = new MultipartRequestWrapper(request, uploadMaxFileSize, uploadThresholdSize, uploadRepositoryPath);
        }

        setPortletRequestFlag(request);

        FacesContext facesContext = facesContext(request, response);

        try
        {
            lifecycle.execute(facesContext);

            if (!facesContext.getResponseComplete())
            {
                response.setRenderParameter(VIEW_ID, facesContext.getViewRoot().getViewId());
            }

            request.getPortletSession().setAttribute(CURRENT_FACES_CONTEXT, facesContext);
        }
        catch (Throwable e)
        {
            facesContext.release();
            handleExceptionFromLifecycle(e);
        }
    }

    protected void handleExceptionFromLifecycle(Throwable e) throws PortletException, IOException
    {
        logException(e, null);

        if (e instanceof IOException)
        {
            throw (IOException) e;
        }

        if (e instanceof PortletException)
        {
            throw (PortletException) e;
        }

        if (e.getMessage() != null)
        {
            throw new PortletException(e.getMessage(), e);
        }

        throw new PortletException(e);
    }

    /**
     * Helper method to serve up the view mode.
     */
    protected void doView(RenderRequest request, RenderResponse response) throws PortletException, IOException
    {
        facesRender(request, response);
    }

    /**
     * Helper method to serve up the edit mode.  Can be overridden to add
     * the edit mode concept to a JSF application.
     */
    protected void doEdit(RenderRequest request, RenderResponse response) throws PortletException, IOException
    {
        facesRender(request, response);
    }

    /**
     * Helper method to serve up the edit mode.  Can be overridden to add
     * the help mode concept to a JSF application.
     */
    protected void doHelp(RenderRequest request, RenderResponse response) throws PortletException, IOException
    {
        facesRender(request, response);
    }

    /**
     * This method follows JSF Spec section 2.1.1.  It renders the default view from a non-faces
     * request.
     *
     * @param request The portlet render request.
     * @param response The portlet render response.
     */
    protected void nonFacesRequest(RenderRequest request, RenderResponse response) throws PortletException
    {
        nonFacesRequest(request, response, selectDefaultView(request, response));
    }

    /**
     * This method follows JSF Spec section 2.1.1.  It renders a view from a non-faces
     * request.  This is useful for a default view as well as for views that need to
     * be rendered from the portlet's edit and help buttons.
     *
     * @param request The portlet render request.
     * @param response The portlet render response.
     * @param view The name of the view that needs to be rendered.
     */
    protected void nonFacesRequest(RenderRequest request, RenderResponse response, String view) throws PortletException
    {
        if (log.isTraceEnabled())
            log.trace("Non-faces request: contextPath = " + request.getContextPath());
        setContentType(request, response); // do this in case nonFacesRequest is called by a subclass
        ApplicationFactory appFactory = (ApplicationFactory) FactoryFinder
                .getFactory(FactoryFinder.APPLICATION_FACTORY);
        Application application = appFactory.getApplication();
        ViewHandler viewHandler = application.getViewHandler();
        FacesContext facesContext = facesContext(request, response);
        UIViewRoot viewRoot = viewHandler.createView(facesContext, view);
        viewRoot.setViewId(view);
        facesContext.setViewRoot(viewRoot);
        lifecycle.render(facesContext);
    }

    protected String selectDefaultView(RenderRequest request, RenderResponse response) throws PortletException
    {
        String view = this.defaultView;
        if (this.defaultViewSelector != null)
        {
            String selectedView = this.defaultViewSelector.selectViewId(request, response);
            if (selectedView != null)
            {
                view = selectedView;
            }
        }

        return view;
    }

    protected FacesContext facesContext(PortletRequest request, PortletResponse response)
    {
        return facesContextFactory.getFacesContext(portletContext, request, response, lifecycle);
    }

    protected ReleaseableExternalContext makeExternalContext(PortletRequest request, PortletResponse response)
    {
        return (ReleaseableExternalContext) new PortletExternalContextImpl(portletContext, request, response);
    }

    protected boolean sessionTimedOut(PortletRequest request)
    {
        return request.getPortletSession(false) == null;
    }

    protected void setPortletRequestFlag(PortletRequest request)
    {
        request.getPortletSession().setAttribute(PortletUtil.PORTLET_REQUEST_FLAG, "true");
    }

    /**
     * Render a JSF view.
     */
    protected void facesRender(RenderRequest request, RenderResponse response) throws PortletException,
            java.io.IOException
    {
        // Set PortletConfig
        request.setAttribute(MyFacesPortletParameters.PORTLET_CONFIG, getPortletConfig());

        baseFacesRender(request, response);

        if (headerResource != null)
        {
            HttpServletResponseWrapper responseWrapper = new HttpServletResponseWrapper((HttpServletResponse) response);
            ExtensionsResponseWrapper exResponse = new ExtensionsResponseWrapper((HttpServletResponse) responseWrapper);
            exResponse.getWriter().write("<head></head>");
            exResponse.finishResponse();
            if (log.isDebugEnabled())
            {
                log.debug("facesRender(RenderRequest, RenderResponse) -" + " exResponse.toString()="
                        + exResponse.toString());
            }

            //TODO should not use HttpServletRequest(AddResource was refactered after 1.1.1...)
            AddResource.writeWithFullHeader((HttpServletRequest) request, exResponse, responseWrapper);

            String headerString = responseWrapper.getStringWriter().toString();
            headerResource.addHeaderResources(request, response, headerString);
        }
    }

    /**
     * Render a JSF view.
     */
    protected void baseFacesRender(RenderRequest request, RenderResponse response) throws PortletException,
            java.io.IOException
    {
        if (log.isTraceEnabled())
            log.trace("called facesRender");

        // Check the portlet mode
        PortletSession session = request.getPortletSession();
        PortletMode mode = (PortletMode) session.getAttribute(CURRENT_PORTLET_MODE);

        if (mode == null)
        {
            mode = request.getPortletMode();
        }

        boolean isPortletModeChanged = false;
        if (mode != request.getPortletMode())
        {
            isPortletModeChanged = true;
        }

        session.setAttribute(CURRENT_PORTLET_MODE, request.getPortletMode());

        // If the portlet mode is changed..
        if (isPortletModeChanged)
        {
            nonFacesRequest(request, response);
            return;
        }

        setContentType(request, response);

        String viewId = request.getParameter(VIEW_ID);
        if (log.isDebugEnabled())
        {
            log.debug("baseFacesRender(RenderRequest, RenderResponse) -  : viewId=" + viewId);
        }

        if ((viewId == null) || sessionTimedOut(request)
                || request.getPortletSession().getAttribute(CURRENT_FACES_CONTEXT) == null)
        {
            setPortletRequestFlag(request);
            nonFacesRequest(request, response);
            return;
        }

        setPortletRequestFlag(request);

        try
        {
            ServletFacesContextImpl facesContext = (ServletFacesContextImpl) request.getPortletSession().getAttribute(
                    CURRENT_FACES_CONTEXT);

            // TODO: not sure if this can happen.  Also double check this against spec section 2.1.3
            if (facesContext.getResponseComplete())
                return;

            facesContext.setExternalContext(makeExternalContext(request, response));
            lifecycle.render(facesContext);
        }
        catch (Throwable e)
        {
            handleExceptionFromLifecycle(e);
        }
    }

    protected void logException(Throwable e, String msgPrefix)
    {
        String msg;
        if (msgPrefix == null)
        {
            if (e.getMessage() == null)
            {
                msg = "Exception in FacesServlet";
            }
            else
            {
                msg = e.getMessage();
            }
        }
        else
        {
            if (e.getMessage() == null)
            {
                msg = msgPrefix;
            }
            else
            {
                msg = msgPrefix + ": " + e.getMessage();
            }
        }

        portletContext.log(msg, e);

        Throwable cause = e.getCause();
        if (cause != null && cause != e)
        {
            logException(cause, "Root cause");
        }

        if (e instanceof PortletException)
        {
            cause = ((PortletException) e).getCause();

            if (cause != null && cause != e)
            {
                logException(cause, "Root cause of PortletException");
            }
        }
    }

}
