001: package simpleorm.core;
002:
003: import java.sql.*;
004: import java.util.HashMap;
005: import java.util.Iterator;
006:
007: /*
008: * Copyright (c) 2002 Southern Cross Software Queensland (SCSQ). All rights
009: * reserved. See COPYRIGHT.txt included in this distribution.
010: */
011:
012: /** Links JDBC Connections to SimpleORM ones and handles transacitons
013: and flushing. <p>
014:
015: Associates existing JDBC transactions with the current thread so that
016: they can be used by other SimpleORM classes. Associating the
017: connection with threads avoids nasty threading problems that can
018: otherwise arrise.<p>
019:
020: SimpleORM takes no interest in how the JDBC connection was actually
021: obtained. It might be obtained directly using
022: DriverManager.getConnection(), connection pooling software such as
023: BitMechanic, or from a J2EE application server. SimpleORM therefor
024: does not restrict the environments in which it can be used.<P>
025:
026: But note that it is very important that transactions ONLY be COMMITED
027: by SimpleORM class, and NOT by raw JDBC calls.<P>
028:
029: All transactions must start with a {@link #begin} and end with
030: either a {@link #commit} or a {@link #rollback}.
031: <code>commit</code> also {@link #flush}es any outsanding changes.
032:
033: (The complex AWT/Swing thread pattern is to have the main thread
034: sets up the forms, and then exit. A second AWT thread then does the
035: real work. In other words a single threaded application pointlessly
036: using two threads! SimpleORM works well with Swing but one needs to
037: use <code>invokeAndWait()</code> to set up the forms the AWT thread,
038: which is much cleaner anyway. See
039: {@link simpleorm.examples.SwingTest} for an example.)<p>
040:
041: This is specialized by SConnectionEJB for use within JTA transaction management.
042: **/
043:
044: public class SConnection extends simpleorm.properties.SPropertyMap
045: implements SConstants {
046: private static SThreadLocal threadSConnection = new SThreadLocal();
047: private static long nrCons = 0;
048:
049: Connection jdbcConnection = null;
050: Connection jdbcSequenceConnection = null;
051: SDriver sDriver = null;
052:
053: /** The getMetaData() url. Saved here so that toString can dispaly
054: it even on a closed connection. */
055: String url = null;
056:
057: /** Transaction is active between begin and commit.*/
058: boolean hasBegun = false;
059:
060: /** Data has been flushed but not committed in this transaction. */
061: boolean uncommittedFlushes = false;
062:
063: /** Only to identify connection for toString() */
064: long conNr = ++nrCons;
065:
066: /** Provided by user to identify thread, eg. HTML session id. Keep
067: it short. */
068: String name = "";
069:
070: /** Of all records retrieved. Both key and body are
071: SRecordInstances, which has redefined equals() and hashTable(); */
072: HashMap transactionCache = new HashMap();
073:
074: /** Ordered list of SRecordInstances to flush and purge. */
075: SArrayList updateList = new SArrayList(20);
076:
077: /** Enables Extra internal validations throughout SimpleORM. Cheap
078: tests are always enabled, but this can be used to turn on tests
079: that are expensive in times of trouble. */
080: static boolean expensiveValidations = false;
081:
082: /** No direct creation of Connections. */
083: protected SConnection() {
084: }
085:
086: /** True iff commit must be called before the connection there are any (dirty) records that need to be
087: committed, regardless of whether they have been flushed.
088: (Overriden in SConnectionEJB.)*/
089: public boolean mustCommitBeforeDetaching() {
090: return updateList.size() > 0 || uncommittedFlushes;
091: }
092:
093: /** Allows SConnection to be subclassed.
094: * connection should normally be null, in which case conf.obtainPrimaryJDBCConnection
095: * is used to open it.
096: * (rawConnection and sDriverName are deprecated.)
097: */
098: protected void innerAttach(SDataSource source,
099: String connectionName, Connection rawConnection,
100: SDriver driver) {
101: name = connectionName;
102:
103: SConnection badscon = getConnection();
104: if (badscon != null)
105: throw new SException.Error(
106: "Thread's SConnection already open " + badscon);
107:
108: dataSource = source;
109: Connection con = rawConnection != null ? rawConnection
110: : dataSource.openPrimaryDBConnection();
111: try {
112: if (con == null || con.isClosed())
113: throw new SException.Error("Connection " + con
114: + " is not open.");
115: } catch (Exception ex) {
116: throw new SException.JDBC(ex);
117: }
118:
119: jdbcConnection = con;
120: rawAttach();
121:
122: try {
123: url = con.getMetaData().getURL();
124: } catch (Exception eu) {
125: throw new SException.JDBC(eu);
126: }
127:
128: if (dataSource == null)
129: sDriver = driver == null ? SDriver.newSDriver(con, null)
130: : driver;
131: else
132: sDriver = SDriver.newSDriver(con, dataSource
133: .getSDriverName());
134: try {
135: if (jdbcConnection.getAutoCommit())
136: jdbcConnection.setAutoCommit(false);
137: } catch (Exception ex) {
138: throw new SException.JDBC(ex);
139: }
140: String jvsn = SJSharp.jvmVersion();
141: SLog.slog.connections("Attached Connection " + this
142: + " SimpleORM " + SUte.simpleormVersion() + " jdk "
143: + jvsn);
144: }
145:
146: static SDataSource dataSource;
147:
148: public SDataSource getDataSource() {
149: return dataSource;
150: }
151:
152: protected void rawAttach() { // Overriden in SConnectionEJB
153: threadSConnection.set(this );
154: }
155:
156: /**
157: * Attaches a SimpleORM connection to the current thread based on conf.
158: * Opens a JDBC connection.<p>
159: *
160: * connectionName is a simple name that can be used to identify this connection
161: * in log traces etc. Eg. the session ID plus the user name. But keep it short
162: * for logging.<p>
163: *
164: * The driver is usually determined automatically (if null) based on the conf,
165: * but if it is overriden make sure to create a new driver instance for each connection.<p>
166: *
167: * @see SDataSource for details.
168: */
169: public static void attach(SDataSource source, String connectionName) {
170: new SConnection().innerAttach(source, connectionName, null,
171: null);
172: }
173:
174: /** Attaches the already opened JDBC Connection <code>con</code> to
175: the current thread for future processing by SimpleORM.
176:
177: @deprecated Use attach() or attach(source) instead which can open mulitple connections needed for some key generation algorithms.
178: @see SDataSource for details.
179: */
180: public static void attach(Connection con, String connectionName,
181: SDriver driver) {
182: new SConnection()
183: .innerAttach(null, connectionName, con, driver);
184: }
185:
186: /** Attaches the already opened JDBC Connection <code>con</code> to
187: the current thread for future processing by SimpleORM.
188:
189: @deprecated Use attach() or attach(source) instead which can open mulitple connections needed for some key generation algorithms.
190: @see SDataSource for details.
191: */
192: public static void attach(Connection con, String connectionName) {
193: new SConnection().innerAttach(null, connectionName, con, null);
194: }
195:
196: /**
197: * Retrieve or create the SConnection associated with this "Context".
198: * Normally this just means (indirectly) call getThreadedConnection
199: * which finds the connection associated with this thread.
200: * However, if EJB.SConnectionEJB is loaded, then this can be dispached
201: * to getTransactionConnection instead.
202: * <p>
203: *
204: It may or may not be begun. May be used to set properties for
205: the connection, which persist between transactions but not
206: between attach ments.*/
207: static public SConnection getConnection() {
208: return connectionGetter.getAConnection();
209: }
210:
211: static SConnection getThreadedConnection() {
212: SConnection scon = (SConnection) threadSConnection.get();
213: return scon;
214: }
215:
216: static protected ConnectionGetter connectionGetter = new ConnectionGetter();
217:
218: /**
219: * Just dispaches getConnection to either SConnection or SConnectionEJB if it was loaded.<p>
220: *
221: * Normally connections are associated with the current thread and SConnectionEJB is not
222: * a core part of SimpleORM and so not loaded.
223: * But if SConnectionEJB (or similar) is loaded then it has to be able to intercept
224: * the SConnection.getConnection call used by all other SimpleORM methods such as
225: * findOrCreate. So SConnectionEJB overrides the connectionGetter static when it loads
226: * so that it can redirect to itself, and then specialize SConnection methods as needed.<p>
227: *
228: * Do not worry about this unless you are using EJB JTA.
229: * Note that this has nothing to do with the creation of jdbc connections.
230: */
231: protected static class ConnectionGetter {
232: protected SConnection getAConnection() {
233: return SConnection.getThreadedConnection();
234: }
235: }
236:
237: /** Gets current SConnection object for thread. Throws an exception
238: if not attached and open. */
239: private static SConnection getOpenedConnection() {
240: SConnection scon = getConnection();
241: if (scon == null || scon.jdbcConnection == null)
242: throw new SException.Error(
243: "Bad SConnection -- Not opened/attached ");
244: try {
245: if (expensiveValidations && scon.jdbcConnection.isClosed())
246: throw new SException.Error("Bad SConnection -- Closed "
247: + scon.jdbcConnection);
248: } catch (Exception ex) {
249: throw new SException.JDBC(ex);
250: }
251: return scon;
252: }
253:
254: /** Gets current SConnection object for thread. Throws an exception
255: if not attached and open AND between begin() and
256: commit() or rollback(). Package local.*/
257: static SConnection getBegunConnection() {
258: SConnection scon = getOpenedConnection();
259: if (!scon.hasBegun())
260: throw new SException.Error("Transaction not begin()ed.");
261: return scon;
262: }
263:
264: /** Transaction is active between begin and commit.*/
265: public boolean hasBegun() {
266: return hasBegun;
267: }
268:
269: /** If the JDBC connection has been attached and not closed. */
270: public boolean isOpened() {
271: try {
272: return jdbcConnection != null && !jdbcConnection.isClosed();
273: } catch (SQLException ex) {
274: return false;
275: }
276: }
277:
278: /** Asserts transaction Begin()ed and returns the JDBC connection.
279: Can be used to integrate direct JDBC calls with the SimpleORM
280: connection. Dangerous. Take care with caching and never use
281: this to commit a transaction. Should only be rarely used in
282: practice. */
283: public static Connection getBegunDBConnection() {
284: SConnection scon = getBegunConnection();
285: return scon.jdbcConnection;
286: }
287:
288: /**
289: @deprecated Renamed to getBegunDB Connection (Might not beJDBC if .Net).
290: */
291: public static Connection getBegunJDBCConnection() {
292: return getBegunDBConnection();
293: }
294:
295: /**
296: * Returns a secondary jdbc connection for generating sequence numbers, if required.
297: * Creates a new one if necessary.
298: */
299: Connection getSequenceDBConnection() {
300: if (jdbcSequenceConnection == null) {
301: if (getDataSource() == null)
302: throw new SException.Error(
303: "No SDataSource supplied to create secondary connection "
304: + this );
305: jdbcSequenceConnection = getDataSource()
306: .openSecondaryDBConnection();
307: }
308: return jdbcSequenceConnection;
309: }
310:
311: /** Detaches the SConnection from the current thread without
312: closing the JDBC connection so that it can be reattached to another thread.
313: Rarely used, <code>detachAndClose</code> is normally what is used.<p>
314:
315: Checks and throws and issues a message if there are any unflushed
316: records that have been updated. One should normally commit() or
317: rollback() before detaching unless embedded within an EJB, in
318: which flush() is required.
319: */
320: public static void detachWithoutClosing() {
321: SConnection scon = getConnection();
322: SLog.slog.connections("Detaching Connection " + scon);
323: if (scon == null)
324: return; // Eg. if exception during long transaction.
325: if (scon.hasBegun && scon.mustCommitBeforeDetaching()) {
326: // throw new SException.Error(...)
327: // Does not behave well in finally clauses -- if the try fails
328: // this then throws another "not committed" exception which masks
329: // the first. No known work arround in Java!
330: SLog.slog
331: .error("Transaction has unflushed updated records. This is normally caused by an unrelated Exception throwing to the finally block in which case ignore this message. But if no other exception then a commit() probably missing.");
332: }
333: destroyAll(); // For J2EE case where flush/detach used.
334: scon.jdbcConnection = null;
335: scon.jdbcSequenceConnection = null;
336: scon.rawDetach();
337: }
338:
339: protected void rawDetach() { // Overriden in SConnectionEJB
340: threadSConnection.set(null);
341: }
342:
343: /**
344: * Closes the JDBC connection and then calls
345: * <code>detachWithoutClosing</code> to detach the SimpleORM connection
346: * from the current thread. Should usually be put in a finally clause. No
347: * error if already detached or closed so safe in finally clauses.
348: * <p>
349: */
350: public static void detachAndClose() {
351: SConnection scon = getConnection();
352: if (scon != null) {
353: closeCon(scon.jdbcConnection);
354: closeCon(scon.jdbcSequenceConnection);
355: }
356: detachWithoutClosing();
357: }
358:
359: private static void closeCon(Connection con) {
360: boolean isOpen = false;
361: try {
362: isOpen = con != null && !con.isClosed();
363: } catch (Exception ex) {
364: throw new SException.JDBC("isClosed ", ex);
365: }
366: if (isOpen) {
367: try {
368: con.rollback(); // If transaction open and
369: // exception, esp. for DB2.
370: } catch (Exception ex) {
371: throw new SException.JDBC("Error rollback " + con, ex);
372: }
373: try {
374: con.close();
375: } catch (Exception ex) {
376: throw new SException.JDBC("Error closing " + con, ex);
377: }
378: }
379: }
380:
381: /** Convenience routine for doing bulk updates using raw JDBC.
382: Dangerous. Take care with caching and never use this to commit
383: a transaction. Returns number of rows updated.
384: Does nothing if sql == null.
385: */
386: public static int rawUpdateDB(String sql, Object[] params) {
387: SLog.slog.updates("rawDB " + sql);
388: int res = -1;
389: if (sql != null) {
390: Connection con = getBegunDBConnection();
391: try {
392: PreparedStatement ps = con.prepareStatement(sql);
393: for (int px = 0; px < params.length; px++) {
394: ps.setObject(px + 1, params[px]);
395: }
396: res = ps.executeUpdate();
397: ps.close();
398: } catch (Exception ex) {
399: throw new SException.JDBC("SQL: " + sql, ex);
400: }
401: }
402: return res;
403: }
404:
405: public static int rawUpdateDB(String sql, Object param) {
406: return rawUpdateDB(sql, new Object[] { param });
407: }
408:
409: public static int rawUpdateDB(String sql) {
410: return rawUpdateDB(sql, new String[0]);
411: }
412:
413: /**
414: @deprecated Renamed to rawUpdateDB Connection (Might not beJDBC).
415: */
416: public static int rawUpdateJDBC(String sql, Object[] params) {
417: return rawUpdateDB(sql, params);
418: }
419:
420: /**
421: @deprecated Renamed to rawUpdateDB Connection (Might not beJDBC).
422: */
423: public static int rawUpdateJDBC(String sql, Object param) {
424: return rawUpdateDB(sql, param);
425: }
426:
427: /**
428: @deprecated Renamed to rawUpdateDB Connection (Might not beJDBC).
429: */
430: public static int rawUpdateJDBC(String sql) {
431: return rawUpdateDB(sql);
432: }
433:
434: /** Utility routine for dropping tables. Hides JDBC exception
435: caused if the table does not exist. Mainly used in test cases.
436: (Does not use the SRecord objects so that they do not all have
437: to be loaded during specific tests.) Dispatched to SDriver.<p>
438:
439: WARNING: JDBC error suppression is crude -- a table may indeed
440: exist and still not be dropped for other reasons,
441: eg. referential integrity. <p>
442:
443: WARNING: Due to bugs in JDBC etc. each dropped table must be in
444: its own transaction in case of errors. This routine commits changes.*/
445: public static void dropTableNoError(String table) {
446: if (getBegunConnection().mustCommitBeforeDetaching())
447: throw new SException.Error(
448: "Uncommited Updates need to be committed.");
449: getDriver().dropTableNoError(table);
450: commit();
451: begin();
452: }
453:
454: /** Convenience routine for doing single row queries using raw JDBC.
455: Be sure to flush the cache appropriately, or you will query
456: behind it.<p>
457:
458: if <code>nrColumns == 0</code> (default) returns one result
459: column as a non array object, otherwise returns an array of
460: results. (nrColumns avoids the need to use JDBC meta data which
461: can be buggy.) */
462: public static Object rawQueryDB(String sql, String[] params,
463: int nrColumns) {
464: Connection con = getBegunJDBCConnection();
465: try {
466: PreparedStatement ps = con.prepareStatement(sql);
467: for (int px = 0; params != null && px < params.length; px++) {
468: ps.setString(px + 1, params[px]);
469: }
470: ResultSet rs = ps.executeQuery();
471: Object res = null;
472: if (rs.next()) {
473: if (nrColumns == 0)
474: res = rs.getObject(1);
475: else {
476: Object[] ares = new Object[nrColumns];
477: for (int rx = 0; rx < nrColumns; rx++)
478: ares[rx] = rs.getObject(rx + 1);
479: res = ares;
480: }
481: if (rs.next())
482: throw new SException.Error(
483: "Query returned multiple rows " + sql
484: + SUte.arrayToString(params));
485: }
486: rs.close();
487: ps.close();
488: if (SLog.slog.enableQueries())
489: SLog.slog.queries("rawJDBC " + sql + " -- " + res);
490: return res;
491: } catch (Exception ex) {
492: throw new SException.JDBC(ex);
493: }
494: }
495:
496: public static Object rawQueryDB(String sql, String[] params) {
497: return rawQueryDB(sql, params, 0);
498: }
499:
500: public static Object rawQueryDB(String sql, String param) {
501: return rawQueryDB(sql, new String[] { param });
502: }
503:
504: public static Object rawQueryDB(String sql) {
505: return rawQueryDB(sql, new String[0]);
506: }
507:
508: /** @deprecated Renamed rawQueryDB */
509: public static Object rawQueryJDBC(String sql, String[] params,
510: int nrColumns) {
511: return rawQueryDB(sql, params, nrColumns);
512: }
513:
514: public static Object rawQueryJDBC(String sql, String[] params) {
515: return rawQueryDB(sql, params);
516: }
517:
518: public static Object rawQueryJDBC(String sql, String param) {
519: return rawQueryDB(sql, param);
520: }
521:
522: public static Object rawQueryJDBC(String sql) {
523: return rawQueryDB(sql);
524: }
525:
526: /** Start a new transaction. A JDBC connection must already be
527: attached to this thread, and must not be mid transaction. */
528: public static void begin() {
529: SConnection scon = getOpenedConnection();
530: if (scon.hasBegun)
531: throw new SException.Error("Transaction already Begun.");
532: if (scon.transactionCache.size() != 0
533: || scon.updateList.size() != 0)
534: throw new SException.InternalError(
535: "Non empty cache/update list.");
536: scon.hasBegun = true;
537: SLog.slog.connections("Begun Connection " + scon);
538: }
539:
540: /** Flush and purge the transaction cache to the database, and then
541: commit the transaction. Note that this is the only way that a
542: transaction should be commited -- do not use JDBC commit
543: directly. Once commited, <code>begin()</code> must be used to
544: start the next transaction.*/
545: public static void commit() {
546: SConnection scon = getBegunConnection();
547: flush();
548: destroyAll();
549:
550: try {
551: scon.jdbcConnection.commit();
552: } catch (Exception ex) {
553: throw new SException.JDBC(ex);
554: }
555: scon.uncommittedFlushes = false;
556: scon.hasBegun = false;
557: SLog.slog.connections("Committed Connection " + scon);
558: }
559:
560: /** Purges the cache and rolls back the transaction. Any uncommited
561: updates to the database are also rolled back. */
562: public static void rollback() {
563: destroyAll();
564: SConnection scon = getBegunConnection();
565: try {
566: scon.jdbcConnection.rollback();
567: } catch (Exception ex) {
568: throw new SException.JDBC(ex);
569: }
570: scon.uncommittedFlushes = false;
571: scon.hasBegun = false;
572: SLog.slog.connections("Rolled Back Connection " + scon);
573: }
574:
575: /** Flush all records of tables in this transaction to the database. */
576: public static void flush() {
577: SConnection scon = getBegunConnection();
578: for (int i = 0; i < scon.updateList.size(); i++) { // Jan reckons iterator causes grief?
579: SRecordInstance ri = (SRecordInstance) scon.updateList
580: .get(i);
581: if (ri != null)
582: ri.flush(); // Could have been manually flushed.
583: }
584: scon.updateList.clear();
585: }
586:
587: /** Flushes and Purges all record instances. Can be used before a
588: raw JDBC update to ensure that the cache remains consistent
589: after the query.
590:
591: @see SRecordInstance#flushAndPurge
592: @see SRecordMeta#flushAndPurge
593: */
594: public static void flushAndPurge() {
595: flush();
596: destroyAll();
597: }
598:
599: /** Destroy all records in all tables so that they cannot be used
600: again and to encourage garbage collection. */
601: static void destroyAll() {
602: SConnection scon = getConnection();
603: Iterator ci = scon.transactionCache.values().iterator();
604: while (ci.hasNext()) {
605: SRecordInstance ri = (SRecordInstance) ci.next();
606: ri.incompleteDestroy();
607: }
608: scon.transactionCache.clear();
609: scon.updateList.clear();
610: }
611:
612: /** Dumps out the entire cache of records for this connection. For
613: debugging wierd bugs only. */
614: public static void dumpCache() {
615: SLog.slog.message("DumpCache");
616: SConnection scon = getConnection();
617: Iterator ci = scon.transactionCache.values().iterator();
618: while (ci.hasNext()) {
619: SRecordInstance ri = (SRecordInstance) ci.next();
620: SLog.slog.message(" " + ri
621: + (ri.isDirty() ? " Dirty" : ""));
622: }
623: }
624:
625: /** Eg. if (getDriver() instanceOf SDriverPostgres) ...
626: * Also getDriver().setMyFavoritePerConnectionParameter.
627: */
628: static public SDriver getDriver() {
629: return getConnection().sDriver;
630: }
631:
632: /** Enables the SimpleORM connection to be disassociated with the
633: current thread, and then associated with another thread.
634: Dangerous. When you pick up the connection you also pick up the
635: hash map of records retrieved during the transaction.<p>
636:
637: This is provided for completeness only. Note that both Swing
638: and EJBs can be used without doing this (see the examples). If
639: you end up with two threads accessing the same connection at the
640: same time you will have horrible, unreproduceable bugs.<p>
641:
642: Don not do it unless you really have to. */
643: public static SConnection unsafeDetachFromThread() {
644: SConnection scon = (SConnection) threadSConnection.get();
645: SLog.slog.connections("unsafeDetachFromThread " + scon);
646: threadSConnection.set(null);
647: return scon;
648: }
649:
650: /** Re-attacheds a detached connection to the current thread.
651:
652: @see #unsafeDetachFromThread
653: */
654: public void unsafeAttachToThread() {
655: SConnection scon = (SConnection) threadSConnection.get();
656: if (scon != null)
657: throw new SException.Error(
658: "This thread already has connection " + scon);
659: threadSConnection.set(this );
660: SLog.slog.connections("unsafeAttachToThread " + this );
661: }
662:
663: /** Short connection/thread identifier for inclusion in log messages
664: to enable output from different threads to be separated. */
665: static String shortToString() {
666: SConnection con = getConnection();
667: if (con != null)
668: return con.conNr + "." + con.name;
669: else
670: return "No Connection";
671: }
672:
673: /*
674: public static static void flush(Class rec){}
675: public void flush(SRecordMeta rec){} // This record
676: public static void purge(){} // ie. and remove from cache.
677: public static void purge(Class rec){}
678: public void purge(SRecordMeta rec){}
679: */
680:
681: public String toString() {
682: return "[SConnection " + conNr + "_" + name + ": " + url + "]";
683: }
684: }
|