| 1 | // Copyright 2004-2007 Jean-Francois Poilpret |
| 2 | // |
| 3 | // Licensed under the Apache License, Version 2.0 (the "License"); |
| 4 | // you may not use this file except in compliance with the License. |
| 5 | // You may obtain a copy of the License at |
| 6 | // |
| 7 | // http://www.apache.org/licenses/LICENSE-2.0 |
| 8 | // |
| 9 | // Unless required by applicable law or agreed to in writing, software |
| 10 | // distributed under the License is distributed on an "AS IS" BASIS, |
| 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 12 | // See the License for the specific language governing permissions and |
| 13 | // limitations under the License. |
| 14 | |
| 15 | package net.sourceforge.hivetranse.transaction; |
| 16 | |
| 17 | import java.util.Iterator; |
| 18 | |
| 19 | import org.apache.commons.logging.Log; |
| 20 | import org.apache.hivemind.Discardable; |
| 21 | import org.apache.hivemind.PoolManageable; |
| 22 | import org.apache.hivemind.util.EventListenerList; |
| 23 | |
| 24 | import net.sourceforge.hiveutils.collections.Stack; |
| 25 | import net.sourceforge.hiveutils.collections.impl.StackImpl; |
| 26 | |
| 27 | /** |
| 28 | * Base class for every actual implementation of <code>TransactionService</code>. |
| 29 | * This class manages the stack of transaction contexts and the notification |
| 30 | * of <code>TransactionEventListener</code>s. |
| 31 | * <p> |
| 32 | * Subclasses just have to implement two abstract methods to begin and close an |
| 33 | * actual transaction, and possibly a third method to store the changing context |
| 34 | * somewhere. For those subclasses, the notion of transaction can be any |
| 35 | * <code>Object</code>. |
| 36 | * <p> |
| 37 | * <b>ServiceModel of subclasses must be threaded or pooled</b> |
| 38 | * |
| 39 | * @author Jean-Francois Poilpret |
| 40 | */ |
| 41 | public abstract class AbstractTransactionService |
| 42 | implements TransactionService, Discardable, PoolManageable |
| 43 | { |
| 44 | protected AbstractTransactionService(Log logger, boolean wrapRuntimeExceptions) |
| 45 | { |
| 46 | _logger = logger; |
| 47 | _wrapRuntimeExceptions = wrapRuntimeExceptions; |
| 48 | } |
| 49 | |
| 50 | // Implementation of Discardable interface |
| 51 | //----------------------------------------- |
| 52 | public void threadDidDiscardService() |
| 53 | { |
| 54 | cleanUp(); |
| 55 | } |
| 56 | |
| 57 | // Implementation of PoolManageable interface |
| 58 | //-------------------------------------------- |
| 59 | public void activateService() |
| 60 | { |
| 61 | // Make sure that current contexts are erased |
| 62 | _stack.clear(); |
| 63 | _current = null; |
| 64 | //#### What about listeners? Should be taken care of by HiveMind |
| 65 | // This must be checked! |
| 66 | } |
| 67 | |
| 68 | public void passivateService() |
| 69 | { |
| 70 | cleanUp(); |
| 71 | } |
| 72 | |
| 73 | // Implementation of TransactionService interface |
| 74 | //------------------------------------------------ |
| 75 | synchronized public void addTransactionEventListener(TransactionEventListener listener) |
| 76 | { |
| 77 | _listeners.addListener(listener); |
| 78 | } |
| 79 | |
| 80 | synchronized public void removeTransactionEventListener(TransactionEventListener listener) |
| 81 | { |
| 82 | _listeners.removeListener(listener); |
| 83 | } |
| 84 | |
| 85 | public Object getCurrentTransaction() |
| 86 | { |
| 87 | return (_current != null ? _current.getTransaction() : null); |
| 88 | } |
| 89 | |
| 90 | //CSOFF: RedundantThrowsCheck |
| 91 | public void begin(TransactionDemarcation demarcation) |
| 92 | throws TransactionException, |
| 93 | MandatoryTransactionException, |
| 94 | ForbiddenTransactionException |
| 95 | { |
| 96 | boolean mustCleanUp = false; |
| 97 | TransactionObject currentTx = null; |
| 98 | if (_current != null) |
| 99 | { |
| 100 | currentTx = _current.getTransactionObject(); |
| 101 | } |
| 102 | |
| 103 | // Find out the next Connection to be used |
| 104 | // (either use the same, use new, or use none) |
| 105 | switch (demarcation) |
| 106 | { |
| 107 | case RequiresNew: |
| 108 | // Use new Connection |
| 109 | currentTx = new TransactionObject(beginTransaction()); |
| 110 | mustCleanUp = true; |
| 111 | break; |
| 112 | |
| 113 | case Required: |
| 114 | // Use current Connection, or create if no Connection yet |
| 115 | if (currentTx == null) |
| 116 | { |
| 117 | currentTx = new TransactionObject(beginTransaction()); |
| 118 | mustCleanUp = true; |
| 119 | } |
| 120 | break; |
| 121 | |
| 122 | case Supports: |
| 123 | // Use current Connection |
| 124 | break; |
| 125 | |
| 126 | case Mandatory: |
| 127 | if (currentTx == null) |
| 128 | { |
| 129 | _logger.error("No transaction available for Mandatory demarcation"); |
| 130 | throw new MandatoryTransactionException( |
| 131 | "No transaction available for Mandatory demarcation"); |
| 132 | } |
| 133 | break; |
| 134 | |
| 135 | case Never: |
| 136 | if (currentTx != null) |
| 137 | { |
| 138 | _logger.error("Existing transaction for Never demarcation"); |
| 139 | throw new ForbiddenTransactionException( |
| 140 | "Existing transaction for Never demarcation"); |
| 141 | } |
| 142 | break; |
| 143 | |
| 144 | case NotSupported: |
| 145 | currentTx = null; |
| 146 | break; |
| 147 | |
| 148 | default: |
| 149 | _logger.error("Unknown transaction demarcation, code=" + demarcation); |
| 150 | throw new TransactionException("Unknown transaction demarcation"); |
| 151 | } |
| 152 | |
| 153 | _current = new Context(mustCleanUp, currentTx); |
| 154 | _stack.push(_current); |
| 155 | storeTransaction(); |
| 156 | // If a new transaction has been created then fire the matching event |
| 157 | if (mustCleanUp) |
| 158 | { |
| 159 | fireAfterNewTransactionBegins(); |
| 160 | } |
| 161 | } |
| 162 | //CSON: RedundantThrowsCheck |
| 163 | |
| 164 | public void end() throws TransactionException |
| 165 | { |
| 166 | // Pop current context from stack |
| 167 | _stack.pop(); |
| 168 | // Check if there is something to do |
| 169 | Exception e = cleanUp(_current); |
| 170 | boolean commit = !_current.isRollbackOnly(); |
| 171 | // Set the new current transaction context |
| 172 | _current = (_stack.isEmpty() ? null : _stack.peek()); |
| 173 | storeTransaction(); |
| 174 | if (e != null) |
| 175 | { |
| 176 | if ( (e instanceof RuntimeException) |
| 177 | && !_wrapRuntimeExceptions) |
| 178 | { |
| 179 | throw (RuntimeException) e; |
| 180 | } |
| 181 | if (commit) |
| 182 | { |
| 183 | throw new TransactionException("Error occurred during transaction commit", e); |
| 184 | } |
| 185 | else |
| 186 | { |
| 187 | throw new TransactionException("Error occurred during transaction rollback", e); |
| 188 | } |
| 189 | } |
| 190 | } |
| 191 | |
| 192 | public void setRollbackOnly() throws TransactionException |
| 193 | { |
| 194 | if (_current == null) |
| 195 | { |
| 196 | _logger.error("No transaction available"); |
| 197 | throw new MandatoryTransactionException("No transaction available"); |
| 198 | } |
| 199 | _current.setRollbackOnly(); |
| 200 | } |
| 201 | |
| 202 | protected void fireBeforeNewTransactionBegins() |
| 203 | { |
| 204 | Iterator i = _listeners.getListeners(); |
| 205 | while (i.hasNext()) |
| 206 | { |
| 207 | TransactionEventListener l = (TransactionEventListener) i.next(); |
| 208 | l.beforeNewTransactionBegins(); |
| 209 | } |
| 210 | } |
| 211 | |
| 212 | protected void fireAfterNewTransactionBegins() |
| 213 | { |
| 214 | Iterator i = _listeners.getListeners(); |
| 215 | while (i.hasNext()) |
| 216 | { |
| 217 | TransactionEventListener l = (TransactionEventListener) i.next(); |
| 218 | l.afterNewTransactionBegins(); |
| 219 | } |
| 220 | } |
| 221 | |
| 222 | protected void fireBeforeTransactionEnds(boolean committed) |
| 223 | { |
| 224 | Iterator i = _listeners.getListeners(); |
| 225 | while (i.hasNext()) |
| 226 | { |
| 227 | TransactionEventListener l = (TransactionEventListener) i.next(); |
| 228 | l.beforeTransactionEnds(committed); |
| 229 | } |
| 230 | } |
| 231 | |
| 232 | protected void fireAfterTransactionEnds(boolean committed) |
| 233 | { |
| 234 | Iterator i = _listeners.getListeners(); |
| 235 | while (i.hasNext()) |
| 236 | { |
| 237 | TransactionEventListener l = (TransactionEventListener) i.next(); |
| 238 | l.afterTransactionEnds(committed); |
| 239 | } |
| 240 | } |
| 241 | |
| 242 | protected void cleanUp() |
| 243 | { |
| 244 | // Close all pending connections |
| 245 | boolean error = false; |
| 246 | while (!_stack.isEmpty()) |
| 247 | { |
| 248 | Context context = _stack.pop(); |
| 249 | context.setRollbackOnly(); |
| 250 | if (cleanUp(context) != null) |
| 251 | { |
| 252 | error = true; |
| 253 | } |
| 254 | } |
| 255 | if (error) |
| 256 | { |
| 257 | _logger.error("threadDidDiscardService could not clean up fully"); |
| 258 | } |
| 259 | } |
| 260 | |
| 261 | //CSOFF: IllegalCatchCheck |
| 262 | protected Object beginTransaction() |
| 263 | { |
| 264 | _logger.debug("beginTransaction(): starting a new transaction"); |
| 265 | fireBeforeNewTransactionBegins(); |
| 266 | try |
| 267 | { |
| 268 | Object tx = createTransaction(); |
| 269 | return tx; |
| 270 | } |
| 271 | catch (Exception e) |
| 272 | { |
| 273 | _logger.error("beginTransaction()", e); |
| 274 | if ( (e instanceof RuntimeException) |
| 275 | && !_wrapRuntimeExceptions) |
| 276 | { |
| 277 | throw (RuntimeException) e; |
| 278 | } |
| 279 | else |
| 280 | { |
| 281 | throw new TransactionException(e); |
| 282 | } |
| 283 | } |
| 284 | } |
| 285 | //CSON: IllegalCatchCheck |
| 286 | |
| 287 | /** |
| 288 | * Method called when creation of a new "transaction" is needed. |
| 289 | * It must be implemented by subclasses. |
| 290 | * <p> |
| 291 | * Please note that "transaction" may be anything and does not have to be |
| 292 | * just a transaction, ie it may be a JDBC Connection, a Hibernate Session... |
| 293 | * @return the new transaction created. Internal storage of this object is |
| 294 | * performed by <code>AbstractTransactionService</code>. |
| 295 | * @exception Exception any exception, that will eventually be wrapped into |
| 296 | * an -unchecked- TransactionException |
| 297 | */ |
| 298 | abstract protected Object createTransaction() throws Exception; |
| 299 | |
| 300 | /** |
| 301 | * Method called when the current "transaction" must end, either committed |
| 302 | * or rolled back. |
| 303 | * Ending a transaction may have more meaning than just ending an actual |
| 304 | * transaction, it may as well mean closing a JDBC Connection, an Hibernate |
| 305 | * Session... |
| 306 | * @param tx the "transaction" object (ie, the same object returned by |
| 307 | * <code>createTransaction</code>) |
| 308 | * @param commit <code>true</code> if the transaction represented by |
| 309 | * <code>tx</code> must be committed, <code>false</code> if it must be |
| 310 | * rolled back |
| 311 | * @exception Exception any exception, that will eventually be wrapped into |
| 312 | * an -unchecked- TransactionException |
| 313 | */ |
| 314 | abstract protected void endTransaction(Object tx, boolean commit) |
| 315 | throws Exception; |
| 316 | |
| 317 | /** |
| 318 | * Method called whenever the current transaction context changes. |
| 319 | * Does nothing by default. |
| 320 | * <p> |
| 321 | * Override this method if you have specific needs when a transaction |
| 322 | * context changes occur. For instance, subclasses could store the new |
| 323 | * current transaction context into ThreadLocalStorage for use by other |
| 324 | * services. |
| 325 | * <p> |
| 326 | * Please carefully note that overriding this method is not equivalent to |
| 327 | * using TransactionEvents (these are triggered when a Transaction is |
| 328 | * created or ended, but they do not indicate what the next transaction |
| 329 | * context is). |
| 330 | * @param tx new current "transaction" object, as created by |
| 331 | * <code>createTransaction</code>; might be <code>null</code> to indicate |
| 332 | * that there is no available transaction context from now on. |
| 333 | */ |
| 334 | protected void storeTransaction(Object tx) |
| 335 | { |
| 336 | } |
| 337 | |
| 338 | protected void storeTransaction() |
| 339 | { |
| 340 | Object tx = (_current != null ? _current.getTransaction() : null); |
| 341 | storeTransaction(tx); |
| 342 | } |
| 343 | |
| 344 | //CSOFF: IllegalCatchCheck |
| 345 | protected Exception cleanUp(Context context) |
| 346 | { |
| 347 | if (!context.mustCleanUp()) |
| 348 | { |
| 349 | return null; |
| 350 | } |
| 351 | boolean commit = !context.isRollbackOnly(); |
| 352 | _logger.debug("cleanUp(): ending transaction, commit=" + commit); |
| 353 | try |
| 354 | { |
| 355 | fireBeforeTransactionEnds(commit); |
| 356 | endTransaction(context.getTransaction(), commit); |
| 357 | fireAfterTransactionEnds(commit); |
| 358 | return null; |
| 359 | } |
| 360 | catch (Exception e) |
| 361 | { |
| 362 | _logger.error("Problem committing transaction", e); |
| 363 | return e; |
| 364 | } |
| 365 | } |
| 366 | //CSON: IllegalCatchCheck |
| 367 | |
| 368 | // For each pass through a TransactionInterceptor, an Context instance is |
| 369 | // pushed on the stack. |
| 370 | // Each Context contains the JDBC Connection to be used, and also contains a |
| 371 | // flag defining whether the Connection is owned by this Context or not. |
| 372 | // If demarcation disables transactions (ie, Never or NotSupported), then |
| 373 | // the Connection is null. |
| 374 | static protected class Context |
| 375 | { |
| 376 | public Context(boolean mustCleanUp, TransactionObject tx) |
| 377 | { |
| 378 | _mustCleanUp = mustCleanUp; |
| 379 | _tx = tx; |
| 380 | } |
| 381 | |
| 382 | public TransactionObject getTransactionObject() |
| 383 | { |
| 384 | return _tx; |
| 385 | } |
| 386 | |
| 387 | public Object getTransaction() |
| 388 | { |
| 389 | if (_tx != null) |
| 390 | { |
| 391 | return _tx.getTransaction(); |
| 392 | } |
| 393 | else |
| 394 | { |
| 395 | return null; |
| 396 | } |
| 397 | } |
| 398 | |
| 399 | public boolean isRollbackOnly() |
| 400 | { |
| 401 | if (_tx != null) |
| 402 | { |
| 403 | return _tx.isRollbackOnly(); |
| 404 | } |
| 405 | else |
| 406 | { |
| 407 | return true; |
| 408 | } |
| 409 | } |
| 410 | |
| 411 | public void setRollbackOnly() |
| 412 | { |
| 413 | if (_tx != null) |
| 414 | { |
| 415 | _tx.setRollbackOnly(); |
| 416 | } |
| 417 | } |
| 418 | |
| 419 | public boolean mustCleanUp() |
| 420 | { |
| 421 | return _mustCleanUp; |
| 422 | } |
| 423 | |
| 424 | private final boolean _mustCleanUp; |
| 425 | private final TransactionObject _tx; |
| 426 | } |
| 427 | |
| 428 | // The TransactionObject embed both the Transaction (Object which meaning |
| 429 | // depends on actual implementation) AND the rollback status of this |
| 430 | // transaction object |
| 431 | static protected class TransactionObject |
| 432 | { |
| 433 | public TransactionObject(Object tx) |
| 434 | { |
| 435 | _tx = tx; |
| 436 | _rollbackOnly = false; |
| 437 | } |
| 438 | |
| 439 | public Object getTransaction() |
| 440 | { |
| 441 | return _tx; |
| 442 | } |
| 443 | |
| 444 | public boolean isRollbackOnly() |
| 445 | { |
| 446 | return _rollbackOnly; |
| 447 | } |
| 448 | |
| 449 | public void setRollbackOnly() |
| 450 | { |
| 451 | _rollbackOnly = true; |
| 452 | } |
| 453 | |
| 454 | private final Object _tx; |
| 455 | private boolean _rollbackOnly; |
| 456 | } |
| 457 | |
| 458 | protected final Log _logger; |
| 459 | protected final boolean _wrapRuntimeExceptions; |
| 460 | protected final Stack<Context> _stack = new StackImpl<Context>(); |
| 461 | protected final EventListenerList _listeners = new EventListenerList(); |
| 462 | protected Context _current = null; |
| 463 | } |