001: /*
002: * This software is released under a licence similar to the Apache Software Licence.
003: * See org.logicalcobwebs.proxool.package.html for details.
004: * The latest version is available at http://proxool.sourceforge.net
005: */
006: package org.logicalcobwebs.proxool;
007:
008: import org.apache.commons.logging.Log;
009: import org.apache.commons.logging.Log;
010:
011: import java.lang.reflect.Method;
012: import java.sql.Connection;
013: import java.sql.SQLException;
014: import java.util.HashMap;
015: import java.util.HashSet;
016: import java.util.Iterator;
017: import java.util.Map;
018: import java.util.Set;
019:
020: /**
021: * Responsible for resetting a Connection to its default state when it is
022: * returned to the pool. It must be initialised by the first Connection that
023: * is made (for each pool) so that we don't make any assumptions about
024: * what the default values are.
025: *
026: * @version $Revision: 1.16 $, $Date: 2006/01/18 14:40:01 $
027: * @author Bill Horsman (bill@logicalcobwebs.co.uk)
028: * @author $Author: billhorsman $ (current maintainer)
029: * @since Proxool 0.5
030: */
031: public class ConnectionResetter {
032:
033: private Log log;
034:
035: /**
036: * @see #initialise
037: */
038: private boolean initialised;
039:
040: /**
041: * @see #addReset
042: * @see #reset
043: */
044: private Map accessorMutatorMap = new HashMap();
045:
046: /**
047: * @see #addReset
048: * @see #reset
049: */
050: private Map defaultValues = new HashMap();
051:
052: /**
053: * We use this to guess if we are changing a property that will need resetting
054: */
055: protected static final String MUTATOR_PREFIX = "set";
056:
057: private String driverName;
058:
059: /**
060: * @see #isTriggerResetException()
061: */
062: protected static boolean triggerResetException;
063:
064: /**
065: * Pass in the log to use
066: * @param log debug information sent here
067: */
068: protected ConnectionResetter(Log log, String driverName) {
069: this .log = log;
070: this .driverName = driverName;
071:
072: // Map all the reset methods
073: addReset("getCatalog", "setCatalog");
074: addReset("isReadOnly", "setReadOnly");
075: addReset("getTransactionIsolation", "setTransactionIsolation");
076: addReset("getTypeMap", "setTypeMap");
077: addReset("getHoldability", "setHoldability");
078: }
079:
080: /**
081: * Add a pair of methods that need resetting each time a connection is
082: * put back in the pool
083: * @param accessorName the name of the "getter" method (e.g. getAutoCommit)
084: * @param mutatorName teh name of the "setter" method (e.g. setAutoCommit)
085: */
086: private void addReset(String accessorName, String mutatorName) {
087:
088: try {
089:
090: Method accessor = null;
091: Method mutator = null;
092:
093: Method[] methods = Connection.class.getMethods();
094: for (int i = 0; i < methods.length; i++) {
095: Method method = methods[i];
096: if (method.getName().equals(accessorName)) {
097: if (accessor == null) {
098: accessor = method;
099: } else {
100: log.info("Skipping ambiguous reset method "
101: + accessorName);
102: return;
103: }
104: }
105: if (method.getName().equals(mutatorName)) {
106: if (mutator == null) {
107: mutator = method;
108: } else {
109: log.info("Skipping ambiguous reset method "
110: + mutatorName);
111: return;
112: }
113: }
114: }
115:
116: if (accessor == null) {
117: log
118: .debug("Ignoring attempt to map reset method "
119: + accessorName
120: + " (probably because it isn't implemented in this JDK)");
121: } else if (mutator == null) {
122: log
123: .debug("Ignoring attempt to map reset method "
124: + mutatorName
125: + " (probably because it isn't implemented in this JDK)");
126: } else if (accessorMutatorMap.containsKey(accessor)) {
127: log
128: .warn("Ignoring attempt to map duplicate reset method "
129: + accessorName);
130: } else if (accessorMutatorMap.containsValue(mutator)) {
131: log
132: .warn("Ignoring attempt to map duplicate reset method "
133: + mutatorName);
134: } else {
135:
136: if (mutatorName.indexOf(MUTATOR_PREFIX) != 0) {
137: log
138: .warn("Resetter mutator "
139: + mutatorName
140: + " does not start with "
141: + MUTATOR_PREFIX
142: + " as expected. Proxool maynot recognise that a reset is necessary.");
143: }
144:
145: if (accessor.getParameterTypes().length > 0) {
146: log.info("Ignoring attempt to map accessor method "
147: + accessorName
148: + ". It must have no arguments.");
149: } else if (mutator.getParameterTypes().length != 1) {
150: log
151: .info("Ignoring attempt to map mutator method "
152: + mutatorName
153: + ". It must have exactly one argument, not "
154: + mutator.getParameterTypes().length);
155: } else {
156: accessorMutatorMap.put(accessor, mutator);
157: }
158: }
159: } catch (Exception e) {
160: log.error("Problem mapping " + accessorName + " and "
161: + mutatorName, e);
162: }
163:
164: }
165:
166: /**
167: * This gets called every time we make a Connection. Not that often
168: * really, so it's ok to synchronize a bit.
169: * @param connection this will be used to get all the default values
170: */
171: protected void initialise(Connection connection) {
172: if (!initialised) {
173: synchronized (this ) {
174: if (!initialised) {
175:
176: Set accessorsToRemove = new HashSet();
177: Iterator i = accessorMutatorMap.keySet().iterator();
178: while (i.hasNext()) {
179: Method accessor = (Method) i.next();
180: Method mutator = (Method) accessorMutatorMap
181: .get(accessor);
182: Object value = null;
183: try {
184: value = accessor.invoke(connection, null);
185: // It's perfectly ok for the default value to be null, we just
186: // don't want to add it to the map.
187: if (value != null) {
188: defaultValues.put(mutator, value);
189: }
190: if (log.isDebugEnabled()) {
191: log.debug("Remembering default value: "
192: + accessor.getName() + "() = "
193: + value);
194: }
195:
196: } catch (Throwable t) {
197: log.debug(driverName + " does not support "
198: + accessor.getName()
199: + ". Proxool doesn't mind.");
200: // We will remove this later (to avoid ConcurrentModifcation)
201: accessorsToRemove.add(accessor);
202: }
203:
204: // Just test that the mutator works too. Otherwise it's going to fall over
205: // everytime we close a connection
206: try {
207: Object[] args = { value };
208: mutator.invoke(connection, args);
209: } catch (Throwable t) {
210: log.debug(driverName + " does not support "
211: + mutator.getName()
212: + ". Proxool doesn't mind.");
213: // We will remove this later (to avoid ConcurrentModifcation)
214: accessorsToRemove.add(accessor);
215: }
216:
217: }
218:
219: // Remove all the reset methods that we had trouble configuring
220: Iterator j = accessorsToRemove.iterator();
221: while (j.hasNext()) {
222: Method accessor = (Method) j.next();
223: Method mutator = (Method) accessorMutatorMap
224: .get(accessor);
225: accessorMutatorMap.remove(accessor);
226: defaultValues.remove(mutator);
227: }
228:
229: initialised = true;
230: }
231: }
232: }
233: }
234:
235: /**
236: * Reset this connection to its default values. If anything goes wrong, it is logged
237: * as a warning or info but it silently continues.
238: * @param connection to be reset
239: * @param id used in log messages
240: * @return true if the reset was error free, or false if it encountered errors. (in which case it should probably not be reused)
241: */
242: protected boolean reset(Connection connection, String id) {
243: boolean errorsEncountered = false;
244:
245: try {
246: connection.clearWarnings();
247: } catch (SQLException e) {
248: errorsEncountered = true;
249: log.warn(id
250: + " - Problem calling connection.clearWarnings()",
251: e);
252: }
253:
254: // Let's see the state of autoCommit. It will help us give better advice in the log messages
255: boolean autoCommit = true;
256: try {
257: autoCommit = connection.getAutoCommit();
258: } catch (SQLException e) {
259: errorsEncountered = true;
260: log.warn(id
261: + " - Problem calling connection.getAutoCommit()",
262: e);
263: }
264:
265: /*
266: Automatically rollback if autocommit is off. If there are no pending
267: transactions then this will have no effect.
268:
269: From Database Language SQL (Proposed revised text of DIS 9075),
270: July 1992 - http://www.contrib.andrew.cmu.edu/~shadow/sql/sql1992.txt
271:
272: "The execution of a <rollback statement> may be initiated implicitly by
273: an implementation when it detects unrecoverable errors. When such an
274: error occurs, an exception condition is raised: transaction rollback
275: with an implementation-defined subclass code".
276:
277: */
278: if (!autoCommit) {
279: try {
280: connection.rollback();
281: } catch (SQLException e) {
282: log
283: .error(
284: "Unexpected exception whilst calling rollback during connection reset",
285: e);
286: }
287: }
288:
289: // Now let's reset each property in turn. With a bit of luck, if there is a
290: // transaction pending then setting one of these properties will throw an
291: // exception (e.g. "operation not possible when transaction is in progress"
292: // or something). We want to know about transactions that are pending.
293: // It doesn't seem like a very good idea to close a connection with
294: // pending transactions.
295: Iterator i = accessorMutatorMap.keySet().iterator();
296: while (i.hasNext()) {
297: Method accessor = (Method) i.next();
298: Method mutator = (Method) accessorMutatorMap.get(accessor);
299: Object[] args = { defaultValues.get(mutator) };
300: try {
301: Object currentValue = accessor.invoke(connection, null);
302: if (currentValue == null && args[0] == null) {
303: // Nothing to do then
304: } else if (currentValue.equals(args[0])) {
305: // Nothing to do here either
306: } else {
307: mutator.invoke(connection, args);
308: if (log.isDebugEnabled()) {
309: log.debug(id + " - Reset: " + mutator.getName()
310: + "(" + args[0] + ") from "
311: + currentValue);
312: }
313: }
314: } catch (Throwable t) {
315: errorsEncountered = true;
316: if (log.isDebugEnabled()) {
317: log.debug(id + " - Problem resetting: "
318: + mutator.getName() + "(" + args[0] + ").",
319: t);
320: }
321: }
322: }
323:
324: // Finally. reset autoCommit.
325: if (!autoCommit) {
326: try {
327: // Setting autoCommit to true might well commit all pending
328: // transactions. But that's beyond our control.
329: connection.setAutoCommit(true);
330: log.debug(id + " - autoCommit reset back to true");
331: } catch (Throwable t) {
332: errorsEncountered = true;
333: log
334: .warn(
335: id
336: + " - Problem calling connection.commit() or connection.setAutoCommit(true)",
337: t);
338: }
339: }
340:
341: if (isTriggerResetException()) {
342: log.warn("Triggering pretend exception during reset");
343: errorsEncountered = true;
344: }
345:
346: if (errorsEncountered) {
347:
348: log
349: .warn(id
350: + " - There were some problems resetting the connection (see debug output for details). It will not be used again "
351: + "(just in case). The thread that is responsible is named '"
352: + Thread.currentThread().getName() + "'");
353: if (!autoCommit) {
354: log
355: .warn(id
356: + " - The connection was closed with autoCommit=false. That is fine, but it might indicate that "
357: + "the problems that happened whilst trying to reset it were because a transaction is still in progress.");
358: }
359: }
360:
361: return !errorsEncountered;
362: }
363:
364: private static boolean isTriggerResetException() {
365: return triggerResetException;
366: }
367:
368: /**
369: * Called by a unit test.
370: * @param triggerResetException true it we should trigger a pretend exception.
371: * @see #isTriggerResetException()
372: */
373: protected static void setTriggerResetException(
374: boolean triggerResetException) {
375: ConnectionResetter.triggerResetException = triggerResetException;
376: }
377: }
378:
379: /*
380: Revision history:
381: $Log: ConnectionResetter.java,v $
382: Revision 1.16 2006/01/18 14:40:01 billhorsman
383: Unbundled Jakarta's Commons Logging.
384:
385: Revision 1.15 2005/10/07 08:21:53 billhorsman
386: New hook to allow unit tests to trigger a deliberate exception during reset
387:
388: Revision 1.14 2003/03/10 23:43:10 billhorsman
389: reapplied checkstyle that i'd inadvertently let
390: IntelliJ change...
391:
392: Revision 1.13 2003/03/10 15:26:46 billhorsman
393: refactoringn of concurrency stuff (and some import
394: optimisation)
395:
396: Revision 1.12 2003/03/03 11:11:57 billhorsman
397: fixed licence
398:
399: Revision 1.11 2003/02/06 17:41:04 billhorsman
400: now uses imported logging
401:
402: Revision 1.10 2003/01/07 17:21:11 billhorsman
403: If autoCommit is off, all connections are rollbacked.
404:
405: Revision 1.9 2002/11/13 20:53:16 billhorsman
406: now checks to see whether is necessary for each property (better logging)
407:
408: Revision 1.8 2002/11/13 18:27:59 billhorsman
409: rethink. committing automatically is bad. so now we just
410: set autoCommit back to true (which might commit anyway but
411: that's down to the driver). we do the autoCommit last so
412: that some of the other resets might throw an exception if
413: there was a pending transaction. (Throwing an exception is
414: good - pending transactions are bad.)
415:
416: Revision 1.7 2002/11/12 21:12:21 billhorsman
417: automatically calls clearWarnings too
418:
419: Revision 1.6 2002/11/12 21:10:41 billhorsman
420: Hmm. Now commits any pending transactions automatically when
421: you close the connection. I'm still pondering whether this is
422: wise or not. The only other sensible option is to rollback
423: since I can't find a way of determining whether either is
424: necessary.
425:
426: Revision 1.5 2002/11/12 20:24:12 billhorsman
427: checkstyle
428:
429: Revision 1.4 2002/11/12 20:18:23 billhorsman
430: Made connection resetter a bit more friendly. Now, if it encounters any problems during
431: reset then that connection is thrown away. This is going to cause you problems if you
432: always close connections in an unstable state (e.g. with transactions open. But then
433: again, it's better to know about that as soon as possible, right?
434:
435: Revision 1.3 2002/11/07 18:55:40 billhorsman
436: demoted log message from info to debug
437:
438: Revision 1.2 2002/11/07 12:38:04 billhorsman
439: performance improvement - only reset when it might be necessary
440:
441: Revision 1.1 2002/11/06 20:25:08 billhorsman
442: New class responsible for resetting connections when
443: they are returned to the pool.
444:
445: */
|