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

import java.io.IOException;
import java.io.InputStream;
import java.rmi.registry.Registry;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.Properties;
import java.util.Stack;

import org.apache.log4j.Logger;
import org.bodington.database.Database;
import org.bodington.xml.XMLRepository;
import org.bodington.xml.XMLUtils;
import org.xml.sax.XMLReader;

/**
 * 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;
        private MimeTypeMapper mimeMapper;

        /**
         * 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);
                    initXMLRepository(context);
                    endContext();

            	}
        	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(BuildingContext context)
		throws BuildingServerException
            {
            try
                {
            XMLReader xmlReader = XMLUtils.getXMLReader();
			String classname = (xmlReader == null)?null:xmlReader.getClass().getName();
			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.setCharacterStream(false);
                    xml_repository.setDBCharacterEncoding("UTF-16LE");
                }
                xml_repository.init(context.getConnection());

			}
            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);
            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;
            }
        
        public String getMimeType(String filename)
            {
            if (mimeMapper == null)
                return null;
            return mimeMapper.getMimeType(filename);
            }

        /**
         * 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
        //}}

        /**
         * Attempts to load the bodington defaults out of the classpath. We
         * allow calling applications to load the defaults so that you can chain
         * your properties objects.
         * @return A properties object that can be used for defaults.
         */
        public static Properties loadBodingtonDefaults()
        {
            Properties defaults = new Properties();
            try
            {
                InputStream input = BuildingServer.class
                .getResourceAsStream("/bodington-defaults.properties");
                if (input == null)
                {
                    log.error("Could not find bodington-defaults.properties");
                }
                else
                {
                    defaults.load(input);
                }
            }
            catch (IOException ioe)
            {
                log.error("Error loading bodington-defaults.properties");
            }
            log.debug("Loaded " + defaults.size()
                + " properties from bodington-defaults.properties");
            return defaults;
        }

        /**
         * Set how we should do convert filenames into mime types.
         */
        public static void setMimeMapper(MimeTypeMapper mapper)
        {
            if (vm_instance == null)
                log.error("Can't set MIME mapper without BuildingServer running");
            else
                vm_instance.mimeMapper = mapper;
        }
    }
