Java tutorial
/* * The DecidR Development Team licenses this file to you under the Apache * License, Version 2.0 (the "License"); you may not use this file except in * compliance with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations under * the License. */ package de.decidr.model.transactions; import java.util.ArrayList; import org.apache.log4j.Level; import org.apache.log4j.Logger; import org.hibernate.Session; import org.hibernate.SessionFactory; import org.hibernate.Transaction; import de.decidr.model.commands.TransactionalCommand; import de.decidr.model.exceptions.TransactionException; import de.decidr.model.logging.DefaultLogger; /** * Invokes {@link TransactionalCommand}s within a Hibernate transaction. Inner * transactions are supported by giving up the durability property of all inner * transactions. * <p> * Usage example: * * <pre> * public static void main() { * TransactionalCommand killCommand = new KillCommand(); * killCommand.setVictim("James Bond"); * HibernateTransactionCoordinator htc = HibernateTransactionCoordinator.create( * Configuration.configure().buildSessionFactory())); * if (htc.run(killCommand).isSuccessfullyCommitted() && !killCommand.victimHasEscaped()) { * // world domination! * } * } * </pre> * * @author Daniel Huss * @author Markus Fischer * @version 0.2 */ public class HibernateTransactionCoordinator implements TransactionCoordinator { /** * Creates a new instance of {@link HibernateTransactionCoordinator} that * uses the given session factory to open new sessions * * @param sessionFactory * session factory to use * @return a new instance * @throws IllegalArgumentException * if sessionFactory is closed or <code>null</code>. */ public static HibernateTransactionCoordinator create(SessionFactory sessionFactory) { return new HibernateTransactionCoordinator(sessionFactory); } /** * Logger */ private Logger logger = DefaultLogger.getLogger(HibernateTransactionCoordinator.class); /** * Hibernate session factory used to create sessions. */ private SessionFactory sessionFactory; /** * The current Hibernate transaction. Inner transactions are executed within * the context of a single Hibernate transaction. */ private Transaction currentTransaction = null; /** * A list of commands that have received the transactionStarted event. These * commands need to be notified of transactionCommitted iff * <code>transactionDepth == 0</code> */ private ArrayList<TransactionalCommand> notifiedReceivers = null; /** * The current Hibernate session. */ private Session session = null; /** * The current transaction depth. */ private int transactionDepth = 0; /** * Creates a new HibernateTransactionCoordinator * * @param sessionFactory * a source for new sessions created by your hibernate * configuration. * @throws IllegalArgumentException * if the session factory is closed or <code>null</code>. */ private HibernateTransactionCoordinator(SessionFactory sessionFactory) { logger.log(Level.DEBUG, "Creating HibernateTransactionCoordinator"); if (sessionFactory == null) { throw new IllegalArgumentException("Session factory must not be null."); } if (sessionFactory.isClosed()) { throw new IllegalArgumentException("Session factory is closed."); } this.sessionFactory = sessionFactory; this.notifiedReceivers = new ArrayList<TransactionalCommand>(); this.currentTransaction = null; this.transactionDepth = 0; this.session = null; } /** * Starts a new transaction. If the new transaction is an inner transaction, * the session of the existing outer transaction is reused. */ protected void beginTransaction() { logger.log(Level.DEBUG, "Beginning transaction. New transaction depth: " + (transactionDepth + 1)); /* * Consistency check. The session / transaction can only exist if and * only if trasactionDepth is greater than zero. */ if (!isTransactionRunning()) { notifiedReceivers.clear(); logger.log(Level.DEBUG, "HibernateTransactionCoordinator: starting outmost transaction"); session = sessionFactory.openSession(); currentTransaction = session.beginTransaction(); } if (session == null || currentTransaction == null) { throw new AssertionError("Transaction unexpectedly not ready."); } transactionDepth++; } /** * Commits the current transaction. The current session is closed if the * outmost transaction is committed. * * @return result of the commit */ @SuppressWarnings("unchecked") protected CommitResult commitCurrentTransaction() { String logMessage = transactionDepth == 1 ? "Committing outmost transaction." : "Delaying commit until the outmost transaction commits."; logger.log(Level.DEBUG, logMessage + " Current transaction depth: " + transactionDepth); /* * We're going to catch exceptions but not errors. This could * potentially be a problem if the JDBC connection doesn't get closed * (not that we can do anything about it). */ Exception resultException = null; if (isTransactionRunning() && transactionDepth == 1) { currentTransaction.commit(); /* * At this point, we assume that the commit has succeeded. We cannot * throw an exception after this point because that would cause a * rollback (which is prohibited by the durability property of * transactions). */ try { session.close(); } catch (Throwable t) { /* * We can't close the session, this is kind of bad. Can we at * least disconnect it? */ try { logger.log(Level.ERROR, "Could not close Hibernate session in commitCurrentTransaction", t); session.disconnect(); } catch (Throwable t2) { /* * Just ignore */ } } /* * Making a shallow copy of the list due to potential list write * access in other methods. */ for (TransactionalCommand c : (ArrayList<TransactionalCommand>) notifiedReceivers.clone()) { try { /* * Note: the chain is broken if one of the commands throws * an exception in its transactionCommitted() method. */ fireTransactionCommitted(c); } catch (Exception e) { resultException = e; break; } } /* * Cleanup */ currentTransaction = null; transactionDepth = 0; session = null; } else if (transactionDepth > 0) { transactionDepth--; } return new CommitResult(transactionDepth == 0, resultException); } @Override protected void finalize() throws Throwable { /* * Perform final cleanup and consistency checks. */ if (session != null || currentTransaction != null || transactionDepth != 0) { logger.log(Level.FATAL, "HibernateTransactionCoordinator got garbage collected while a transaction was running!"); } if (session != null) { try { session.disconnect(); } catch (Throwable t) { /* * Just ignore */ } } session = null; currentTransaction = null; } /** * Fires transaction aborted event. * * @param receiver * the receiver of the "transaction aborted" event * @param t * the exception / error that caused the rollback. */ private void fireTransactionAborted(TransactionalCommand receiver, Throwable t) throws TransactionException { TransactionAbortedEvent event = new TransactionAbortedEvent(t, false, this); receiver.transactionAborted(event); } /** * Fires transaction committed event. * * @param receiver * the receiver of the "transaction committed" event * @throws TransactionException * the only checked exception that is allowed to occur within * the "transaction committed" event */ private void fireTransactionCommitted(TransactionalCommand receiver) throws TransactionException { TransactionEvent event = new TransactionEvent(transactionDepth > 1, this); receiver.transactionCommitted(event); } /** * Fires transaction started event. * * @param receiver * the receiver of the "transaction started" event. */ private void fireTransactionStarted(TransactionalCommand receiver) throws TransactionException { TransactionStartedEvent event = new TransactionStartedEvent(transactionDepth > 1, this, session); receiver.transactionStarted(event); } /** * @return the current Hibernate session or <code>null</code> if no session * has been opened yet. The returned session may have been closed * using the close() method, and therefore is not necessarily open.<br> */ public Session getCurrentSession() { return session; } /** * Checks whether a transaction is currently running. Additionally, this * method performs a consistency checks that raises an * {@link AssertionError} if the {@link HibernateTransactionCoordinator} is * in an inconsistent state. * * @return whether a transaction is currently runnning */ protected boolean isTransactionRunning() { /* * Consistency check. */ if (transactionDepth > 0 && (session == null || currentTransaction == null)) { AssertionError error = new AssertionError( "A previous transaction or session hasn't been properly closed."); rollbackCurrentTransaction(error); throw error; } if ((transactionDepth < 0) || (transactionDepth == 0 && (session != null || currentTransaction != null))) { AssertionError error = new AssertionError("Invalid transaction depth"); rollbackCurrentTransaction(error); throw error; } return (transactionDepth > 0); } /** * Performs a rollback for the current transaction. All enclosing outer * transactions are rolled back as well. * * @param t * Exception / error that caused the rollback */ @SuppressWarnings("unchecked") protected void rollbackCurrentTransaction(Throwable t) { /*- * We have to guarantee the following things: * * 1. When this method returns, transactionDepth is 0, * curentTransaction and session are null. * 2. The transaction is terminated. */ try { /* * Try to notify the commands of the rollback. */ logger.log(Level.DEBUG, "Aborting transaction. Current transaction depth: " + transactionDepth); /* * Making a shallow copy of the list due to potential list write * access in other methods. */ for (TransactionalCommand c : (ArrayList<TransactionalCommand>) notifiedReceivers.clone()) { /* * Exceptions thrown in transactionAborted must be ignored to * give all commands a chance to react to the rollback. */ try { fireTransactionAborted(c, t); } catch (Throwable receiverRollbackException) { logger.log(Level.WARN, "Exception during transactionAborted", receiverRollbackException); } } notifiedReceivers.clear(); } finally { /* * We must do everything we can to make sure that the JDBC * connection is closed and the transaction ends. If we somehow fail * to close the connection, all data that has been accessed in this * connection will stay write-locked forever! */ try { if (currentTransaction != null) { currentTransaction.rollback(); } } catch (Throwable ignored) { /* * Just ignore. */ } try { if (session != null) { session.close(); } } catch (Throwable ignored) { try { /* * We can't close the session, this is kind of bad. Can we * at least disconnect it? */ session.disconnect(); } catch (Throwable ignored2) { /* * Just ignore. */ } } currentTransaction = null; session = null; transactionDepth = 0; } } /** * {@inheritDoc} */ public CommitResult runTransaction(TransactionalCommand... commands) throws TransactionException { // check parameters if (commands == null) { throw new TransactionException(new IllegalArgumentException("Command(s) must not be null.")); } if (commands.length == 0) { throw new TransactionException( new IllegalArgumentException("Must supply at least one command to execute.")); } for (TransactionalCommand c : commands) { if (c == null) { throw new TransactionException(new IllegalArgumentException("Command(s) must not be null.")); } } /* * Why all these try...catch blocks? * * Once beginTransaction is called, we MUST either commit or abort the * new transaction. Otherwise the transaction becomes a "corpse" that * prevents other transactions from accessing any modified data due to * the isolation property of transactions. */ try { beginTransaction(); for (TransactionalCommand c : commands) { notifiedReceivers.add(c); String className = c.getClass().getSimpleName(); if (className.isEmpty()) { className = "<anonymous inner class>"; } logger.log(Level.DEBUG, "Attempting to execute " + className); fireTransactionStarted(c); } CommitResult commitResult = commitCurrentTransaction(); if (commitResult.isCommitted() && (commitResult.getCommittedEventException() == null)) { logger.log(Level.DEBUG, "Everything OK, no exceptions in transactionCommitted."); } else if (commitResult.isCommitted()) { /* * There was an exception in a "transaction committed" event. */ logger.log(Level.DEBUG, "Exception thrown in transactionCommitted.", commitResult.getCommittedEventException()); } return commitResult; } catch (Throwable t) { try { Level level; String type; if (t instanceof Exception) { level = Level.INFO; type = "Exception"; } else { level = Level.FATAL; type = "Error"; } logger.log(level, type + " in transactionStarted.", t); } finally { rollbackCurrentTransaction(t); } if (t instanceof TransactionException) { /* * Don't have to wrap a TransactionException in another * TransactionException. */ throw (TransactionException) t; } else if (t instanceof Exception) { /* * Wrap checked exception in TransactionException. */ throw new TransactionException(t); } else if (t instanceof Error) { /* * Don't have to wrap errors. */ throw (Error) t; } else { /* * In the unlikely event that t is neither an exception nor an * error, the most reasonable thing we can do is wrapping it in * an error. */ throw new Error("Throwable was neither Exception nor Error", t); } } } }