/* ======================================================================
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.server;

import java.util.logging.*;



import org.bodington.database.*;
import org.bodington.xml.*;
import org.bodington.logging.*;

import java.util.Hashtable;
import java.util.Properties;
import java.util.Enumeration;
import java.util.Stack;
import java.util.logging.*;

import java.sql.*;
import java.io.*;
import java.rmi.Naming;
import java.rmi.registry.Registry;
import java.rmi.registry.LocateRegistry;

/**
 * Coordinating class for context and database services.
 * 
 * @author Jon Maber
 * @version 1
 */

public class BuildingServer extends Object
    {

    /**
     * Reference to this VM's instance of the class.
     */
    private static BuildingServer vm_instance=null;
    
    public static final int STATUS_NONE=-1;
    public static final int STATUS_CREATED=0;
    public static final int STATUS_STARTING=1;
    public static final int STATUS_STOPPING=2;
    public static final int STATUS_PAUSED=3;
    public static final int STATUS_STOPPED=4;
    public static final int STATUS_READY=5;

    public static final int STATUS_READY_FOR_SETUP=6;

	 private XMLRepository xml_repository;
    
    
    /**
     * For a particular VM there should only ever be one instantiated 
     * object of this class.  This method returns that instance.
     */
    
    public static BuildingServer getInstance()
        {
        if ( vm_instance!=null )
        	{
        	synchronized ( vm_instance )
        		{
        		if ( vm_instance.status == STATUS_READY || vm_instance.status == STATUS_READY_FOR_SETUP )
	        		return vm_instance;
	        	}
	        }

     	return null;
        }


    /**
     */
    
    public static int getStatus()
        {
        if ( vm_instance==null )
            return STATUS_NONE;
            
        synchronized ( vm_instance )
        	{
	        return vm_instance.status;
	        }
        }


    /**
     * If the current instance was created for a setup procedure
     * switch it to be ready for users.
     */
        
    public static void setupComplete()
    {
	if ( vm_instance!=null )
	{
	    if ( vm_instance.status == STATUS_READY_FOR_SETUP )
	    {
		vm_instance.status = STATUS_READY; 
		vm_instance.initServices();
	    }
	}

	
    }
        
        
    /**
     * If the current instance is active this method will
     * return it but otherwise it will create a new
     * instance and return that.
     */
        
    public static BuildingServer createInstance()
        {
        return createInstance( false, null );
        }
    /**
     * If the current instance is active this method will
     * return it but otherwise it will create a new
     * instance and return that.
     */
        
    public static BuildingServer createInstance( boolean for_setup, String prop_file )
        {
        if ( vm_instance!=null )
            return vm_instance;
        
        vm_instance=new BuildingServer();
        vm_instance.init( for_setup, prop_file );
        return vm_instance;
        }

    /**
     * A static method for redirecting code tracing messages.
     * Currently messages are sent to System.out
     * 
     * @param message This is the message string to be logged or output.
     */

    public static void codeTrace( String message )
        {
        Logger.getLogger( "org.bodington" ).fine( message );
        }

    /**
     * Holds current status of the instance.
     */

     
     
    private int status;
    private Properties properties;
    private Registry rmi_registry;

    /**
     * Table of contexts.
     */
    private Hashtable contexts;
    private Stack spare_contexts;

    /**
     * The database loaded for this server.  A property of the server controls
     * which implementation of org.bodington.database.Database is used.
     */
    private Database database;

    /**
     * The constructor is declared private because the class should only 
     * be instantiated by its own static methods.
     */
    
    private BuildingServer()
        {
        status=STATUS_CREATED;
        		//{{INIT_CONTROLS
		//}}
		}

    /**
     * Loads properties from properties files and initialises contexts
     * and database.
     */
        
    private void init( boolean for_setup, String prop_file )
        {
        FileInputStream in;
        //Properties def_prop;
        String bodington_properties;
        
		// synchronization will prevent other threads fetching the
		// instance until it has been initialized
        synchronized ( this )
        	{
        	
        	if ( status!=STATUS_CREATED )
            	return;
        	status=STATUS_STARTING;
	            
        	try
            	{
            	if ( prop_file == null )
                	bodington_properties = System.getProperty( 
            								"bodington.properties" );
            	else
                	bodington_properties = prop_file;
	                
            	System.err.println( bodington_properties );

            	if ( bodington_properties == null )
                	{
                	status = STATUS_STOPPED;
                	return;
                	}
	                
            	properties = new Properties();
            	try
                	{
                	in = new FileInputStream( new File( bodington_properties ) );
                	properties.load( in );
                	in.close();
                	}
            	catch ( FileNotFoundException fex2 )
                	{
                	System.err.println( "Main properties file not found..." );
                	status = STATUS_STOPPED;
                	return;
                	}

		BuildingContext.resetTempDirectory();
		String logging_path = getProperty( "buildingserver.logging.path" );
		String logging_level = getProperty( "buildingserver.logging.level" );
		String logging_binary = getProperty( "buildingserver.logging.binary", "no" );
		String logging_formatter = getProperty( "buildingserver.logging.formatter", "java.util.logging.SimpleFormatter" );

		Logger init_logger = Logger.getLogger( "org.bodington.init" );
                FileHandler init_fh = new FileHandler( logging_path + "/init_bodlog%g.txt", 10*1024*1024, 3 );
                init_fh.setFormatter( new SimpleFormatter() );
                init_logger.addHandler( init_fh );
                
		Logger logger = Logger.getLogger( "org.bodington" );
		logger.setUseParentHandlers( false );
		Logger req_logger = Logger.getLogger( "org.bodington.servlet.requests" );
		req_logger.setUseParentHandlers( false );
                if ( "yes".equalsIgnoreCase( logging_binary ) )
                {
                    ObjectFileHandler ofh = new ObjectFileHandler( logging_path + "/bodlog%g.bin", 10*1024*1024, 100 );
                    logger.addHandler(ofh);
                    ObjectFileHandler rofh = new ObjectFileHandler( logging_path + "/reqlog%g.bin", 10*1024*1024, 100 );
                    req_logger.addHandler(rofh);
                    //ObjectSocketHandler osh = new ObjectSocketHandler( "localhost", 4444 );
                    //logger.addHandler(osh);
                }
                else
                {
                    Class formatterclass = Class.forName( logging_formatter );
                    FileHandler fh = new FileHandler( logging_path + "/bodlog%g.txt", 10*1024*1024, 100 );
                    fh.setFormatter( (Formatter)formatterclass.newInstance() );
                    logger.addHandler(fh);

                    Class rformatterclass = Class.forName( "org.bodington.logging.VerySimpleFormatter" );
                    FileHandler rfh = new FileHandler( logging_path + "/reqlog%g.txt", 10*1024*1024, 100 );
                    rfh.setFormatter( (Formatter)rformatterclass.newInstance() );
                    req_logger.addHandler(rfh);
                }
		logger.setLevel( Level.parse( logging_level ) );
		logger.severe( "Opened log" );
		req_logger.setLevel( Level.FINE );
		req_logger.severe( "Opened log" );
		
	            
            	contexts=new Hashtable();
            	spare_contexts=new Stack();
	            
                // private call need to make sure we get a
                // context even though the server isn't marked as ready.
            	startContextPrivate();
            	initDatabase();
            	BuildingContext context=getContext();
            	context.setDatabase( database );
            	endContext();
	            
	            
            	initXMLRepository();
            	}
        	catch ( Exception ex )
            	{
		Logger.getLogger( "org.bodington.init" ).logp( 
		    Level.SEVERE, 
		    "BuildingServer", 
		    "init", 
		    "Exception initialising BuildingServer: " + ex, 
		    ex );
		status=STATUS_STOPPED;
            	return;
            	}

			if ( for_setup )
				{
  				status=STATUS_READY_FOR_SETUP;
  				// don't need RMI or job scheduler in setup situation
  				return;
  				}
			else
  				status=STATUS_READY;

 			Logger.getLogger( "org.bodington" ).info( "\n\n BuildingServer started." );
  			}
  			
		initServices();
			
    }
  	
    
	private void initServices()
        {
        try
            {
            //initialise RMI service...
				String rmi_internal_registry = properties.getProperty( "buildingserver.rmi.internal_registry", "no" );
				String rmi_host = properties.getProperty( "buildingserver.rmi.host", "localhost" );
				String rmi_port = properties.getProperty( "buildingserver.rmi.port", Integer.toString(Registry.REGISTRY_PORT) );
				String rmi_service = properties.getProperty( "buildingserver.rmi.service", "bodingtonsessionmanager" );
				if ( rmi_internal_registry.equalsIgnoreCase( "yes" ) )
					{
					Logger.getLogger( "org.bodington" ).info( "Configured for internal RMI registry." );
					//rmi_registry = LocateRegistry.getRegistry();
					// start one if there isn't one already
					//if ( rmi_registry==null )
						{
						rmi_registry = LocateRegistry.createRegistry( Integer.parseInt( rmi_port ) );
						if ( rmi_registry ==null )
							Logger.getLogger( "org.bodington" ).info( "RMI registry not created." );
						else
							{
							Logger.getLogger( "org.bodington" ).info( "Created RMI registry." );
					
            			String[] names;
            			Class c;
            			int i;
            			Logger.getLogger( "org.bodington" ).info( "\n\n\nRMI registration..." );
            			BuildingSessionManager manager = new BuildingSessionManagerImpl();

            			Naming.rebind( "//" + rmi_host + ":" + rmi_port + "/" + rmi_service, manager );
			            
            			Logger.getLogger( "org.bodington" ).info( "\n\n\nRMI after class instantiated..." );
            			names = Naming.list( "rmi://" + rmi_host + ":" + rmi_port  + "/" );
            			for ( i=0; i<names.length; i++ ) Logger.getLogger( "org.bodington" ).info( names[i] );
			            
            			BuildingSessionManager manref = 
            				(BuildingSessionManager)Naming.lookup( "//" + rmi_host + ":" + rmi_port + "/" + rmi_service );
            			}
						}
						
					}

            }
        catch ( Exception ex )
            {
	    Logger.getLogger( "org.bodington" ).logp( 
		Level.SEVERE, 
		"BuildingServer", 
		"init", 
		"Exception starting RMI server: " + ex, 
		ex );
            //status=STATUS_STOPPED;
            }

		String job_scheduler = properties.getProperty( "buildingserver.job_scheduler.enabled", "no" );
		if ( !job_scheduler.equalsIgnoreCase( "yes" ) )
			return;
			
		Logger.getLogger( "org.bodington" ).info( "\n\n\nStarting Job Scheduler..." );
        
        try
            {
            //kick Job Scheduler into action
            JobScheduler.getJobScheduler( true );
            }
        catch ( Exception ex )
            {
            //status=STATUS_STOPPED;
	    Logger.getLogger( "org.bodington" ).logp( 
		Level.SEVERE, 
		"BuildingServer", 
		"init", 
		"Exception starting Job scheduler: " + ex, 
		ex );
	    return;
            }

		Logger.getLogger( "org.bodington" ).info( "\n\n Job Scheduler started." );
        }

	/**
	 * Initialises the database - called from init()
	 * 
	 * @exception org.bodington.server.BuildingServerException
	 */

	private void initDatabase()
		throws BuildingServerException
		{
		String db_class_name = properties.getProperty( "buildingserver.dbimplementation", "org.bodington.sqldatabase.SqlDatabase" );
		Logger.getLogger( "org.bodington" ).info( "Database class name: " + db_class_name );
		try
			{
			Class db_class = Class.forName( db_class_name );
			database = (Database)db_class.newInstance();
			database.init( properties );
			}
		catch ( Exception ex )
			{
			Logger.getLogger( "org.bodington" ).logp( 
			    Level.SEVERE, 
			    "BuildingServer", 
			    "initDatabase", 
			    "Exception starting database: " + ex, 
			    ex );
			throw new BuildingServerException( ex.toString() );
			}
		}

	/**
	 * Initialises the database - called from init()
	 * 
	 * @exception org.bodington.server.BuildingServerException
	 */

	private void initXMLRepository()
		throws BuildingServerException
		{
		try
			{
			String classname = properties.getProperty( "xmlrepository.driver", "org.apache.crimson.parser.XMLReaderImpl" );
			String otable = properties.getProperty( "xmlrepository.table.objects", "xml_objects" );
			String etable = properties.getProperty( "xmlrepository.table.elements", "xml_elements" );
			String atable = properties.getProperty( "xmlrepository.table.attributes", "xml_attributes" );
			String ctable = properties.getProperty( "xmlrepository.table.cdata", "xml_cdata" );
			String ttable = properties.getProperty( "xmlrepository.table.tokens", "xml_tokens" );
			String wtable = properties.getProperty( "xmlrepository.table.words", "xml_words" );
			xml_repository = new XMLRepository( classname, otable, etable, atable, ctable, ttable, wtable );
			
			xml_repository.setTempDirectory( BuildingContext.getTempDirectory() );
			
			// old weblogic driver is not JDBC 2 compatible.
			String jdbc_driver = properties.getProperty( "sqldatabase.driver", "connect.microsoft.MicrosoftDriver" );
			if ( jdbc_driver.equals( "connect.microsoft.MicrosoftDriver" ) )
				{
				xml_repository.useCharacterStream( false );
				xml_repository.setDBCharacterEncoding( "UTF-16LE" );
				}
				
			}
		catch ( Exception ex )
			{
			Logger.getLogger( "org.bodington" ).logp( 
			    Level.SEVERE, 
			    "BuildingServer", 
			    "initXMLRepository", 
			    "Exception starting XML repository: " + ex, 
			    ex );
			throw new BuildingServerException( "Problem initialising xml repository: " + ex.getMessage() );
			}
		}

	public XMLRepository getXMLRepository()
		{
		return xml_repository;
		}


    /**
     * Is the server ready - has it completed initialisation.
     * This functionality is not properly developed yet.
     * 
     * @return Returns true if the server is ready.
     */
		
    public boolean isReady()
        {
        return status==STATUS_READY;
        }

    /**
     * Initiate shutdown - should allow pending transactions to
     * complete but prevent new transactions.  This functionality
     * is not yet properly implemented.
     */
    
    public void shutdown()
        {
        status=STATUS_STOPPING;
        
        // record when we started the shutdown
        long start_time = System.currentTimeMillis();
        long current_time;
        
        int i, n;
        
        do
        {
            // remove all inactive contexts from list
            synchronized ( this )
            {
                n = contexts.size();
                Object[] key_list = new Object[n];
                Enumeration enum = contexts.keys();
                BuildingContext context;
                for ( i=0; enum.hasMoreElements(); i++ )
                    key_list[i] = enum.nextElement();
                for ( i=0; i<key_list.length; i++ )
                {
                    context = (BuildingContext)contexts.get( key_list[i] );
                    if ( context.getState() == BuildingContext.STATE_UNASSIGNED || 
                          context.getState() == BuildingContext.STATE_COMPLETED   )
                        contexts.remove( key_list[i] );
                }
            }
            
            if ( n>0 )
                try { Thread.sleep( 10*1000 ); } catch ( InterruptedException ie ) {}
            current_time = System.currentTimeMillis();
            
            // keep thinning out until none left or a long time has elapsed.
        }
        while ( n>0 && (current_time-start_time)<1000*60*10 );
        
        database.destroy();
        
        status=STATUS_STOPPED;
        }

    /**
     * This method is called by a client at the start of a series
     * of transactions and before an authentication routine starts its 
     * work.  This method is here in case an envirmonent requires
     * custom code for authentication and that code needs to make use
     * of BuildingContext services.
     * 
     * @return The freshly initialised context object.
     */
    
    public synchronized BuildingContext startContextForAuthentication()
        {
        if ( !isReady() )
            return null;
        return startContextForAuthenticationPrivate();
        }
    
    public synchronized BuildingContext startContextForAuthenticationPrivate()
        {
        BuildingContext context;
        Thread t = Thread.currentThread();
        Thread t2;
		
		// reclaim contexts on dead threads
        Enumeration enum = contexts.keys();
        while ( enum.hasMoreElements() )
        	{
        	t2=(Thread)enum.nextElement();
        	if ( !t2.isAlive() )
        		{
	        	context = (BuildingContext)contexts.get( t2 );
	        	if ( context!=null )
	        		{
	        		context.clear();
       				spare_contexts.push( context );
       				}
        		contexts.remove( t2 );
        		}
        	}
        	
        // get context for this thread
		context=(BuildingContext)contexts.get( t );
		if ( context!=null )
		    {
		    context.close();
		    context.setState( BuildingContext.STATE_AUTHENTICATING );
		    context.setUser( null );
		    //context.setAuthenticationMethod( "none" );
		    return context;
		    }
		
		if ( !spare_contexts.empty() )
			{
			context=(BuildingContext)spare_contexts.pop();
		    context.init();
			}
		else
			{
			context = new BuildingContext();
			}
			
		context.setDatabase( database );
	    context.setState( BuildingContext.STATE_AUTHENTICATING );
		contexts.put( t, context );
		
		return context;
        }

	public static synchronized void dumpContextInformation()
		{
		Logger.getLogger( "org.bodington" ).warning( "=====================================================================" );
		Logger.getLogger( "org.bodington" ).warning( "=========   BuildingServer.dumpContextInformation   =================" );
		Logger.getLogger( "org.bodington" ).warning( "=====================================================================" );
		if ( vm_instance == null )
			{
			Logger.getLogger( "org.bodington" ).warning( "No active building server." );
			Logger.getLogger( "org.bodington" ).warning( "=====================================================================" );
			return;
			}
			
		Logger.getLogger( "org.bodington" ).warning( "Hashtable of active contexts...." );
		Enumeration enum = vm_instance.contexts.elements();
		BuildingContext context;
		while ( enum.hasMoreElements() )
			{
			context = (BuildingContext)enum.nextElement();
			Logger.getLogger( "org.bodington" ).warning( "Context = " + context.toString() );
			if ( context.peekConnection() == null )
				Logger.getLogger( "org.bodington" ).warning( "No database connection assigned." ); 
			else
				Logger.getLogger( "org.bodington" ).warning( "A database connection is assigned." ); 
			context.dumpThisTrace();
			Logger.getLogger( "org.bodington" ).warning( "=====================================================================" );
			}

		Logger.getLogger( "org.bodington" ).warning( "Stack of spare contexts...." );
		for ( int i=0; i<vm_instance.spare_contexts.size(); i++ )
			{
			context = (BuildingContext)vm_instance.spare_contexts.elementAt( i );
			Logger.getLogger( "org.bodington" ).warning( "Context = " + context.toString() );
			if ( context.peekConnection() == null )
				Logger.getLogger( "org.bodington" ).warning( "No database connection assigned." ); 
			else
				Logger.getLogger( "org.bodington" ).warning( "A database connection is assigned." ); 
			context.dumpThisTrace();
			Logger.getLogger( "org.bodington" ).warning( "=====================================================================" );
			}
		Logger.getLogger( "org.bodington" ).warning( "=================     END     =======================================" );
		Logger.getLogger( "org.bodington" ).warning( "=====================================================================" );
		}


    /**
     * This method is called by a client at the start of a series
     * of transactions possibly after another part of the client
     * has performed authentication code.
     * 
     * @return The freshly initialised context object.
     */
    
    
    public synchronized BuildingContext startContext()
        {
        if ( !isReady() )
            return null;
        return startContextPrivate();
        }
        
    private synchronized BuildingContext startContextPrivate()
        {
        BuildingContext context=getContext();
        
        if ( context==null || context.getState() == BuildingContext.STATE_UNASSIGNED )
        	{
        	context=startContextForAuthenticationPrivate();
        	context.setUser( null );
	        //context.setAuthenticationMethod( "none" );
        	return context;
        	}
        	
        if ( context.getState() == BuildingContext.STATE_AUTHENTICATING )
        	return context;

		return null;
        }

    /**
     * Retrieves the context object associated with the current
     * thread of execution.
     * 
     * @return The context object for this thread or null no context 
     * object has been set up.
     */
        
    public synchronized BuildingContext getContext()
        {
        BuildingContext context;
        Thread t;
        t = Thread.currentThread();
		context=(BuildingContext)contexts.get( t );
		return context;
        }

    /**
     * Called by client to signal that transactions have been
     * completed and that the context object is no longer needed.
     * The server will reinitialise the context so it is available
     * the next time this thread calls a start method again.
     * There ought also to be some house keeping that removes
     * contexts if the associated thread no longer exists.
     */

    public synchronized void endContext()
        {
        BuildingContext context=getContext();
        if ( context==null )
        	return;
        if ( database != null )
	        database.freeContext();
        context.setState( BuildingContext.STATE_COMPLETED );
        context.close();
        }

    /**
     * Retreives a property of the server.  Properties will
     * be loaded by the server from file when it initialises.
     * 
     * @param key The name of the property.
     * @return The value of the property or null if it isn't found.
     */
        
    public String getProperty( String key )
    	{
    	return properties.getProperty( key );
    	}

    /**
     * Retreives a property of the server.  Properties will
     * be loaded by the server from file when it initialises.
     * 
     * @param key The name of the property.
     * @return The value of the property or null if it isn't found.
     */
        
    public String getProperty( String key, String def )
    	{
    	return properties.getProperty( key, def );
    	}


	public Properties getProperties()
		{
		return properties;
		}



    /**
     * A debugging routine to dump internal state to System.out
     */
        
    public synchronized void dumpStatus()
        {
        for ( Enumeration enum=contexts.elements(); enum.hasMoreElements(); )
            {
            BuildingContext c=(BuildingContext)enum.nextElement();
            Logger.getLogger( "org.bodington" ).info( "Context ID = " + c.getId() );
            Logger.getLogger( "org.bodington" ).info( "    Thread = " + c.getThread() );
            }
        }
    	//{{DECLARE_CONTROLS
	//}}
}
