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 */
018package org.apache.commons.dbcp2.managed;
019
020import javax.transaction.RollbackException;
021import javax.transaction.Status;
022import javax.transaction.Synchronization;
023import javax.transaction.SystemException;
024import javax.transaction.Transaction;
025import javax.transaction.xa.XAResource;
026import java.sql.Connection;
027import java.sql.SQLException;
028import 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 $Id: TransactionContext.java 1649430 2015-01-04 21:29:32Z tn $
038 * @since 2.0
039 */
040public class TransactionContext {
041    private final TransactionRegistry transactionRegistry;
042    private final WeakReference<Transaction> transactionRef;
043    private Connection sharedConnection;
044
045    /**
046     * Creates a TransactionContext for the specified Transaction and TransactionRegistry.  The
047     * TransactionRegistry is used to obtain the XAResource for the shared connection when it is
048     * enlisted in the transaction.
049     *
050     * @param transactionRegistry the TransactionRegistry used to obtain the XAResource for the
051     * shared connection
052     * @param transaction the transaction
053     */
054    public TransactionContext(TransactionRegistry transactionRegistry, Transaction transaction) {
055        if (transactionRegistry == null) {
056            throw new NullPointerException("transactionRegistry is null");
057        }
058        if (transaction == null) {
059            throw new NullPointerException("transaction is null");
060        }
061        this.transactionRegistry = transactionRegistry;
062        this.transactionRef = new WeakReference<>(transaction);
063    }
064
065    /**
066     * Gets the connection shared by all ManagedConnections in the transaction.  Specifically,
067     * connection using the same XAConnectionFactory from which the TransactionRegistry was
068     * obtained.
069     * @return the shared connection for this transaction
070     */
071    public Connection getSharedConnection() {
072        return sharedConnection;
073    }
074
075    /**
076     * Sets the shared connection for this transaction.  The shared connection is enlisted
077     * in the transaction.
078     *
079     * @param sharedConnection the shared connection
080     * @throws SQLException if a shared connection is already set, if XAResource for the connection
081     * could not be found in the transaction registry, or if there was a problem enlisting the
082     * connection in the transaction
083     */
084    public void setSharedConnection(Connection sharedConnection) throws SQLException {
085        if (this.sharedConnection != null) {
086            throw new IllegalStateException("A shared connection is already set");
087        }
088
089        // This is the first use of the connection in this transaction, so we must
090        // enlist it in the transaction
091        Transaction transaction = getTransaction();
092        try {
093            XAResource xaResource = transactionRegistry.getXAResource(sharedConnection);
094            if ( !transaction.enlistResource(xaResource) ) {
095                throw new SQLException("Unable to enlist connection in transaction: enlistResource returns 'false'.");
096            }
097        } catch (RollbackException e) {
098            // transaction was rolled back... proceed as if there never was a transaction
099        } catch (SystemException e) {
100            throw new SQLException("Unable to enlist connection the transaction", e);
101        }
102
103        this.sharedConnection = sharedConnection;
104    }
105
106    /**
107     * Adds a listener for transaction completion events.
108     *
109     * @param listener the listener to add
110     * @throws SQLException if a problem occurs adding the listener to the transaction
111     */
112    public void addTransactionContextListener(final TransactionContextListener listener) throws SQLException {
113        try {
114            getTransaction().registerSynchronization(new Synchronization() {
115                @Override
116                public void beforeCompletion() {
117                }
118
119                @Override
120                public void afterCompletion(int status) {
121                    listener.afterCompletion(TransactionContext.this, status == Status.STATUS_COMMITTED);
122                }
123            });
124        } catch (RollbackException e) {
125            // JTA spec doesn't let us register with a transaction marked rollback only
126            // just ignore this and the tx state will be cleared another way.
127        } catch (Exception e) {
128            throw new SQLException("Unable to register transaction context listener", e);
129        }
130    }
131
132    /**
133     * True if the transaction is active or marked for rollback only.
134     * @return true if the transaction is active or marked for rollback only; false otherwise
135     * @throws SQLException if a problem occurs obtaining the transaction status
136     */
137    public boolean isActive() throws SQLException {
138        try {
139            Transaction transaction = this.transactionRef.get();
140            if (transaction == null) {
141                return false;
142            }
143            int status = transaction.getStatus();
144            return status == Status.STATUS_ACTIVE || status == Status.STATUS_MARKED_ROLLBACK;
145        } catch (SystemException e) {
146            throw new SQLException("Unable to get transaction status", e);
147        }
148    }
149
150    private Transaction getTransaction() throws SQLException {
151        Transaction transaction = this.transactionRef.get();
152        if (transaction == null) {
153            throw new SQLException("Unable to enlist connection because the transaction has been garbage collected");
154        }
155        return transaction;
156    }
157}