/*
 * HttpSession.java
 *
 * Created on 29 May 2002, 10:06
 */

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 java.util.logging.*;

import javax.servlet.http.*;
/**
 *
 * @author  bmb6jrm
 * @version
 */
public class HttpSession implements javax.servlet.http.HttpSession
{
    
    
    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()
    {
	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();
        }
    }
    
    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 Properties 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;
    
    /** Creates new HttpSession */
    public HttpSession()
    {
	valid=true;
	is_new=true;
	id = nextSessionId();
	creation_time = System.currentTimeMillis();
	last_accessed_time = creation_time;
	max_inactive_interval = 60*60*1000;
	user_id = null;
	attributes = new Hashtable();
	preferences = new Properties();
        
	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 );
    }
    
    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;
    }
    
    public String getPreference( String key )
    {
        // try current value (in http_session)
        String value=null;
        value = preferences.getProperty( key );
        if ( value != null )
        {
            if ( "null".equals( value ) )
                return null;
            return value;
        }

        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() )
                return null;
            value = nav_session.getUserProperty( key );
        }
        catch ( Exception e )
        {
            Logger.getLogger( "org.bodington" ).logp(
                Level.SEVERE,
                "HttpSession",
                "getPreference",
                e.getMessage(),
                e );
            value = null;
        }

        if ( value == null )
            value = "null";
        
        // if found there store in session data for later use
        preferences.setProperty( key, value );
        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;
    }
    
    public void setPreference( String key, String value )
    {
        // if found there store in session data for later use
        preferences.setProperty( key, 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() )
                return;
            nav_session.setUserProperty( key, value );
        }
        catch ( Exception e )
        {
            Logger.getLogger( "org.bodington" ).logp(
                Level.SEVERE,
                "HttpSession",
                "setPreference",
                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 enum = attributes.keys();
	    String key;
	    while ( enum.hasMoreElements() )
	    {
		key = (String)enum.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 enum = attributes.keys();
	    for ( int i=0; i<list.length; i++ )
	    {
		list[i] = (String)enum.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 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;
    }
    
    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.resources.NavigationSessionImpl.class );
		setAttribute( "org.bodington.server.resources.navigation_session", 
		n_session );
	    }
	    catch ( BuildingServerException e )
	    {
		Logger.getLogger( "org.bodington" ).logp(
		Level.SEVERE,
		"HttpSession",
		"getServerNavigationSession",
		e.getMessage(),
		e );
		return null;
	    }
	}
	return n_session;
    }
    
    
    public class HouseKeeper extends Thread
    {
        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 enum;
	    Vector hit_list = new Vector();
	    HttpSession session;
	    String id;
	    int n, total;
	    long idle_time;
	    
	    while ( !done )
	    {
		try
		{
		    Thread.sleep( 10*60*1000 );  // checks at ten minute intervals
		}
		catch ( InterruptedException ex )
		{
		}
		
                if ( done )
                    return;
                
		try
		{
		    Logger.getLogger( "org.bodington" ).logp(
		    Level.FINE,
		    "HttpSession",
		    "run",
		    "Looking for dead HttpSession objects."
		    );
		    
		    total=0;
		    synchronized ( lock )
		    {
			hit_list.clear();
			enum = sessions_by_id.elements();
			while ( enum.hasMoreElements() )
			{
			    session = (HttpSession)enum.nextElement();
			    total++;
			    
			    if ( (System.currentTimeMillis() - 
				    session.getLastAccessedTime())
			    < session.getMaxInactiveInterval() )
				continue;
			    
			    hit_list.addElement( session );
			}
		    }
		    
		    Logger.getLogger( "org.bodington" ).logp(
		    Level.SEVERE,
		    "HttpSession",
		    "run",
		    "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 )
		{
		    Logger.getLogger( "org.bodington" ).logp(
		    Level.SEVERE,
		    "HttpSession",
		    "run",
		    t.getMessage(),
		    t );
		    
		}
	    }
	}
    }
}
