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 }