/*
 * 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.
 */
package org.apache.myfaces.orchestra.lib.jsf;

import java.util.LinkedList;
import java.util.ListIterator;
import java.util.Map;

import javax.faces.FacesException;
import javax.faces.context.FacesContext;
import javax.faces.context.FacesContextFactory;
import javax.faces.lifecycle.Lifecycle;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

/**
 * Setup some aspects of the Orchestra framework whenever a JSF request is being processed.
 * <p>
 * The Orchestra jarfile contains a faces-config.xml file that is automatically loaded by
 * the FacesServlet. It defines this class as the factory that servlet uses to create a
 * FacesContext object for each request.
 * <p>
 * That factory method is used here as a convenient point to initialize any per-request
 * Orchestra data-structures. Note that this (of course) only initializes Orchestra for
 * <i>JSF requests</i>; Orchestra is intended to support non-jsf functionality too (eg
 * plain jsp or servlets), in which case the appropriate initialization for that environment
 * needs to be configured via some other mechanism.
 * <p>
 * This factory fetches the actual FacesContext object from the previous factory in the
 * chain, then decorates the returned FacesContext object; this means that this class
 * integrates fine with other libraries that also configure a custom FacesContextFactory. 
 * 
 * @since 1.1
 */
public class OrchestraFacesContextFactory extends FacesContextFactory
{
    private final Log log = LogFactory.getLog(OrchestraFacesContextFactory.class);
    private final FacesContextFactory original;
    private PortletOrchestraFacesContextFactory portletOrchestraFacesContextFactory;

    public OrchestraFacesContextFactory(FacesContextFactory original)
    {
        this.original = original;
    }
    
    public FacesContext getFacesContext(
            final Object context, 
            final Object request, 
            final Object response, 
            final Lifecycle lifecycle) throws FacesException
    {
        log.debug("getFacesContext: entry");
        final FacesContext facesContext = original.getFacesContext(context, request, response, lifecycle);
        if (facesContext == null)
        {
            // should not happen
            return null;
        }

        if (!ExternalContextUtils.getRequestType(context, request).isPortlet())
        {
            // The handlers need to be reset on every request, as each request has a different
            // url which could potentially affect the list of handlers for that request.
            final LinkedList handlers = new LinkedList();
            handlers.add(new FrameworkAdapterRequestHandler());
            handlers.add(new ContextLockRequestHandler());
            handlers.add(new ConversationManagerRequestHandler());
            handlers.add(new DataSourceLeakRequestHandler());
    
            // Add any other handlers registered by filters or similar
            Map reqScope = facesContext.getExternalContext().getRequestMap();
            handlers.addAll(ConfigUtils.getRequestHandlers(reqScope));
    
            // Create and return the custom instance. Note that install=false
            // is specified for the FacesContextWrapper constructor, meaning
            // that the wrapper does not bind itself to the current thread:
            // the original object will still be the one that is returned
            // by FacesContext.getCurrentInstance(), not this wrapper. What 
            // is important here is that the FacesServlet calls the release 
            // method on this particular object, and that will happen because
            // FacesServlet uses the return value from this method as the object
            // to call release on, even though it is not the "current instance".
            // Not making the wrapper the current instance saves a little bit
            // of performance..
            log.debug("getFacesContext: creating custom instance");
            return new _FacesContextWrapper(facesContext, false)
            {
                // Constructor. Note that the parent constructor runs first,
                // which means that FacesContext.currentInstance is valid
                // at the time that the RequestHandler objects run.
                {
                    log.debug("getFacesContext: running inner constructor");
                    ListIterator i = handlers.listIterator();
                    try
                    {
                        while(i.hasNext())
                        {
                            RequestHandler h = (RequestHandler) i.next();
                            
                            if (log.isDebugEnabled())
                            {
                                log.debug("Running inithandler of type " + h.getClass().getName());
                            }
    
                            h.init(facesContext);
                        }
                    }
                    catch(RuntimeException e)
                    {
                        // Oops, something went wrong. Undo any processing done by the
                        // RequestHandlers that have successfully run so far.
                        //
                        // Warning:: this tries to run deinit on the object that just
                        // failed to initialise. RequestHandler classes should be written
                        // to correctly handle this.
                        log.error("Problem initialising RequestHandler", e);
                        _release(i);
                        throw e;
                    }
                }
    
                public void release()
                {
                    // As the "setup" code for this inner class runs after the
                    // parent class constructor, the release code for this
                    // class should run before the parent release. This also
                    // ensures that FacesContext.currentInstance() is still
                    // valid when the RequestHandler objects run.
                    log.debug("Running release");
                    
                    // Here, run the registered RequestHandlers in reverse order.
                    // Unfortunately, there is no ReverseListIterator class, so
                    // instead here we wind an iterator forward to the end of the
                    // list before passing it to _release, which then walks
                    // backwards through the list. Ecch.
                    ListIterator i = handlers.listIterator();
                    while (i.hasNext())
                    {
                        i.next();
                    }
                    _release(i);
    
                    // And invoke the parent release (which will invoke release
                    // on the target instance that this object decorates).
                    log.debug("Release completed");
                    super.release();
                }
                
                private void _release(ListIterator i)
                {
                    while (i.hasPrevious())
                    {
                        try
                        {
                            RequestHandler h = (RequestHandler) i.previous();
    
                            if (log.isDebugEnabled())
                            {
                                log.debug("Running deinithandler of type " + h.getClass().getName());
                            }
    
                            h.deinit();
                        }
                        catch(Exception e)
                        {
                            // ignore errors, so we always deinitialise anything
                            // that we initialised.
                            log.error("Problem deinitialising RequestHandler", e);
                        }
                    }
                }
            };
        }
        else
        {
            if (portletOrchestraFacesContextFactory == null)
            {
                portletOrchestraFacesContextFactory = new PortletOrchestraFacesContextFactory();
            }
            return portletOrchestraFacesContextFactory.getFacesContext(
                    facesContext, context, request, response);
        }
    }
}
