/*
 * JBoss, Home of Professional Open Source.
 * Copyright 2006, Red Hat Middleware LLC, and individual contributors
 * as indicated by the @author tags. See the copyright.txt file in the
 * distribution for a full listing of individual contributors.
 *
 * This is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 2.1 of
 * the License, or (at your option) any later version.
 *
 * This software is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */
package org.jboss.soa.bpel.runtime.db;

import java.io.File;
import java.io.IOException;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.sql.Statement;

import org.h2.tools.Server;
import org.jboss.mx.util.MBeanProxyExt;
import org.jboss.system.ServiceMBeanSupport;
import org.jboss.system.server.ServerConfigImpl;
import org.jboss.system.server.ServerConfigImplMBean;

/**
 * Integration with H2.
 *
 * @author <a href="mailto:rickard.oberg@telkel.com">Rickard �berg</a>
 * @author <a href="mailto:Scott_Stark@displayscape.com">Scott Stark</a>.
 * @author <a href="mailto:pf@iprobot.com">Peter Fagerlund</a>
 * @author <a href="mailto:jason@planet57.com">Jason Dillon</a>
 * @author <a href="mailto:vesco.claudio@previnet.it">Claudio Vesco</a>
 * @author <a href="mailto:dimitris@jboss.org">Dimitris Andreadis</a>
 * @author <a href="mailto:kevin.conner@jboss.org">Kevin Conner</a>
 * @author <a href="mailto:dbevenius@redhat.com">Daniel Bevenius</a>
 * @version $Revision: 27106 $
 */
public class H2Database extends ServiceMBeanSupport implements H2DatabaseMBean
{
    /** Default password: <code>empty string</code>. */
    private static final String DEFAULT_PASSWORD = "";
       
    /** Default user: <code>sa</code>. */
    private static final String DEFAULT_USER = "sa";
       
    /** JDBC Driver class: <code>org.h2.Driver</code>. */
    private static final String JDBC_DRIVER_CLASS = "org.h2.Driver";
       
    /** JDBC URL common prefix: <code>jdbc:h2:</code>. */
    private static final String JDBC_URL_PREFIX = "jdbc:h2:";
   
    /** JDBC in memory URL prefix: <code>jdbc:h2:mem:</code>. */
    private static final String JDBC_MEM_URL_PREFIX = JDBC_URL_PREFIX + "mem:";
       
    /** JDBC flags */
    private static final String DEFAULT_FLAGS = ";MVCC=TRUE;DB_CLOSE_ON_EXIT=FALSE";
       
    /** Default data subdir: <code>h2</code>. */
    private static final String H2_DATA_DIR = "h2";
       
    /** Default database name: <code>default</code>. */
    private static final String DEFAULT_DATABASE_NAME = "default";
       
    /** Default address for remote h2: <code>0.0.0.0</code>. */
    private static final String DEFAULT_ADDRESS = "0.0.0.0";
       
    /** Default port for remote h2: <code>9092</code>. */
    private static final int DEFAULT_PORT = 9092;
    
    /** Default delay for remote hypersonic initialisation (ms): <code>5000</code>. */
    private static final long DEFAULT_DELAY = 5000;
    
    // Private Data --------------------------------------------------
           
    /** Full path to db/h2. */
    private File dbPath;
    
    /** Database name. */
    private String name = DEFAULT_DATABASE_NAME;
       
    /** In memory mode. */
    private boolean inMemoryMode ;
       
    /** Database user. */
    private String user = DEFAULT_USER;
       
    /** Database password. */
    private String password = DEFAULT_PASSWORD;
       
    /** Database flags */
    private String flags = DEFAULT_FLAGS ;
       
    /** Hold a connection for in memory h2. */
    private Connection connection;
       
    /** Default address. */
    private String address = DEFAULT_ADDRESS;
       
    /** Default port. */
    private int port = DEFAULT_PORT;
       
    /** Server/remote mode. */
    private boolean serverMode = false;
   
    /** Server thread for remote h2. */
    private Thread serverThread;
    
    /** The remote server instance */
    private Server remoteServer ;
    
    /** Server thread delay for remote H2. */
    private long delay = DEFAULT_DELAY ;

    private String datadir;

    // Attributes ----------------------------------------------------
   
    /**
     * Set the database name.
     * 
     * @jmx.managed-attribute
     */
    public void setDatabase(String name)
    {
        if (name == null)
        {
            name = DEFAULT_DATABASE_NAME;
        }
        this.name = name;
   }

    /**
     * Get the database name.
     * 
     * @jmx.managed-attribute
     */
    public String getDatabase()
    {
        return name;
    }
    
    /**
     * Get the full database path.
     * 
     * @jmx.managed-attribute
     */
    public String getDatabasePath()
    {
        if (dbPath != null)
        {
            return dbPath.toString();
        }
        else
        {
            return null;
        }
    }
    
    /**
     * @return the <code>inMemoryMode</code> flag.
     * 
     * @jmx.managed-attribute 
     */
    public boolean isInMemoryMode()
    {
        return inMemoryMode;
    }

    /**
     * If <b>true</b> the h2 is in memory mode otherwise embedded mode.
     * 
     * @param b in memory mode.
     * 
     * @jmx.managed-attribute
     */
    public void setInMemoryMode( boolean b )
    {
        inMemoryMode = b;
    }
    
    /**
     * @return the password
     * 
     * @jmx.managed-attribute 
     */
    public String getPassword()
    {
        return password;
    }

    /**
     * @return the user
     * 
     * @jmx.managed-attribute 
     */
    public String getUser()
    {
        return user;
    }
    
    /**
     * @return the flags
     * 
     * @jmx.managed-attribute 
     */
    public String getFlags()
    {
        return flags;
    }
    
    /**
     * @param password
     * 
     * @jmx.managed-attribute 
     */
    public void setPassword(String password)
    {
        if (password == null)
        {
            password = DEFAULT_PASSWORD;
        }
        this.password = password;
    }
    
    /**
     * @param user
     * 
     * @jmx.managed-attribute 
     */
    public void setUser(String user)
    {
        if (user == null)
        {
            user = DEFAULT_USER;
        }
        this.user = user;
    }
        
    /**
     * @param flags
     * 
     * @jmx.managed-attribute 
     */
    public void setFlags(String flags)
    {
        if (flags == null)
        {
            flags = DEFAULT_FLAGS;
        }
      this.flags = flags;
    }
   
    /**
     * @return the serverMode
     * 
     * @jmx.managed-attribute 
     */
    public boolean isServerMode() 
    {
        return serverMode;
    }

    /**
     * @param serverMode
     * 
     * @jmx.managed-attribute 
     */
    public void setServerMode( boolean serverMode ) 
	{
       	this.serverMode = serverMode;
    }

    /**
     * @return the address
     * 
     * @jmx.managed-attribute 
     */
	public String getBindAddress() 
	{
		return address;
	}

    /**
     * @return the port
     * 
     * @jmx.managed-attribute 
     */
	public int getPort() 
	{
		return port;
	}

    /**
     * @param address
     * 
     * @jmx.managed-attribute 
     */
    public void setBindAddress(String address) 
    {
		this.address = address;
	}

    /**
     * @param port
     * 
     * @jmx.managed-attribute 
     */
    public void setPort(int port) 
    {
        this.port = port;
    }
    
    /**
     * Set the delay for remote hypersonic initialisation.
     * 
     * @jmx.managed-attribute
     */
    public void setDelay(final long delay)
    {
        this.delay = delay;
    }
    
    /**
     * Get the delay for remote hypersonic initialisation.
     * 
     * @jmx.managed-attribute
     */
    public long getDelay()
    {
        return delay;
    }
    
    // Lifecycle -----------------------------------------------------
   
    /**
     * Start the database
     */
    protected void startService() throws Exception
    {
        if (serverMode)
        {
            startRemoteDatabase();
        }
        else if (inMemoryMode)
        {
            startInMemoryDatabase();
        }
        else
        {
            startStandaloneDatabase();
        }
    }

    /**
     * We now close the connection clean by calling the
     * serverSocket throught jdbc. The MBeanServer calls this 
     * method at closing time.
     */
    protected void stopService() throws Exception
    {
        if (serverMode)
        {
            stopRemoteDatabase();
        }
        else if (inMemoryMode)
        {
            stopInMemoryDatabase();
        }
        else
        {
            stopStandaloneDatabase();
        }
    }

    // Private -------------------------------------------------------
   
    /**
     * Start the standalone (in process) database.
     */
    private void startStandaloneDatabase() throws Exception
    {
        final File h2Dir = checkDataDir() ;
               
        dbPath = new File(h2Dir, name);
        
        final String dbURL = JDBC_URL_PREFIX + dbPath.toURI().toString() + flags ;
        log.info(dbURL);
        
        // Check we have connectivity
        connection = getConnection(dbURL);
    }

    /**
    * Start the only in memory database.
    */
    private void startInMemoryDatabase() throws Exception
    {
        final String dbURL = JDBC_MEM_URL_PREFIX + name + flags ;

        // hold a connection so h2 does not close the database
        connection = getConnection(dbURL);
    }
   
    /**
     * Start a remote/server database
     * @throws Exception
     */
    private void startRemoteDatabase() throws Exception
    {
        final File h2Dir = checkDataDir() ;
        dbPath = new File(h2Dir, name);
       
        // Start DB in new thread, or else it will block us
        serverThread = new Thread("h2-" + name)
        {
            public void run()
            {
                try
                {
                	log.debug( "Starting remote h2 db with port : " + port );
                	final String[] args = new String[] { 
                			"-baseDir", dbPath.getAbsolutePath(), 
                			"-tcpPort", String.valueOf(port),
                			"-tcpAllowOthers","" }; //	need the extra empty string or a exception is thrown by H2
                	final Server server = Server.createTcpServer(args) ;
                	server.start() ;
                	setRemoteServer(server);
	            }
	            catch (Exception e)
	            {
	               log.error("Failed to start database", e);
                }
            }
        };
        
        serverThread.start();
        
        if (delay > 0)
        {
            log.debug("Waiting for Database initialisation: maximum " + delay + " milliseconds") ;
            try
            {
                serverThread.join(delay) ;
            }
            catch (final InterruptedException ie)
            {
                Thread.currentThread().interrupt() ;
            }
            
            if (serverThread.isAlive())
            {
                log.warn("Database initialisation is still active") ;
            }
            else
            {
                log.debug("Database initialisation completed") ;
            }
        }
    }

   /**
    * Stop the standalone (in process) database.
    */
   private void stopStandaloneDatabase() throws Exception
   {
       try
       {
           final Statement stmt = connection.createStatement() ;
           stmt.execute("shutdown") ;
       }
       finally
       {
           connection = null;
       }
       log.info("Database standalone closed clean");
   }

   /**
    * Stop the in memory database.
    */
   private void stopInMemoryDatabase() throws Exception
   {
       try
       {
           connection.close() ;
       }
       finally
       {
           connection = null;
       }
       log.info("Database in memory closed clean");
   }
   
   /**
    * Stop the remote database.
    */
   private void stopRemoteDatabase() throws SQLException 
   {
       final Server server = getRemoteServer() ;
       if (server != null)
       {
           server.stop() ;
       }
   }
   
   /**
    * Set the remote server instance.
    * @param remoteServer The remote server instance.
    */
   private synchronized void setRemoteServer(final Server remoteServer)
   {
       this.remoteServer = remoteServer ;
   }
   
   /**
    * Get the remote server instance.
    * @return The remote server instance.
    */
   private synchronized Server getRemoteServer()
   {
       return remoteServer ;
   }
   
    /**
     * Get the connection.
     * 
     * @param dbURL jdbc url.
     * @return the connection, allocate one if needed.
     * @throws Exception
     */
    private synchronized Connection getConnection(String dbURL) throws Exception
    {
        if (connection == null)
        {
            ClassLoader cl = Thread.currentThread().getContextClassLoader();
            Class.forName(JDBC_DRIVER_CLASS, true, cl).newInstance();
            connection = DriverManager.getConnection(dbURL, user, password);
        }
        return connection;
    }
   
    /**
     * Check the existence of the h2 data directory.
     * @return The h2 data directory.
     * @throws IOException For errors checking/creating the h2 data directory.
     */
    private File checkDataDir() throws IOException
    {
        // Get the server data directory
        final File dataDir = getDataDir();

        // Get DB directory
        final File h2Dir = new File(dataDir, H2_DATA_DIR);

        if (!h2Dir.exists())
        {
            h2Dir.mkdirs();
        }
        else if (!h2Dir.isDirectory())
        {
            throw new IOException("Failed to create directory: " + h2Dir);
        }
        return h2Dir ;
    }
   
    File getDataDir()
    {
        if (datadir == null)
        {
            final ServerConfigImpl serverConfig = (ServerConfigImpl) MBeanProxyExt.create(ServerConfigImplMBean.class,ServerConfigImplMBean.OBJECT_NAME);
            return serverConfig.getServerDataDir();
        }
        return new File(datadir);
    }

    public void setDataDir(String datadir)
    {
        this.datadir = datadir;
    }

}
