/*
 * Copyright 2001-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.axis.handlers;

import org.apache.axis.AxisEngine;
import org.apache.axis.AxisFault;
import org.apache.axis.Constants;
import org.apache.axis.Message;
import org.apache.axis.MessageContext;
import org.apache.axis.components.logger.LogFactory;
import org.apache.axis.message.SOAPEnvelope;
import org.apache.axis.message.SOAPHeaderElement;
import org.apache.axis.session.SimpleSession;
import org.apache.axis.utils.Messages;
import org.apache.axis.utils.SessionUtils;
import org.apache.commons.logging.Log;

import javax.xml.namespace.QName;
import javax.xml.rpc.server.ServiceLifecycle;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;

/** This handler uses SOAP headers to do simple session management.
 *
 * <p>Essentially, you install it on both the request and response chains of
 * your service, on both the client and the server side.</p>
 *
 * <p>ON THE SERVER:</p>
 * <ul>
 * <li>The REQUEST is checked for a session ID header.  If present, we
 *     look up the correct SimpleSession.  If not, we create a new session.
 *     In either case, we install the session into the MessageContext, and
 *     put its ID in the SESSION_ID property.
 * <li>The RESPONSE gets a session ID header tacked on, assuming we found a
 *     SESSION_ID property in the MessageContext.
 * </ul>
 * <p>ON THE CLIENT:</p>
 * <ul>
 * <li>The RESPONSE messages are checked for session ID headers.  If present,
 *     we pull the ID out and insert it into an option in the AxisClient.
 *     This works because a given Call object is associated with a single
 *     AxisClient.  However, we might want to find a way to put it into the
 *     Call object itself, which would make a little more sense.  This would
 *     mean being able to get to the Call from the MC, i.e. adding a getCall()
 *     API (which would only work on the client side)....
 * <li>When REQUESTS are generated, we look to see if an ID option is present
 *     in the AxisClient associated with the MessageContext.  If so, we
 *     insert a session ID header with the appropriate ID.
 * </ul>
 *
 * <p>SimpleSessions are "reaped" periodically via a very simplistic
 * mechanism.  Each time the handler is invoke()d we check to see if more
 * than <b>reapPeriodicity</b> milliseconds have elapsed since the last
 * reap.  If so, we walk the collection of active Sessions, and for each
 * one, if it hasn't been "touched" (i.e. had a getProperty() or setProperty()
 * performed) in longer than its timeout, we remove it from the collection.</p>
 *
 * @author Glen Daniels (gdaniels@apache.org)
 */
public class SimpleSessionHandler extends BasicHandler
{
    protected static Log log =
        LogFactory.getLog(SimpleSessionHandler.class.getName());

    public static final String SESSION_ID = "SimpleSession.id";
    public static final String SESSION_NS = "http://xml.apache.org/axis/session";
    public static final String SESSION_LOCALPART = "sessionID";
    public static final QName sessionHeaderName = new QName(SESSION_NS,
                                                            SESSION_LOCALPART);

    private Hashtable activeSessions = new Hashtable();

    // Reap timed-out sessions on the first request after this many
    // seconds.
    private long reapPeriodicity = 30;
    private long lastReapTime = 0;

    // By default, sessions time out after 1 minute of inactivity (60 sec)
    private int defaultSessionTimeout = 60;

    /**
     * Process a MessageContext.
     */
    public void invoke(MessageContext context) throws AxisFault
    {
        // Should we reap timed out sessions?
        long curTime = System.currentTimeMillis();
        boolean reap = false;
        
        // Minimize synchronicity, just check in here, do reap later.
        synchronized (this) {
            if (curTime > lastReapTime + (reapPeriodicity * 1000)) {
                reap = true;
                lastReapTime = curTime;
            }
        }
        
        if (reap) {
            Set entries = activeSessions.entrySet();
            Set victims = new HashSet();
            Object key;
            Iterator i;
            for (i = entries.iterator(); i.hasNext();) {
                Map.Entry entry = (Map.Entry) i.next();
                key = entry.getKey();
                SimpleSession session = (SimpleSession) entry.getValue();
                if ((curTime - session.getLastAccessTime()) >
                     (session.getTimeout() * 1000)) {
                    log.debug(Messages.getMessage("timeout00",
                                                        key.toString()));

                    // Don't modify the hashtable while we're iterating.
                    victims.add(key);
                }
            }

            // Now go remove all the victims we found during the iteration.
            for (i = victims.iterator(); i.hasNext();) {
                key = i.next();
                SimpleSession session = (SimpleSession)activeSessions.get(key);
                activeSessions.remove(key);

                // For each victim, swing through the data looking for
                // ServiceLifecycle objects, and calling destroy() on them.
                // FIXME : This cleanup should probably happen on another
                //         thread, as it might take a little while.
                Enumeration keys = session.getKeys();
                while (keys != null && keys.hasMoreElements()) {
                    String keystr = (String)keys.nextElement();
                    Object obj = session.get(keystr);
                    if (obj != null && obj instanceof ServiceLifecycle) {
                        ((ServiceLifecycle)obj).destroy();
                    }
                }
            }
        }
        
        if (context.isClient()) {
            doClient(context);
        } else {
            doServer(context);
        }
    }

    /**
     * Client side of processing.
     */
    public void doClient(MessageContext context) throws AxisFault
    {
        if (context.getPastPivot()) {
            // This is a response.  Check it for the session header.
            Message msg = context.getResponseMessage();
            if (msg == null)
                return;
            SOAPEnvelope env = msg.getSOAPEnvelope();
            SOAPHeaderElement header = env.getHeaderByName(SESSION_NS,
                                                           SESSION_LOCALPART);
            if (header == null)
                return;
            
            // Got one!
            try {
                Long id = (Long)header.
                             getValueAsType(Constants.XSD_LONG);
                // Store it away.
                AxisEngine engine = context.getAxisEngine();
                engine.setOption(SESSION_ID, id);
                // Note that we processed this header!
                header.setProcessed(true);
            } catch (Exception e) {
                throw AxisFault.makeFault(e);
            }
        } else {
            AxisEngine engine = context.getAxisEngine();
            Long id = (Long)engine.getOption(SESSION_ID);
            if (id == null)
                return;
            
            // We have a session ID, so insert the header
            Message msg = context.getRequestMessage();
            if (msg == null)
                throw new AxisFault(Messages.getMessage("noRequest00"));
            
            SOAPEnvelope env = msg.getSOAPEnvelope();
            SOAPHeaderElement header = new SOAPHeaderElement(SESSION_NS,
                                                             SESSION_LOCALPART,
                                                             id);
            env.addHeader(header);
        }
    }

    /**
     * Server side of processing.
     */
    public void doServer(MessageContext context) throws AxisFault
    {
        if (context.getPastPivot()) {
            // This is a response.  Add the session header if we have an
            // ID.
            Long id = (Long)context.getProperty(SESSION_ID);
            if (id == null)
                return;
            
            Message msg = context.getResponseMessage();
            if (msg == null)
                return;
            SOAPEnvelope env = msg.getSOAPEnvelope();
            SOAPHeaderElement header = new SOAPHeaderElement(SESSION_NS,
                                                             SESSION_LOCALPART,
                                                             id);
            env.addHeader(header);
        } else {
            // Request.  Set up the session if we find the header.
            Message msg = context.getRequestMessage();
            if (msg == null)
                throw new AxisFault(Messages.getMessage("noRequest00"));
            
            SOAPEnvelope env = msg.getSOAPEnvelope();
            SOAPHeaderElement header = env.getHeaderByName(SESSION_NS,
                                                           SESSION_LOCALPART);
            Long id;
            
            if (header != null) {
                // Got one!
                try {
                    id = (Long)header.
                            getValueAsType(Constants.XSD_LONG);
                } catch (Exception e) {
                    throw AxisFault.makeFault(e);
                }
            } else {
                id = getNewSession();
            }
            
            SimpleSession session = (SimpleSession)activeSessions.get(id);
            if (session == null) {
                // Must have timed out, get a new one.
                id = getNewSession();
                session = (SimpleSession)activeSessions.get(id);
            }

            // This session is still active...
            session.touch();
            
            // Store it away in the MessageContext.
            context.setSession(session);
            context.setProperty(SESSION_ID, id);
        }
    }
    
    /**
     * Generate a new session, register it, and return its ID.
     *
     * @return the new session's ID for later lookup.
     */
    private synchronized Long getNewSession()
    {
        Long id = SessionUtils.generateSession();
        SimpleSession session = new SimpleSession();
        session.setTimeout(defaultSessionTimeout);
        activeSessions.put(id, session);
        return id;
    }

    /**
     * Set the reaper periodicity in SECONDS
     *
     * Convenience method for testing.
     *
     * !!! TODO: Should be able to set this via options on the Handler
     * or perhaps the engine.
     */
    public void setReapPeriodicity(long reapTime)
    {
        reapPeriodicity = reapTime;
    }

    /**
     * Set the default session timeout in SECONDS
     *
     * Again, for testing.
     */
    public void setDefaultSessionTimeout(int defaultSessionTimeout) {
        this.defaultSessionTimeout = defaultSessionTimeout;
    }
}
