/* ======================================================================
   Parts Copyright 2006 University of Leeds, Oxford University, University of the Highlands and Islands.

   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.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 Set previous_session_ids = new HashSet();
    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 HashSet();
            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 )
    {
	HttpSession session;
    synchronized ( lock )
    {
        session = (HttpSession)sessions_by_id.get( Id );
    }
    if ( session!=null )
        session.accessedNow();
    return session;
    }
    
    public static HttpSession findSessionByAuthenticationString( 
    String auth )
    {
	HttpSession session;
    synchronized ( lock )
    {
        session = (HttpSession)sessions_by_auth.get( auth );
    }
    if ( session!=null )
        session.accessedNow();
    return session;
    }    

    public static HttpSession findSessionByCertificate( Object certificate )
    {
	HttpSession session;
    synchronized ( lock )
    {
        session = (HttpSession)sessions_by_cert.get( certificate );
    }
    if ( session!=null )
        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.contains( code ) );
	    previous_session_ids.add( code );
	}
	
	return code;
    }

    public static int getSessionCount()
    {
        return sessions_by_id.size();
    }
    
    
    private String id;
    private long creation_time, last_accessed_time;
    /**
     * Max inactivity time in whole seconds.
     */
    private int max_inactive_interval;
    private Hashtable attributes;
    private Map preferences;
    private boolean is_new;
    private javax.servlet.ServletContext servlet_context;
    private String authentication_string;
    private Object certificate;
    private boolean small_screen;
    
    private String remoteHost = null;
    
    /** Creates new HttpSession */
    public HttpSession()
    {
	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; 
	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);
        }
    }

    /**
     * Clears user preferences.
     * Switching user doesn't create a new session, so cleanout preferences 
     * that are only relevent to the current user.
     */
    public void cleanout()
    {
        // Clear the cached preferences.
        preferences.clear();
        // Remove the tracked resource.
        removeAttribute(Request.RESOURCE_ATTRIBUTE);
        // clear the user style sheet to force recompilation
        removeAttribute("org.bodington.user_style_sheet");
        removeAttribute("org.bodington.user_style_sheet_table");
        removeAttribute("org.bodington.user_style_sheet_auto_table");
        removeAttribute("org.bodington.user_style_sheet_session_data");
    }
    
    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;
    }
    
    /**
     * @deprecated This is deprecated in the servlet API.
     */
    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()
    {
        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 PrimaryKey getUserId()
    {
        NavigationSession session = getServerNavigationSession();
        try
        {
            if (session != null)
                return session.getAuthenticatedUserId();
        }
        catch (Exception e)
        {
            log.error("Failed to get authenticated user.", e);
        }
        return null;
    }
    /**
     * 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 =
		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);
		}
	    }
	}
    }
}
