/* ======================================================================
The Bodington System Software License, Version 1.0
  
Copyright (c) 2001 The University of Leeds.  All rights reserved.
  
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:

1.  Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.

2.  Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.

3.  The end-user documentation included with the redistribution, if any,
must include the following acknowledgement:  "This product includes
software developed by the University of Leeds
(http://www.bodington.org/)."  Alternately, this acknowledgement may
appear in the software itself, if and wherever such third-party
acknowledgements normally appear.

4.  The names "Bodington", "Nathan Bodington", "Bodington System",
"Bodington Open Source Project", and "The University of Leeds" must not be
used to endorse or promote products derived from this software without
prior written permission. For written permission, please contact
d.gardner@leeds.ac.uk.

5.  The name "Bodington" may not appear in the name of products derived
from this software without prior written permission of the University of
Leeds.

THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
WARRANTIES, INCLUDING, BUT NOT LIMITED TO,  TITLE,  THE IMPLIED WARRANTIES 
OF QUALITY  AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.  IN NO 
EVENT SHALL THE UNIVERSITY OF LEEDS OR ITS CONTRIBUTORS BE LIABLE FOR 
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE 
GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 
HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, 
STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 
ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 
POSSIBILITY OF SUCH DAMAGE.
=========================================================

This software was originally created by the University of Leeds and may contain voluntary 
contributions from others.  For more information on the Bodington Open Source Project, please 
see http://bodington.org/

====================================================================== */

package org.bodington.servlet;

import org.bodington.server.*;
import org.bodington.server.resources.*;
import org.bodington.database.PrimaryKey;

import java.util.*;
import java.util.regex.*;
import org.apache.log4j.*;

import javax.servlet.http.*;
/**
 * @author  bmb6jrm
 */
public class HttpSession implements javax.servlet.http.HttpSession
{
    private static final Logger log = Logger.getLogger( HttpSession.class );
    
    
    private static Random random = new Random( System.currentTimeMillis() );
    private static Hashtable previous_session_ids = new Hashtable();
    private static Hashtable sessions_by_id = new Hashtable();
    private static Hashtable sessions_by_auth = new Hashtable();
    private static Hashtable sessions_by_cert = new Hashtable();
    
    private static Object lock = new Object();
    
    // this object is a thread that will dereference inactive sessions
    private static HouseKeeper housekeeper = null;
    
    // the number of active sessions last time housekeeper checked
    private static int active_sessions=0;
    
    private static Pattern has_screen_size_pattern = 
        Pattern.compile( "[0-9]*x[0-9]*" );
    private static Pattern screen_size_pattern = 
        Pattern.compile( "[0-9]*x[0-9]*" );
    private static Pattern integer_pattern = 
        Pattern.compile( "[0-9]*" );
    
    
    public static void shutdown()
    {
        log.info("Shutdown");
	synchronized ( lock )
	{
            if ( housekeeper!=null )
            {
                housekeeper.done = true;
                housekeeper.interrupt();
                housekeeper = null;
            }
            previous_session_ids = new Hashtable();
            sessions_by_id = new Hashtable();
            sessions_by_auth = new Hashtable();
            sessions_by_cert = new Hashtable();
        }
    }
    
    /**
     * Return all session ids.
     * Used for user monitoring
     */
    public static Iterator getAllSessions()
    {
    	return sessions_by_id.values().iterator();
    }
    
    public static HttpSession findSessionById( String Id )
    {
	return findSessionById( Id, true );
    }
    
    public static HttpSession findSessionByAuthenticationString( 
    String auth )
    {
	return findSessionByAuthenticationString( auth, true );
    }    

    public static HttpSession findSessionByCertificate( Object certificate )
    {
	return findSessionByCertificate( certificate, true );
    }    

    public static HttpSession findSessionById( String Id, boolean touch )
    {
	HttpSession session;
	synchronized ( lock )
	{
	    session = (HttpSession)sessions_by_id.get( Id );
	}
	if ( session!=null && touch  )
	    session.accessedNow();
	return session;
    }
    
    public static HttpSession findSessionByAuthenticationString( 
    String auth, boolean touch )
    {
	HttpSession session;
	synchronized ( lock )
	{
	    session = (HttpSession)sessions_by_auth.get( auth );
	}
	if ( session!=null && touch )
	    session.accessedNow();
	return session;
    }
    
    public static HttpSession findSessionByCertificate( 
    Object cert, boolean touch )
    {
	HttpSession session;
	synchronized ( lock )
	{
	    session = (HttpSession)sessions_by_cert.get( cert );
	}
	if ( session!=null && touch )
	    session.accessedNow();
	return session;
    }
    
    private static String nextSessionId()
    {
	long l1, l2, l3;
	String code;
	
	synchronized ( lock )
	{
	    do
	    {
		l1 = random.nextLong();
		l2 = random.nextLong();
		l3 = random.nextLong();
		code = Long.toHexString( l1 ) +
		Long.toHexString( l2 ) +
		Long.toHexString( l3 );
	    }
	    while ( previous_session_ids.containsKey( code ) );
	    previous_session_ids.put( code, code );
	}
	
	return code;
    }

    public static int getSessionCount()
    {
        return sessions_by_id.size();
    }
    
    
    private String id;
    private long creation_time, last_accessed_time;
    private int max_inactive_interval;
    private Hashtable attributes;
    private Map preferences;
    private boolean valid;
    private boolean is_new;
    private javax.servlet.ServletContext servlet_context;
    private PrimaryKey user_id;
    private String authentication_string;
    private Object certificate;
    private boolean small_screen;
    
    private String remoteHost = null;
    
    /** Creates new HttpSession */
    public HttpSession()
    {
	valid=true;
	is_new=true;
	id = nextSessionId();
	creation_time = System.currentTimeMillis();
	last_accessed_time = creation_time;
	// NOTE: whole seconds as per servlet spec.
	max_inactive_interval = 60*60; 
	user_id = null;
	attributes = new Hashtable();
	preferences = new HashMap();
        
	synchronized ( lock )
	{
	    sessions_by_id.put( this.getId(), this );
	}
	
	if ( housekeeper == null )
	    housekeeper = new HouseKeeper();
    }
    

    public void init( HttpServletRequest request )
    {
	String agent = request.getHeader( "user-agent" );
        small_screen = agentHasSmallScreen( agent );
        
        //cache the remote client host in the session.
        remoteHost = request.getRemoteAddr();        
    }
    
    public String getRemoteHost() { return remoteHost; }
    
    private boolean agentHasSmallScreen( String agent )
    {
        if ( agent == null )
            return false;
        Matcher matcher = has_screen_size_pattern.matcher( agent );
        if ( !matcher.find() )
            return false;
        matcher = screen_size_pattern.matcher( agent );
        if ( !matcher.find() )
            return false;
        String substring = matcher.group();
        matcher = integer_pattern.matcher( substring );
        if ( !matcher.find() )
            return false;
        String width_string = matcher.group();
        int width;
        try
        {
            width = Integer.parseInt( width_string );
        }
        catch ( NumberFormatException e )
        {
            return false;
        }
        return width < 400;
    }
    
    /**
     * Get a value for a preference.
     * This method caches the results that we get back from the NavigationSession.
     * @param key The key to lookup.
     * @return The value of the preference, null if it couldn't be found.
     */
    public String getPreference( String key )
    {
        String value = null;
        
        if (preferences.containsKey(key))
        {
            // If we have it cached lets use that value.
            value = (String)preferences.get(key);
        }
        else
        {
            try
            {
                // go to database through nav_session
                NavigationSession nav_session = this.getServerNavigationSession();
                // must be authenticated and a real person to use stored values
                if ( nav_session == null || !nav_session.isAuthenticated() || nav_session.isAnonymous() )
                    value = null;
                else
                    value = nav_session.getUserProperty( key );
                preferences.put( key, value );
            }
            catch ( Exception e )
            {
                log.error(e.getMessage(), e );
            }
        }
        return value;
    }

    
    public String getAutomaticPreferenceByScreenSize( String key, String small_default, String big_default )
    {
        String pref = getPreference( key );
        
        if ( pref == null || pref.length() == 0 )
        {
            pref = "automatic";
        }
        
        if ( !"automatic".equalsIgnoreCase( pref ) )
            return pref;
        
        return small_screen?small_default:big_default;
    }
    
    /**
     * Sets a preference for the current session. 
     * If there is a NavigationSession associated with this session and it is not
     * anonymous then store them there so they persist through logouts.
     * @param key The key to store the value against.
     * @param value The value to store.
     */
    public void setPreference( String key, String value )
    {
        try
        {
            // if real person store in database too
            NavigationSession nav_session = this.getServerNavigationSession();
            // must be authenticated and a real person to use stored values
            if ( nav_session == null || !nav_session.isAuthenticated() || nav_session.isAnonymous() )
                ;
            else
                nav_session.setUserProperty( key, value );
            // Always put the value in the local cache so anonymous people can change stuff.
            preferences.put( key, value );
        }
        catch ( Exception e )
        {
            log.error(e.getMessage(), e);
        }
    }

    
    
    public java.lang.Object getAttribute(java.lang.String str)
    {
	synchronized( attributes )
	{
	    return attributes.get( str );
	}
    }
    
    public java.util.Enumeration getAttributeNames()
    {
	synchronized( attributes )
	{
	    Vector list = new Vector();
	    Enumeration enumeration = attributes.keys();
	    String key;
	    while ( enumeration.hasMoreElements() )
	    {
		key = (String)enumeration.nextElement();
		list.addElement( key );
	    }
	    return list.elements();
	}
    }
    
    public long getCreationTime()
    {
	return creation_time;
    }
    
    public java.lang.String getId()
    {
	return id;
    }
    
    
    public void accessedNow()
    {
	last_accessed_time = System.currentTimeMillis();
	is_new = false;
    }
    
    public long getLastAccessedTime()
    {
	return last_accessed_time;
    }
    
    public int getMaxInactiveInterval()
    {
	return max_inactive_interval;
    }
    
    public javax.servlet.http.HttpSessionContext getSessionContext()
    {
	return null;
    }
    
    public java.lang.Object getValue(java.lang.String str)
    {
	return getAttribute( str );
    }
    
    public java.lang.String[] getValueNames()
    {
	synchronized( attributes )
	{
	    String[] list = new String[ attributes.size() ];
	    Enumeration enumeration = attributes.keys();
	    for ( int i=0; i<list.length; i++ )
	    {
		list[i] = (String)enumeration.nextElement();
	    }
	    return list;
	}
    }
    
    public void invalidate()
    {
	valid=false;
	synchronized ( lock )
	{
	    if ( authentication_string != null )
		sessions_by_auth.remove( authentication_string );
            if ( certificate != null )
                sessions_by_cert.remove( certificate );
	    sessions_by_id.remove( this.getId() );
	}
    }
    
    public boolean isNew()
    {
	return is_new;
    }
    
    public boolean isValid()
    {
     return valid;
    }
    
    public void putValue(java.lang.String str, java.lang.Object obj)
    {
	setAttribute( str, obj );
    }
    
    public void removeAttribute(java.lang.String str)
    {
	synchronized( attributes )
	{
	    attributes.remove( str );
	}
    }
    
    public void removeValue(java.lang.String str)
    {
	removeAttribute( str );
    }
    
    public void setAttribute(java.lang.String str, java.lang.Object obj)
    {
	synchronized( attributes )
	{
	    attributes.put( str, obj );
	}
    }
    
    public void setMaxInactiveInterval(int param)
    {
	max_inactive_interval = param;
    }
    
    
    public void setServletContext( javax.servlet.ServletContext context )
    {
	servlet_context = context;
    }
    
    public javax.servlet.ServletContext getServletContext()
    {
	return servlet_context;
    }
    
    
    public void setAuthenticationString( String auth )
    {
	synchronized ( lock )
	{
	    if ( authentication_string != null )
		sessions_by_auth.remove( authentication_string );
	}
	authentication_string = auth;
	if ( auth != null )
	{
	    synchronized ( lock )
	    {
		sessions_by_auth.put( auth, this );
	    }
	    setAttribute( "org.bodington.servlet.authentication_string", auth );
	}
	else
	    removeAttribute( "org.bodington.servlet.authentication_string" );
    }
    
    public void setCertificate( Object cert )
    {
	synchronized ( lock )
	{
	    if ( certificate != null )
		sessions_by_cert.remove( certificate );
	}
	certificate = cert;
	if ( cert != null )
	{
	    synchronized ( lock )
	    {
		sessions_by_cert.put( cert, this );
	    }
	    setAttribute( "org.bodington.servlet.certificate", cert );
	}
	else
	    removeAttribute( "org.bodington.servlet.certificate" );
    }
    
    
    public void setAuthType( String auth_type )
    {
	if ( auth_type != null )
	    setAttribute( "org.bodington.servlet.auth_type", auth_type );
	else
	    removeAttribute( "org.bodington.servlet.auth_type" );
	
    }
    
    public String getAuthType()
    {
	return (String)getAttribute( "org.bodington.servlet.auth_type" );
    }
    
    public void setUserId( PrimaryKey user_id )
    {
	this.user_id = user_id;
	if ( user_id != null )
	    setAttribute( "org.bodington.servlet.user_id", user_id );
	else
	    removeAttribute( "org.bodington.servlet.user_id" );
	
    }
    
    public PrimaryKey getUserId()
    {
	return user_id;
    }
    
    /**
     * Get the navigation session for this HttpSession.
     * If the current session doesn't have a navigation session then we get a new 
     * one from the BuildingSessionManager and associate it with the session.
     * @return The NavigationSession for the current user.
     */
    public NavigationSession getServerNavigationSession()
    {
	NavigationSession n_session = (NavigationSession)getAttribute(
	"org.bodington.server.resources.navigation_session" );
	if ( n_session == null )
	{
	    try
	    {
		n_session = (NavigationSession)
		BuildingSessionManagerImpl.getSession(
		org.bodington.server.NavigationSessionImpl.class );
		setAttribute( "org.bodington.server.resources.navigation_session", 
		n_session );
	    }
	    catch ( BuildingServerException e )
	    {
		log.error(e.getMessage(),e);
		return null;
	    }
	}
	return n_session;
    }
    
    
    public class HouseKeeper extends Thread
    {

        // This can be checked and changed buy two threads at the same time.
        volatile boolean done=false;
        
	public HouseKeeper()
	{
            super( "HttpSession-housekeeper" );
            // this thread can be safely killed when all the non-daemon
            // threads are dead.
            this.setDaemon( true );
	    this.start();
	}
	
	/**
	 * When an object implementing interface <code>Runnable</code> is used
	 * to create a thread, starting the thread causes the object's
	 * <code>run</code> method to be called in that separately executing
	 * thread.
	 * <p>
	 * The general contract of the method <code>run</code> is that it may
	 * take any action whatsoever.
	 *
	 * @see     java.lang.Thread#run()
	 */
	public void run()
	{
	    Enumeration enumeration;
	    Vector hit_list = new Vector();
	    HttpSession session;
	    String id;
	    int n, total;
	    long idle_time;
        log.info("HouseKeeper Startup.");
	    
	    while ( !done )
	    {
		try
		{
		    Thread.sleep( 10*60*1000 );  // checks at ten minute intervals
		}
		catch ( InterruptedException ex )
		{
		}
		
                if ( done )
                {
                    log.info("HouseKeeper Shutdown.");
                    return;
                }
                
		try
		{
		    log.debug("Looking for dead HttpSession objects.");
		    
		    total=0;
		    synchronized ( lock )
		    {
			hit_list.clear();
			enumeration = sessions_by_id.elements();
			while ( enumeration.hasMoreElements() )
			{
			    session = (HttpSession)enumeration.nextElement();
			    total++;
			    // NOTE: whole seconds as per servlet spec.
			    if ( (System.currentTimeMillis() - 
				    session.getLastAccessedTime())
			    < (session.getMaxInactiveInterval() * 1000) )
				continue;
			    
			    hit_list.addElement( session );
			}
		    }
		    if (log.isDebugEnabled())
		        log.debug("Found " + total + " sessions " + hit_list.size() + " inactive.");
		    
		    for ( n=0; n<hit_list.size(); n++ )
		    {
			session = (HttpSession)hit_list.elementAt( n );
			if ( session != null )
			    session.invalidate();
		    }
                    
		}
		catch ( Throwable t )
		{
		    log.error( t.getMessage(), t);
		}
	    }
	}
    }
}
