de.decidr.model.transactions.HibernateTransactionCoordinator.java Source code

Java tutorial

Introduction

Here is the source code for de.decidr.model.transactions.HibernateTransactionCoordinator.java

Source

/*
 * 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(&quot;James Bond&quot;);
 *     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);
            }
        }
    }
}