/* ======================================================================
 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 org.apache.log4j.Logger;

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.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
    {

    	private static Logger log = Logger.getLogger(BuildingServer.class);
        /**
         * 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.
         * @param for_setup If true then BuildingServer status on sucessful
         *               startup is {@link #STATUS_READY_FOR_SETUP}, this allows the
         *               setup code to move from setup status to {@link #STATUS_READY}
         *               after completing setup.
         */

    public static BuildingServer createInstance(boolean for_setup, Properties props)
            {
            if (vm_instance != null)
                return vm_instance;

            vm_instance = new BuildingServer();
        vm_instance.init( for_setup, props );
            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)
            {
            log.debug(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, Properties properties)
            {
            if (status != STATUS_CREATED)
                return;
            status = STATUS_STARTING;
            synchronized (this)
                {
                try
                    {
                    this.properties = new Properties(properties);
                    BuildingContext.resetTempDirectory();
                    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 )
                    {
        	    log.fatal( "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;

                log.info("BuildingServer started.");
                }

            initServices();

            }

        private void initServices()
            {
            
            String job_scheduler = properties.getProperty( "buildingserver.job_scheduler.enabled", "yes" );
            if (!job_scheduler.equalsIgnoreCase("yes"))
                return;
            
            log.debug("Trying to start Job Scheduler.");
            
            try
            {
                //kick Job Scheduler into action
                JobScheduler.getJobScheduler(true);
            }
            catch ( Exception ex )
            {
                //status=STATUS_STOPPED;
                log.fatal( "Exception starting Job scheduler: " + ex, ex );
                return;
            }

            log.debug("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" );
            log.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 )
            {
                log.error("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", XMLUtils.getXMLReader() );
			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");
                if (("connect.microsoft.MicrosoftDriver").equals(jdbc_driver))
                {
                    xml_repository.useCharacterStream(false);
                    xml_repository.setDBCharacterEncoding("UTF-16LE");
                }

			}
            catch ( Exception ex )
            {
                log.error( "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()
        {
            log.info("Shutdown");
            // Wake up the Job Scheduler so it doesn't hang around for
            // too long after bodington has closed shop.
            JobScheduler js = JobScheduler.getJobScheduler(false);
            if (js != null)
            {
                js.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)
                    {
                    
                    Enumeration enumeration = contexts.keys();
                    BuildingContext context;
                    while(enumeration.hasMoreElements())
                    {
                        final Object key = enumeration.nextElement();
                        context = (BuildingContext) contexts.get(key);
                    if ( context.getState() == BuildingContext.STATE_UNASSIGNED || 
                          context.getState() == BuildingContext.STATE_COMPLETED   )
                            contexts.remove(key);
                        }
                    }

                if (contexts.size() > 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 ( contexts.size() >0 && (current_time-start_time)<1000*60*10 );

            database.destroy();

            status = STATUS_STOPPED;
            vm_instance = null;
            }

        /**
     * 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 enumeration = contexts.keys();
            while (enumeration.hasMoreElements())
                {
                t2 = (Thread) enumeration.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()
        {
            log.warn( "=====================================================================" );
            log.warn( "=========   BuildingServer.dumpContextInformation   =================" );
            log.warn( "=====================================================================" );
            if (vm_instance == null)
            {
                log.warn("No active building server.");
                log.warn( "=====================================================================" );
                return;
            }
            
            log.warn("Hashtable of active contexts....");
            Enumeration enumeration = vm_instance.contexts.elements();
            BuildingContext context;
            while (enumeration.hasMoreElements())
            {
                context = (BuildingContext) enumeration.nextElement();
                log.warn("Context = " + context.toString());
                if (context.peekConnection() == null)
                    log.warn("No database connection assigned.");
                else
                    log.warn("A database connection is assigned.");
                context.dumpThisTrace();
                log.warn( "=====================================================================" );
            }
            
            log.warn("Stack of spare contexts....");
            for (int i = 0; i < vm_instance.spare_contexts.size(); i++)
            {
                context = (BuildingContext) vm_instance.spare_contexts.elementAt(i);
                log.warn("Context = " + context.toString());
                if (context.peekConnection() == null)
                    log.warn("No database connection assigned.");
                else
                    log.warn("A database connection is assigned.");
                context.dumpThisTrace();
                log.warn( "=====================================================================" );
            }
            log.warn( "=================     END     =======================================" );
            log.warn( "=====================================================================" );
        }

        /**
     * 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 enumeration = contexts.elements(); enumeration.hasMoreElements();)
                {
                BuildingContext c = (BuildingContext) enumeration.nextElement();
                log.info("Context ID = " + c.getId());
                log.info("    Thread = " + c.getThread());
                }
            }
        //{{DECLARE_CONTROLS
        //}}
    }