/*
 * 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.catalina.valves;


import java.io.IOException;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;

import javax.servlet.ServletException;
import javax.servlet.http.HttpSession;
import javax.servlet.http.HttpSessionEvent;
import javax.servlet.http.HttpSessionListener;

import org.apache.catalina.Context;
import org.apache.catalina.Lifecycle;
import org.apache.catalina.LifecycleEvent;
import org.apache.catalina.LifecycleException;
import org.apache.catalina.LifecycleListener;
import org.apache.catalina.LifecycleState;
import org.apache.catalina.comet.CometEvent;
import org.apache.catalina.comet.CometProcessor;
import org.apache.catalina.connector.CometEventImpl;
import org.apache.catalina.connector.Request;
import org.apache.catalina.connector.Response;


/**
 * <p>Implementation of a Valve that tracks Comet connections, and closes them
 * when the associated session expires or the webapp is reloaded.</p>
 *
 * <p>This Valve should be attached to a Context.</p>
 *
 * @author Remy Maucherat
 */
public class CometConnectionManagerValve extends ValveBase
    implements HttpSessionListener, LifecycleListener {

    //------------------------------------------------------ Constructor
    public CometConnectionManagerValve() {
        super(false);
    }


    // ----------------------------------------------------- Instance Variables

    /**
     * List of current Comet connections.
     */
    protected final List<Request> cometRequests =
        Collections.synchronizedList(new ArrayList<Request>());


    /**
     * Name of session attribute used to store list of comet connections.
     */
    protected final String cometRequestsAttribute =
        "org.apache.tomcat.comet.connectionList";


    /**
     * Start this component and implement the requirements
     * of {@link org.apache.catalina.util.LifecycleBase#startInternal()}.
     *
     * @exception LifecycleException if this component detects a fatal error
     *  that prevents this component from being used
     */
    @Override
    protected synchronized void startInternal() throws LifecycleException {

        if (container instanceof Context) {
            container.addLifecycleListener(this);
        }

        setState(LifecycleState.STARTING);
    }


    /**
     * Stop this component and implement the requirements
     * of {@link org.apache.catalina.util.LifecycleBase#stopInternal()}.
     *
     * @exception LifecycleException if this component detects a fatal error
     *  that prevents this component from being used
     */
    @Override
    protected synchronized void stopInternal() throws LifecycleException {

        setState(LifecycleState.STOPPING);

        if (container instanceof Context) {
            container.removeLifecycleListener(this);
        }
    }


    @Override
    public void lifecycleEvent(LifecycleEvent event) {
        if (Lifecycle.BEFORE_STOP_EVENT.equals(event.getType())) {
            // The container is getting stopped, close all current connections
            Iterator<Request> iterator = cometRequests.iterator();
            while (iterator.hasNext()) {
                Request request = iterator.next();
                // Remove the session tracking attribute as it isn't
                // serializable or required.
                HttpSession session = request.getSession(false);
                if (session != null) {
                    session.removeAttribute(cometRequestsAttribute);
                }
                // Close the comet connection
                CometEventImpl cometEvent = request.getEvent();
                try {
                    cometEvent.setEventType(CometEvent.EventType.END);
                    cometEvent.setEventSubType(
                            CometEvent.EventSubType.WEBAPP_RELOAD);
                    getNext().event(request, request.getResponse(), cometEvent);
                } catch (Exception e) {
                    container.getLogger().warn(
                            sm.getString("cometConnectionManagerValve.event"),
                            e);
                } finally {
                    try {
                        cometEvent.close();
                    } catch (IOException e) {
                        container.getLogger().warn(sm.getString(
                                "cometConnectionManagerValve.event"), e);
                    }
                }
            }
            cometRequests.clear();
        }
    }


    // --------------------------------------------------------- Public Methods

    /**
     * Register requests for tracking, whenever needed.
     *
     * @param request The servlet request to be processed
     * @param response The servlet response to be created
     *
     * @exception IOException if an input/output error occurs
     * @exception ServletException if a servlet error occurs
     */
    @Override
    public void invoke(Request request, Response response)
        throws IOException, ServletException {
        // Perform the request
        getNext().invoke(request, response);

        if (request.isComet() && !response.isClosed()) {
            // Start tracking this connection, since this is a
            // begin event, and Comet mode is on
            HttpSession session = request.getSession(true);

            // Track the connection for webapp reload
            cometRequests.add(request);

            // Track the connection for session expiration
            synchronized (session) {
                ConnectionList list = (ConnectionList) session.getAttribute(
                        cometRequestsAttribute);
                Request[] requests = null;
                if (list != null) {
                    requests = list.get();
                }
                if (requests == null) {
                    requests = new Request[1];
                    requests[0] = request;
                    session.setAttribute(cometRequestsAttribute,
                            new ConnectionList(requests));
                } else {
                    Request[] newRequests =
                        new Request[requests.length + 1];
                    for (int i = 0; i < requests.length; i++) {
                        newRequests[i] = requests[i];
                    }
                    newRequests[requests.length] = request;
                    session.setAttribute(cometRequestsAttribute,
                            new ConnectionList(newRequests));
                }
            }
        }

    }


    /**
     * Use events to update the connection state.
     *
     * @param request The servlet request to be processed
     * @param response The servlet response to be created
     *
     * @exception IOException if an input/output error occurs
     * @exception ServletException if a servlet error occurs
     */
    @Override
    public void event(Request request, Response response, CometEvent event)
        throws IOException, ServletException {

        // Perform the request
        boolean ok = false;
        try {
            getNext().event(request, response, event);
            ok = true;
        } finally {
            if (!ok || response.isClosed()
                    || (event.getEventType() == CometEvent.EventType.END)
                    || (event.getEventType() == CometEvent.EventType.ERROR
                            && !(event.getEventSubType() ==
                                CometEvent.EventSubType.TIMEOUT))) {

                // Remove the connection from webapp reload tracking
                cometRequests.remove(request);

                // Remove connection from session expiration tracking
                // Note: can't get the session if it has been invalidated but
                // OK since session listener will have done clean-up
                HttpSession session = request.getSession(false);
                if (session != null) {
                    synchronized (session) {
                        Request[] reqs = null;
                        try {
                            ConnectionList list =
                                    (ConnectionList) session.getAttribute(
                                            cometRequestsAttribute);
                            if (list != null) {
                                reqs = list.get();
                            }
                        } catch (IllegalStateException ise) {
                            // Ignore - session has been invalidated
                            // Listener will have cleaned up
                        }
                        if (reqs != null) {
                            boolean found = false;
                            for (int i = 0; !found && (i < reqs.length); i++) {
                                found = (reqs[i] == request);
                            }
                            if (found) {
                                if (reqs.length > 1) {
                                    Request[] newConnectionInfos =
                                        new Request[reqs.length - 1];
                                    int pos = 0;
                                    for (int i = 0; i < reqs.length; i++) {
                                        if (reqs[i] != request) {
                                            newConnectionInfos[pos++] = reqs[i];
                                        }
                                    }
                                    try {
                                        session.setAttribute(
                                                cometRequestsAttribute,
                                                new ConnectionList(
                                                        newConnectionInfos));
                                    } catch (IllegalStateException ise) {
                                        // Ignore - session has been invalidated
                                        // Listener will have cleaned up
                                    }
                                } else {
                                    try {
                                        session.removeAttribute(
                                                cometRequestsAttribute);
                                    } catch (IllegalStateException ise) {
                                        // Ignore - session has been invalidated
                                        // Listener will have cleaned up
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }

    }


    @Override
    public void sessionCreated(HttpSessionEvent se) {
        // NOOP
    }


    @Override
    public void sessionDestroyed(HttpSessionEvent se) {
        // Close all Comet connections associated with this session
        ConnectionList list = (ConnectionList) se.getSession().getAttribute(
                cometRequestsAttribute);
        Request[] reqs = null;
        if (list != null) {
            reqs = list.get();
        }
        if (reqs != null) {
            for (int i = 0; i < reqs.length; i++) {
                Request req = reqs[i];
                try {
                    CometEventImpl event = req.getEvent();
                    event.setEventType(CometEvent.EventType.END);
                    event.setEventSubType(CometEvent.EventSubType.SESSION_END);
                    ((CometProcessor)
                            req.getWrapper().getServlet()).event(event);
                    event.close();
                } catch (Exception e) {
                    req.getWrapper().getParent().getLogger().warn(sm.getString(
                            "cometConnectionManagerValve.listenerEvent"), e);
                }
            }
        }
    }


    private static class ConnectionList implements Serializable {

        private static final long serialVersionUID = 1L;

        private transient Request[] connectionList = null;

        private ConnectionList(Request[] connectionList){
            this.connectionList = connectionList;
        }

        public Request[] get(){
            return connectionList;
        }
    }
}
