/*
 * 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.pluto.container.driver;

import java.io.IOException;
import java.util.Timer;
import java.util.TimerTask;

import javax.portlet.ActionRequest;
import javax.portlet.ActionResponse;
import javax.portlet.EventPortlet;
import javax.portlet.EventRequest;
import javax.portlet.EventResponse;
import javax.portlet.Portlet;
import javax.portlet.PortletConfig;
import javax.portlet.PortletException;
import javax.portlet.PortletRequest;
import javax.portlet.PortletResponse;
import javax.portlet.RenderRequest;
import javax.portlet.RenderResponse;
import javax.portlet.ResourceRequest;
import javax.portlet.ResourceResponse;
import javax.portlet.ResourceServingPortlet;
import javax.portlet.UnavailableException;
import javax.servlet.ServletConfig;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.pluto.container.FilterManager;
import org.apache.pluto.container.PortletContainerException;
import org.apache.pluto.container.PortletInvokerService;
import org.apache.pluto.container.PortletRequestContext;
import org.apache.pluto.container.PortletResponseContext;
import org.apache.pluto.container.PortletWindow;
import org.apache.pluto.container.om.portlet.PortletDefinition;

/**
 * Portlet Invocation Servlet. This servlet recieves cross context requests from
 * the the container and services the portlet request for the specified method.
 *
 * @version 1.1
 * @since 09/22/2004
 */
public class PortletServlet extends HttpServlet
{
    private static final long serialVersionUID = -5096339022539360365L;

    static class NullPortlet implements EventPortlet, ResourceServingPortlet, Portlet
    {
        public void processEvent(EventRequest arg0, EventResponse arg1)
        throws PortletException, IOException
        {
        }

        public void serveResource(ResourceRequest arg0, ResourceResponse arg1)
        throws PortletException, IOException
        {
        }

        public void destroy()
        {
        }

        public void init(PortletConfig arg0) throws PortletException
        {
        }

        public void processAction(ActionRequest arg0, ActionResponse arg1)
        throws PortletException, IOException
        {
        }

        public void render(RenderRequest arg0, RenderResponse arg1)
        throws PortletException, IOException
        {
        }
    }

    // Private Member Variables ------------------------------------------------
    /**
     * The portlet name as defined in the portlet app descriptor.
     */
    private String portletName;

    /**
     * The portlet instance wrapped by this servlet.
     */
    private Portlet portlet;

    /**
     * The internal portlet context instance.
     */
    private DriverPortletContext portletContext;

    /**
     * The internal portlet config instance.
     */
    private DriverPortletConfig portletConfig;

    /**
     * The Event Portlet instance (the same object as portlet) wrapped by this
     * servlet.
     */
    private EventPortlet eventPortlet;

    /** The resource serving portlet instance wrapped by this servlet. */
    private ResourceServingPortlet resourceServingPortlet;

    private PortletContextService contextService;

    private boolean started = false;
    Timer   startTimer;

    // HttpServlet Impl --------------------------------------------------------

    public String getServletInfo()
    {
        return "Pluto PortletServlet [" + portletName + "]";
    }

    /**
     * Initialize the portlet invocation servlet.
     *
     * @throws ServletException
     *             if an error occurs while loading portlet.
     */
    public void init(ServletConfig config) throws ServletException
    {

        // Call the super initialization method.
        super.init(config);

        // Retrieve portlet name as defined as an initialization parameter.
        portletName = getInitParameter("portlet-name");

        started = false;

        startTimer = new Timer(true);
        final ServletContext servletContext = getServletContext();
        final ClassLoader paClassLoader = Thread.currentThread().getContextClassLoader();
        startTimer.schedule(new TimerTask()
        {
            public void run()
            {
                synchronized(servletContext)
                {
                    if (startTimer != null)
                    {
                        if (attemptRegistration(servletContext, paClassLoader ))
                        {
                            startTimer.cancel();
                            startTimer = null;
                        }
                    }
                }
            }
        }, 1, 10000);
    }

    protected boolean attemptRegistration(ServletContext context, ClassLoader paClassLoader)
    {
        if (PlutoServices.getServices() != null)
        {
            contextService = PlutoServices.getServices().getPortletContextService();
            try
            {
                ServletConfig sConfig = getServletConfig();
                if (sConfig == null)
                {
                    String msg = "Problem obtaining servlet configuration(getServletConfig() returns null).";
                    context.log(msg);
                    return true;
                }

                String applicationName = contextService.register(sConfig);
                started = true;
                portletContext = contextService.getPortletContext(applicationName);
                portletConfig = contextService.getPortletConfig(applicationName, portletName);

            }
            catch (PortletContainerException ex)
            {
                context.log(ex.getMessage(),ex);
                return true;
            }

            PortletDefinition portletDD = portletConfig.getPortletDefinition();

            //          Create and initialize the portlet wrapped in the servlet.
            try
            {
                Class<?> clazz = paClassLoader.loadClass((portletDD.getPortletClass()));
                portlet = (Portlet) clazz.newInstance();
                portlet.init(portletConfig);
                initializeEventPortlet();
                initializeResourceServingPortlet();
                return true;
            }
            catch (Exception ex)
            {
                context.log(ex.getMessage(),ex);
                // take out of service
                portlet = null;
                portletConfig = null;
                return true;
            }
        }
        return false;
    }

    public void destroy()
    {
        synchronized(getServletContext())
        {
            if ( startTimer != null )
            {
                startTimer.cancel();
                startTimer = null;
            }
            else if ( started && portletContext != null)
            {
                started = false;
                contextService.unregister(portletContext);
                if (portlet != null)
                {
                    try
                    {
                        portlet.destroy();
                    }
                    catch (Exception e)
                    {
                        // ignore
                    }
                    portlet = null;
                }
            }
            super.destroy();
        }
    }

    protected void doGet(HttpServletRequest request,
            HttpServletResponse response) throws ServletException, IOException
            {
        dispatch(request, response);
            }

    protected void doPost(HttpServletRequest request,
            HttpServletResponse response) throws ServletException, IOException
            {
        dispatch(request, response);
            }

    protected void doPut(HttpServletRequest request,
            HttpServletResponse response) throws ServletException, IOException
            {
        dispatch(request, response);
            }

    // Private Methods ---------------------------------------------------------

    /**
     * Dispatch the request to the appropriate portlet methods. This method
     * assumes that the following attributes are set in the servlet request
     * scope:
     * <ul>
     * <li>METHOD_ID: indicating which method to dispatch.</li>
     * <li>PORTLET_REQUEST: the internal portlet request.</li>
     * <li>PORTLET_RESPONSE: the internal portlet response.</li>
     * </ul>
     *
     * @param request
     *            the servlet request.
     * @param response
     *            the servlet response.
     * @throws ServletException
     * @throws IOException
     */
    private void dispatch(HttpServletRequest request,
            HttpServletResponse response) throws ServletException, IOException
            {
        if (portlet == null)
        {
            throw new javax.servlet.UnavailableException("Portlet "+portletName+" unavailable");
        }

        // Retrieve attributes from the servlet request.
        Integer methodId = (Integer) request.getAttribute(PortletInvokerService.METHOD_ID);

        final PortletRequest portletRequest = (PortletRequest)request.getAttribute(PortletInvokerService.PORTLET_REQUEST);

        final PortletResponse portletResponse = (PortletResponse)request.getAttribute(PortletInvokerService.PORTLET_RESPONSE);

        final PortletRequestContext requestContext = (PortletRequestContext)portletRequest.getAttribute(PortletInvokerService.REQUEST_CONTEXT);
        final PortletResponseContext responseContext = (PortletResponseContext)portletRequest.getAttribute(PortletInvokerService.RESPONSE_CONTEXT);

        final FilterManager filterManager = (FilterManager)request.getAttribute(PortletInvokerService.FILTER_MANAGER);

        request.removeAttribute(PortletInvokerService.METHOD_ID);
        request.removeAttribute(PortletInvokerService.PORTLET_REQUEST);
        request.removeAttribute(PortletInvokerService.PORTLET_RESPONSE);
        request.removeAttribute(PortletInvokerService.FILTER_MANAGER);

        requestContext.init(portletConfig, getServletContext(), request, response);
        responseContext.init(request, response);

        PortletWindow window = requestContext.getPortletWindow();

        PortletInvocationEvent event = new PortletInvocationEvent(portletRequest, window, methodId.intValue());

        notify(event, true, null);

        // FilterManager filtermanager = (FilterManager) request.getAttribute(
        // "filter-manager");

        try
        {

            // The requested method is RENDER: call Portlet.render(..)
            if (methodId == PortletInvokerService.METHOD_RENDER)
            {
                RenderRequest renderRequest = (RenderRequest) portletRequest;
                RenderResponse renderResponse = (RenderResponse) portletResponse;
                filterManager.processFilter(renderRequest, renderResponse,
                        portlet, portletContext);
            }

            // The requested method is RESOURCE: call
            // ResourceServingPortlet.serveResource(..)
            else if (methodId == PortletInvokerService.METHOD_RESOURCE)
            {
                ResourceRequest resourceRequest = (ResourceRequest) portletRequest;
                ResourceResponse resourceResponse = (ResourceResponse) portletResponse;
                filterManager.processFilter(resourceRequest, resourceResponse,
                        resourceServingPortlet, portletContext);
            }

            // The requested method is ACTION: call Portlet.processAction(..)
            else if (methodId == PortletInvokerService.METHOD_ACTION)
            {
                ActionRequest actionRequest = (ActionRequest) portletRequest;
                ActionResponse actionResponse = (ActionResponse) portletResponse;
                filterManager.processFilter(actionRequest, actionResponse,
                        portlet, portletContext);
            }

            // The request methode is Event: call Portlet.processEvent(..)
            else if (methodId == PortletInvokerService.METHOD_EVENT)
            {
                EventRequest eventRequest = (EventRequest) portletRequest;
                EventResponse eventResponse = (EventResponse) portletResponse;
                filterManager.processFilter(eventRequest, eventResponse,
                        eventPortlet, portletContext);
            }
            // The requested method is ADMIN: call handlers.
            else if (methodId == PortletInvokerService.METHOD_ADMIN)
            {
                PortalAdministrationService pas = PlutoServices.getServices().getPortalAdministrationService();

                for (AdministrativeRequestListener l : pas.getAdministrativeRequestListeners())
                {
                    l.administer(portletRequest, portletResponse);
                }
            }

            // The requested method is LOAD: do nothing.
            else if (methodId == PortletInvokerService.METHOD_LOAD)
            {
                // Do nothing.
            }

            notify(event, false, null);

        }
        catch (UnavailableException ex)
        {
            //
            // if (e.isPermanent()) { throw new
            // UnavailableException(e.getMessage()); } else { throw new
            // UnavailableException(e.getMessage(), e.getUnavailableSeconds());
            // }
            //

            // Portlet.destroy() isn't called by Tomcat, so we have to fix it.
            try
            {
                portlet.destroy();
            }
            catch (Throwable th)
            {
                // Don't care for Exception
                this.getServletContext().log("Error during portlet destroy.", th);
            }
            // take portlet out of service
            portlet = null;

            // TODO: Handle everything as permanently for now.
            throw new javax.servlet.UnavailableException(ex.getMessage());

        }
        catch (PortletException ex)
        {
            notify(event, false, ex);
            throw new ServletException(ex);

        }
            }

    protected void notify(PortletInvocationEvent event, boolean pre, Throwable e)
    {
        PortalAdministrationService pas = PlutoServices.getServices().getPortalAdministrationService();

        for (PortletInvocationListener listener : pas.getPortletInvocationListeners())
        {
            if (pre)
            {
                listener.onBegin(event);
            }
            else if (e == null)
            {
                listener.onEnd(event);
            }
            else
            {
                listener.onError(event, e);
            }
        }
    }

    private void initializeEventPortlet()
    {
        if (portlet instanceof EventPortlet)
        {
            eventPortlet = (EventPortlet) portlet;
        }
        else
        {
            eventPortlet = new NullPortlet();
        }
    }

    private void initializeResourceServingPortlet()
    {
        if (portlet instanceof ResourceServingPortlet)
        {
            resourceServingPortlet = (ResourceServingPortlet) portlet;
        }
        else
        {
            resourceServingPortlet = new NullPortlet();
        }
    }
}
