PoemDatabaseFactory.java
/*
* $Source$
* $Revision$
*
* Copyright (C) 2007 Tim Pizey
*
* Part of Melati (http://melati.org), a framework for the rapid
* development of clean, maintainable web applications.
*
* Melati is free software; Permission is granted to copy, distribute
* and/or modify this software under the terms either:
*
* a) the GNU General Public License as published by the Free Software
* Foundation; either version 2 of the License, or (at your option)
* any later version,
*
* or
*
* b) any version of the Melati Software License, as published
* at http://melati.org
*
* You should have received a copy of the GNU General Public License and
* the Melati Software License along with this program;
* if not, write to the Free Software Foundation, Inc.,
* 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA to obtain the
* GNU General Public License and visit http://melati.org to obtain the
* Melati Software License.
*
* Feel free to contact the Developers of Melati (http://melati.org),
* if you would like to work out a different arrangement than the options
* outlined here. It is our intention to allow Melati to be used by as
* wide an audience as possible.
*
* This program 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 General Public License for more details.
*
* Contact details for copyright holder:
*
* Tim Pizey <timp At paneris.org>
* http://paneris.org/~timp
*/
package org.melati.poem;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.Vector;
/**
* @author timp
* @since 2 Feb 2007
*
*/
public final class PoemDatabaseFactory {
private static final Hashtable<String,Database> databases = new Hashtable<String,Database>();
private static PoemShutdownThread poemShutdownThread = new PoemShutdownThread();
/**
* Disallow instantiation.
*/
private PoemDatabaseFactory() {
}
/**
* Retrieve the databases which have completed initialisation.
*
* @return a <code>Vector</code> of the initialised databases
*/
public static Vector<Database> initialisedDatabases() {
Vector<Database> dbs = new Vector<Database>();
Enumeration<String> e = null;
synchronized (databases) {
e = databases.keys();
while (e.hasMoreElements()) {
Database dbOrPending = databases.get(e.nextElement());
if (dbOrPending != pending)
dbs.addElement(dbOrPending);
}
}
return dbs;
}
/**
* Retrieve the names of the databases which have completed initialisation.
* Note that a database which has not been used will not have been initialised.
*
* @return a <code>Vector</code> of the initialised database names
*/
public static Vector<String> getInitialisedDatabaseNames() {
Vector<String> dbs = new Vector<String>();
Enumeration<String> e = null;
synchronized (databases) {
e = databases.keys();
while (e.hasMoreElements()) {
String key = (String)e.nextElement();
Object dbOrPending = databases.get(key);
if (dbOrPending != pending)
dbs.addElement(key);
}
}
return dbs;
}
private static final Database pending = new PoemDatabase();
/**
* Retrieve a database by name.
*
* @param name
* the name of the database
* @throws DatabaseInitialisationPoemException
* if any Exception is trapped
* @return a <code>Database</code> with the name specified
*/
public static Database getDatabase(String name)
throws DatabaseInitialisationPoemException {
if (name == null)
throw new NullPointerException();
Object dbOrPending;
synchronized (databases) {
dbOrPending = databases.get(name);
}
if (dbOrPending == pending)
throw new ConnectionPendingException(name);
return (Database)dbOrPending;
}
/**
* Return a database from the cache or create it. NOTE The first sucessful
* invocation will determine databases settings.
*
* @param name
* a short name of the db
* @param url
* a JDBC url
* @param user
* user authorised to access the databse through JDBC
* @param password
* password for the user
* @param clazz
* the name of the (POEM) database class
* @param dbmsClass
* the name of the (POEM) dbms class
* @param addConstraints
* whether to add constraints to the databases JDBC meta data
* @param logSQL
* whether SQL statements should be logged
* @param logCommits
* whether commits should be logged
* @param maxTransactions
* the number of transactions (one less than the number of
* connections)
* @return a new or existing database
*/
public static Database getDatabase(String name, String url, String user,
String password, String clazz, String dbmsClass,
boolean addConstraints, boolean logSQL, boolean logCommits,
int maxTransactions) {
Database dbOrPending;
synchronized (databases) {
dbOrPending = databases.get(name);
}
if (dbOrPending == pending)
throw new ConnectionPendingException(name);
if (dbOrPending != null)
return (Database)dbOrPending;
// Set an entry whilst we load
databases.put(name, pending);
Database database = null;
try {
try {
Object databaseObject = null;
try {
databaseObject = Thread.currentThread().getContextClassLoader()
.loadClass(clazz).newInstance();
} catch (Exception e) {
databaseObject = Class.forName(clazz).newInstance();
}
if (!(databaseObject instanceof Database))
throw new ClassCastException("The .class=" + clazz
+ " entry named a class of type " + databaseObject.getClass()
+ ", " + "which is not an org.melati.poem.Database");
database = (Database)databaseObject;
// Set properties
database.setLogSQL(logSQL);
database.setLogCommits(logCommits);
database.connect(name, dbmsClass, url, user, password, maxTransactions);
if (addConstraints)
database.addConstraints();
} finally {
// get it removed from the "initialising" state even if an Error, such
// as no class found, occurs
databases.remove(name);
}
databases.put(name, database);
} catch (Exception e) {
throw new DatabaseInitialisationPoemException(name, e);
}
return database;
}
/**
* Enable a database to be reinitialised, without
* incurring the full overhead of restarting hsqldb,
* used in tests.
*
* @param name
* name of db to remove
*/
public static void removeDatabase(String name) {
databases.remove(name);
}
/**
* Disconnect and disconnect from a known database.
*
* @param name
* name of db to remove
*/
public static void disconnectDatabase(String name) {
Database db = (Database)databases.get(name);
if(db != null && db.getCommittedConnection() != null) {
db.disconnect();
databases.remove(name);
}
}
/**
* Disconnect from all initialised databases.
*/
public static void disconnectFromDatabases() {
Vector<String> dbs = PoemDatabaseFactory.getInitialisedDatabaseNames();
Enumeration<String> them = dbs.elements();
while (them.hasMoreElements()) {
disconnectDatabase((String)them.nextElement());
}
}
/**
* Shutdown databases cleanly when JVM exits.
*
* This method is called in one of two ways: when the jvm exits or when a
* ServletContext is detroyed.
* Jetty, and presumably other servlet containers, registers its listeners as
* shutdown hooks too, so there is no way of determining which is to be executed first.
*
* If PoemDatabaseFactory has not been initialised before the ContextListener is
* activated then this thread will be created but will fail to register.
* It will then be run by the ContextListener.
*
* It may well be that the registered thread will be run before the ContextListener thread,
* or visa versa, so there is a synchronised variable to ensure it actually does something
* only the first time.
*
* @author timp
* @since 23 May 2007
* See org.melati.servlet.PoemServletContextListener
*
*/
public static class PoemShutdownThread extends Thread {
/** Constructor. */
public PoemShutdownThread() {
super();
setName("PoemShutdownThread");
try {
Runtime.getRuntime().addShutdownHook(this);
System.err.println("\n*** PoemShutdownThread registered. ***\n");
} catch (IllegalStateException e) {
System.err.println("\n*** PoemShutdownThread tried to register during shutdown. ***\n");
// Happens when the PoemServletContextListener
// tries to shutdown databases when none have yet been
// initialised.
e = null;
}
}
private static Boolean haveRun = Boolean.FALSE;
/**
* {@inheritDoc}
*
* @see java.lang.Thread#run()
*/
public void run() {
synchronized(haveRun) {
if (!haveRun.booleanValue()) {
haveRun = Boolean.TRUE;
try {
boolean removed = Runtime.getRuntime().removeShutdownHook(this);
System.err.println("\n*** PoemShutdownThread removed: " + removed + " ***\n");
} catch (IllegalStateException e) {
System.err.println("\n*** PoemShutdownThread cannot be removed at this stage. ***\n");
// I think this happens during normal termination,
// you cannot remove a hook during shutdown,
// but if we are run when not in shutdown
// we do want to be removed.
e = null;
}
System.err.println("*** PoemShutdownThread starting to shutdown dbs. ***");
disconnectFromDatabases();
System.err.println("*** PoemShutdownThread has shutdown dbs. ***");
} else
System.err.println("*** PoemShutdownThread has already run. ***");
}
}
}
/**
* @return the poemShutdownThread
*/
public static PoemShutdownThread getPoemShutdownThread() {
return poemShutdownThread;
}
}