001    /**
002     *
003     * Licensed to the Apache Software Foundation (ASF) under one or more
004     * contributor license agreements.  See the NOTICE file distributed with
005     * this work for additional information regarding copyright ownership.
006     * The ASF licenses this file to You under the Apache License, Version 2.0
007     * (the "License"); you may not use this file except in compliance with
008     * the License.  You may obtain a copy of the License at
009     *
010     *     http://www.apache.org/licenses/LICENSE-2.0
011     *
012     *  Unless required by applicable law or agreed to in writing, software
013     *  distributed under the License is distributed on an "AS IS" BASIS,
014     *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
015     *  See the License for the specific language governing permissions and
016     *  limitations under the License.
017     */
018    package org.apache.commons.dbcp.managed;
019    
020    import javax.transaction.RollbackException;
021    import javax.transaction.Status;
022    import javax.transaction.Synchronization;
023    import javax.transaction.SystemException;
024    import javax.transaction.Transaction;
025    import javax.transaction.xa.XAResource;
026    import java.sql.Connection;
027    import java.sql.SQLException;
028    import java.lang.ref.WeakReference;
029    
030    /**
031     * TransactionContext represents the association between a single XAConnectionFactory and a Transaction.
032     * This context contains a single shared connection which should be used by all ManagedConnections for
033     * the XAConnectionFactory, the ability to listen for the transaction completion event, and a method
034     * to check the status of the transaction.
035     *
036     * @author Dain Sundstrom
037     * @version $Revision: 892307 $
038     */
039    public class TransactionContext {
040        private final TransactionRegistry transactionRegistry;
041        private final WeakReference transactionRef;
042        private Connection sharedConnection;
043    
044        /**
045         * Creates a TransactionContext for the specified Transaction and TransactionRegistry.  The
046         * TransactionRegistry is used to obtain the XAResource for the shared connection when it is
047         * enlisted in the transaction.
048         *
049         * @param transactionRegistry the TransactionRegistry used to obtain the XAResource for the
050         * shared connection
051         * @param transaction the transaction
052         */
053        public TransactionContext(TransactionRegistry transactionRegistry, Transaction transaction) {
054            if (transactionRegistry == null) throw new NullPointerException("transactionRegistry is null");
055            if (transaction == null) throw new NullPointerException("transaction is null");
056            this.transactionRegistry = transactionRegistry;
057            this.transactionRef = new WeakReference(transaction);
058        }
059    
060        /**
061         * Gets the connection shared by all ManagedConnections in the transaction.  Specifically,
062         * connection using the same XAConnectionFactory from which the TransactionRegistry was
063         * obtained.
064         * @return the shared connection for this transaction
065         */
066        public Connection getSharedConnection() {
067            return sharedConnection;
068        }
069    
070        /**
071         * Sets the shared connection for this transaction.  The shared connection is enlisted
072         * in the transaction.
073         *
074         * @param sharedConnection the shared connection
075         * @throws SQLException if a shared connection is already set, if XAResource for the connection
076         * could not be found in the transaction registry, or if there was a problem enlisting the
077         * connection in the transaction
078         */
079        public void setSharedConnection(Connection sharedConnection) throws SQLException {
080            if (this.sharedConnection != null) {
081                throw new IllegalStateException("A shared connection is alredy set");
082            }
083    
084            // This is the first use of the connection in this transaction, so we must
085            // enlist it in the transaction
086            Transaction transaction = getTransaction();
087            try {
088                XAResource xaResource = transactionRegistry.getXAResource(sharedConnection);
089                transaction.enlistResource(xaResource);
090            } catch (RollbackException e) {
091                // transaction was rolled back... proceed as if there never was a transaction
092            } catch (SystemException e) {
093                throw (SQLException) new SQLException("Unable to enlist connection the transaction").initCause(e);
094            }
095    
096            this.sharedConnection = sharedConnection;
097        }
098    
099        /**
100         * Adds a listener for transaction completion events.
101         *
102         * @param listener the listener to add
103         * @throws SQLException if a problem occurs adding the listener to the transaction
104         */
105        public void addTransactionContextListener(final TransactionContextListener listener) throws SQLException {
106            try {
107                getTransaction().registerSynchronization(new Synchronization() {
108                    public void beforeCompletion() {
109                    }
110    
111                    public void afterCompletion(int status) {
112                        listener.afterCompletion(TransactionContext.this, status == Status.STATUS_COMMITTED);
113                    }
114                });
115            } catch (RollbackException e) {
116                // JTA spec doesn't let us register with a transaction marked rollback only
117                // just ignore this and the tx state will be cleared another way.
118            } catch (Exception e) {
119                throw (SQLException) new SQLException("Unable to register transaction context listener").initCause(e);
120            }
121        }
122    
123        /**
124         * True if the transaction is active or marked for rollback only.
125         * @return true if the transaction is active or marked for rollback only; false otherwise
126         * @throws SQLException if a problem occurs obtaining the transaction status
127         */
128        public boolean isActive() throws SQLException {
129            try {
130                Transaction transaction = (Transaction) this.transactionRef.get();
131                if (transaction == null) {
132                    return false;
133                }
134                int status = transaction.getStatus();
135                return status == Status.STATUS_ACTIVE || status == Status.STATUS_MARKED_ROLLBACK;
136            } catch (SystemException e) {
137                throw (SQLException) new SQLException("Unable to get transaction status").initCause(e);
138            }
139        }
140    
141        private Transaction getTransaction() throws SQLException {
142            Transaction transaction = (Transaction) this.transactionRef.get();
143            if (transaction == null) {
144                throw new SQLException("Unable to enlist connection because the transaction has been garbage collected");
145            }
146            return transaction;
147        }
148    }