View Javadoc
1   /*
2    * $Source$
3    * $Revision$
4    *
5    * Copyright (C) 2007 Tim Pizey
6    *
7    * Part of Melati (http://melati.org), a framework for the rapid
8    * development of clean, maintainable web applications.
9    *
10   * Melati is free software; Permission is granted to copy, distribute
11   * and/or modify this software under the terms either:
12   *
13   * a) the GNU General Public License as published by the Free Software
14   *    Foundation; either version 2 of the License, or (at your option)
15   *    any later version,
16   *
17   *    or
18   *
19   * b) any version of the Melati Software License, as published
20   *    at http://melati.org
21   *
22   * You should have received a copy of the GNU General Public License and
23   * the Melati Software License along with this program;
24   * if not, write to the Free Software Foundation, Inc.,
25   * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA to obtain the
26   * GNU General Public License and visit http://melati.org to obtain the
27   * Melati Software License.
28   *
29   * Feel free to contact the Developers of Melati (http://melati.org),
30   * if you would like to work out a different arrangement than the options
31   * outlined here.  It is our intention to allow Melati to be used by as
32   * wide an audience as possible.
33   *
34   * This program is distributed in the hope that it will be useful,
35   * but WITHOUT ANY WARRANTY; without even the implied warranty of
36   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
37   * GNU General Public License for more details.
38   *
39   * Contact details for copyright holder:
40   *
41   *     Tim Pizey <timp At paneris.org>
42   *     http://paneris.org/~timp
43   */
44  package org.melati.poem;
45  
46  import java.util.Enumeration;
47  import java.util.Hashtable;
48  import java.util.Vector;
49  
50  /**
51   * @author timp
52   * @since 2 Feb 2007
53   * 
54   */
55  public final class PoemDatabaseFactory {
56  
57    private static final Hashtable<String,Database> databases = new Hashtable<String,Database>();
58  
59    private static PoemShutdownThread poemShutdownThread = new PoemShutdownThread();
60  
61    /**
62     * Disallow instantiation.
63     */
64    private PoemDatabaseFactory() {
65    }
66  
67    /**
68     * Retrieve the databases which have completed initialisation.
69     * 
70     * @return a <code>Vector</code> of the initialised databases
71     */
72    public static Vector<Database> initialisedDatabases() {
73      Vector<Database> dbs = new Vector<Database>();
74      Enumeration<String> e = null;
75      synchronized (databases) {
76        e = databases.keys();
77        while (e.hasMoreElements()) {
78          Database dbOrPending = databases.get(e.nextElement());
79          if (dbOrPending != pending)
80            dbs.addElement(dbOrPending);
81        }
82      }
83      return dbs;
84    }
85  
86    /**
87     * Retrieve the names of the databases which have completed initialisation.
88     * Note that a database which has not been used will not have been initialised.
89     * 
90     * @return a <code>Vector</code> of the initialised database names
91     */
92    public static Vector<String> getInitialisedDatabaseNames() {
93      Vector<String> dbs = new Vector<String>();
94      Enumeration<String> e = null;
95      synchronized (databases) {
96        e = databases.keys();
97        while (e.hasMoreElements()) {
98          String key = (String)e.nextElement();
99          Object dbOrPending = databases.get(key);
100         if (dbOrPending != pending)
101           dbs.addElement(key);
102       }
103     }
104     return dbs;
105   }
106 
107   private static final Database pending = new PoemDatabase();
108 
109   /**
110    * Retrieve a database by name.
111    * 
112    * @param name
113    *          the name of the database
114    * @throws DatabaseInitialisationPoemException
115    *           if any Exception is trapped
116    * @return a <code>Database</code> with the name specified
117    */
118   public static Database getDatabase(String name)
119           throws DatabaseInitialisationPoemException {
120     if (name == null)
121       throw new NullPointerException();
122 
123     Object dbOrPending;
124 
125     synchronized (databases) {
126       dbOrPending = databases.get(name);
127     }
128     if (dbOrPending == pending)
129       throw new ConnectionPendingException(name);
130     return (Database)dbOrPending;
131   }
132 
133   /**
134    * Return a database from the cache or create it. NOTE The first sucessful
135    * invocation will determine databases settings.
136    * 
137    * @param name
138    *          a short name of the db
139    * @param url
140    *          a JDBC url
141    * @param user
142    *          user authorised to access the databse through JDBC
143    * @param password
144    *          password for the user
145    * @param clazz
146    *          the name of the (POEM) database class
147    * @param dbmsClass
148    *          the name of the (POEM) dbms class
149    * @param addConstraints
150    *          whether to add constraints to the databases JDBC meta data
151    * @param logSQL
152    *          whether SQL statements should be logged
153    * @param logCommits
154    *          whether commits should be logged
155    * @param maxTransactions
156    *          the number of transactions (one less than the number of
157    *          connections)
158    * @return a new or existing database
159    */
160   public static Database getDatabase(String name, String url, String user,
161           String password, String clazz, String dbmsClass,
162           boolean addConstraints, boolean logSQL, boolean logCommits,
163           int maxTransactions) {
164 
165     Database dbOrPending;
166 
167     synchronized (databases) {
168       dbOrPending = databases.get(name);
169     }
170     if (dbOrPending == pending)
171       throw new ConnectionPendingException(name);
172     if (dbOrPending != null)
173       return (Database)dbOrPending;
174 
175     // Set an entry whilst we load
176     databases.put(name, pending);
177 
178     Database database = null;
179     try {
180 
181       try {
182         Object databaseObject = null;
183 
184         try {
185           databaseObject = Thread.currentThread().getContextClassLoader()
186                   .loadClass(clazz).newInstance();
187         } catch (Exception e) {
188           databaseObject = Class.forName(clazz).newInstance();
189         }
190 
191         if (!(databaseObject instanceof Database))
192           throw new ClassCastException("The .class=" + clazz
193                   + " entry named a class of type " + databaseObject.getClass()
194                   + ", " + "which is not an org.melati.poem.Database");
195 
196         database = (Database)databaseObject;
197 
198         // Set properties
199         database.setLogSQL(logSQL);
200         database.setLogCommits(logCommits);
201 
202         database.connect(name, dbmsClass, url, user, password, maxTransactions);
203 
204         if (addConstraints)
205           database.addConstraints();
206       } finally {
207         // get it removed from the "initialising" state even if an Error, such
208         // as no class found, occurs
209         databases.remove(name);
210       }
211 
212       databases.put(name, database);
213     } catch (Exception e) {
214       throw new DatabaseInitialisationPoemException(name, e);
215     }
216     return database;
217   }
218 
219   /**
220    * Enable a database to be reinitialised, without 
221    * incurring the full overhead of restarting hsqldb, 
222    * used in tests.
223    * 
224    * @param name
225    *          name of db to remove
226    */
227   public static void removeDatabase(String name) {
228     databases.remove(name);
229   }
230 
231   /**
232    * Disconnect and disconnect from a known database.
233    * 
234    * @param name
235    *          name of db to remove
236    */
237   public static void disconnectDatabase(String name) {
238     Database db = (Database)databases.get(name);
239     if(db != null && db.getCommittedConnection() != null) {
240       db.disconnect();
241       databases.remove(name);
242     } 
243   }
244 
245   /**
246    * Disconnect from all initialised databases.
247    */
248   public static void disconnectFromDatabases() { 
249     Vector<String> dbs = PoemDatabaseFactory.getInitialisedDatabaseNames();
250     Enumeration<String> them = dbs.elements();
251     while (them.hasMoreElements()) {
252       disconnectDatabase((String)them.nextElement());
253     }
254   }
255   /**
256    * Shutdown databases cleanly when JVM exits.
257    * 
258    * This method is called in one of two ways: when the jvm exits or when a 
259    * ServletContext is detroyed.
260    * Jetty, and presumably other servlet containers, registers its listeners as 
261    * shutdown hooks too, so there is no way of determining which is to be executed first.
262    * 
263    * If PoemDatabaseFactory has not been initialised before the ContextListener is 
264    * activated then this thread will be created but will fail to register.
265    * It will then be run by the ContextListener.
266    * 
267    * It may well be that the registered thread will be run before the ContextListener thread, 
268    * or visa versa, so there is a synchronised variable to ensure it actually does something 
269    * only the first time. 
270    * 
271    * @author timp
272    * @since 23 May 2007
273    * See org.melati.servlet.PoemServletContextListener
274    * 
275    */
276   public static class PoemShutdownThread extends Thread {
277     /** Constructor. */
278     public PoemShutdownThread() {
279       super();
280       setName("PoemShutdownThread");
281       try { 
282         Runtime.getRuntime().addShutdownHook(this);
283         System.err.println("\n*** PoemShutdownThread registered. ***\n");
284       } catch (IllegalStateException e) { 
285         System.err.println("\n*** PoemShutdownThread tried to register during shutdown. ***\n");
286         
287         // Happens when the PoemServletContextListener 
288         // tries to shutdown databases when none have yet been 
289         // initialised.
290         e = null;
291       }
292     }
293     private static Boolean haveRun = Boolean.FALSE;
294     /**
295      * {@inheritDoc}
296      * 
297      * @see java.lang.Thread#run()
298      */
299     public void run() {
300       synchronized(haveRun) { 
301         if (!haveRun.booleanValue()) {
302           haveRun = Boolean.TRUE;
303           try { 
304             boolean removed = Runtime.getRuntime().removeShutdownHook(this);
305             System.err.println("\n*** PoemShutdownThread removed: " + removed + " ***\n");
306           } catch (IllegalStateException e) { 
307             System.err.println("\n*** PoemShutdownThread cannot be removed at this stage. ***\n");
308             // I think this happens during normal termination, 
309             // you cannot remove a hook during shutdown, 
310             // but if we are run when not in shutdown 
311             // we do want to be removed.
312             e = null;
313           }
314           System.err.println("*** PoemShutdownThread starting to shutdown dbs. ***");
315           disconnectFromDatabases();
316           System.err.println("*** PoemShutdownThread has shutdown dbs. ***");
317         } else 
318           System.err.println("*** PoemShutdownThread has already run. ***");
319       }
320     }
321   }
322   /**
323    * @return the poemShutdownThread
324    */
325   public static PoemShutdownThread getPoemShutdownThread() {
326     return poemShutdownThread;
327   }
328 
329 }