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.TransactionSynchronizationRegistry;
026import javax.transaction.xa.XAResource;
027import java.sql.Connection;
028import java.sql.SQLException;
029import java.util.Objects;
030import java.lang.ref.WeakReference;
031
032/**
033 * TransactionContext represents the association between a single XAConnectionFactory and a Transaction. This context
034 * contains a single shared connection which should be used by all ManagedConnections for the XAConnectionFactory, the
035 * ability to listen for the transaction completion event, and a method to check the status of the transaction.
036 *
037 * @since 2.0
038 */
039public class TransactionContext {
040    private final TransactionRegistry transactionRegistry;
041    private final WeakReference<Transaction> transactionRef;
042    private final TransactionSynchronizationRegistry transactionSynchronizationRegistry;
043    private Connection sharedConnection;
044    private boolean transactionComplete;
045
046    /**
047     * Creates a TransactionContext for the specified Transaction and TransactionRegistry. The TransactionRegistry is
048     * used to obtain the XAResource for the shared connection when it is enlisted in the transaction.
049     *
050     * @param transactionRegistry
051     *            the TransactionRegistry used to obtain the XAResource for the shared connection
052     * @param transaction
053     *            the transaction
054     * @param transactionSynchronizationRegistry
055     *              The optional TSR to register synchronizations with
056     * @since 2.6.0
057     */
058    public TransactionContext(final TransactionRegistry transactionRegistry, final Transaction transaction,
059                              final TransactionSynchronizationRegistry transactionSynchronizationRegistry) {
060        Objects.requireNonNull(transactionRegistry, "transactionRegistry is null");
061        Objects.requireNonNull(transaction, "transaction is null");
062        this.transactionRegistry = transactionRegistry;
063        this.transactionRef = new WeakReference<>(transaction);
064        this.transactionComplete = false;
065        this.transactionSynchronizationRegistry = transactionSynchronizationRegistry;
066    }
067
068    /**
069     * Provided for backwards compatibility
070     *
071     * @param transactionRegistry the TransactionRegistry used to obtain the XAResource for the
072     * shared connection
073     * @param transaction the transaction
074     */
075    public TransactionContext(final TransactionRegistry transactionRegistry, final Transaction transaction) {
076        this (transactionRegistry, transaction, null);
077    }
078
079    /**
080     * Gets the connection shared by all ManagedConnections in the transaction. Specifically, connection using the same
081     * XAConnectionFactory from which the TransactionRegistry was obtained.
082     *
083     * @return the shared connection for this transaction
084     */
085    public Connection getSharedConnection() {
086        return sharedConnection;
087    }
088
089    /**
090     * Sets the shared connection for this transaction. The shared connection is enlisted in the transaction.
091     *
092     * @param sharedConnection
093     *            the shared connection
094     * @throws SQLException
095     *             if a shared connection is already set, if XAResource for the connection could not be found in the
096     *             transaction registry, or if there was a problem enlisting the connection in the transaction
097     */
098    public void setSharedConnection(final Connection sharedConnection) throws SQLException {
099        if (this.sharedConnection != null) {
100            throw new IllegalStateException("A shared connection is already set");
101        }
102
103        // This is the first use of the connection in this transaction, so we must
104        // enlist it in the transaction
105        final Transaction transaction = getTransaction();
106        try {
107            final XAResource xaResource = transactionRegistry.getXAResource(sharedConnection);
108            if (!transaction.enlistResource(xaResource)) {
109                throw new SQLException("Unable to enlist connection in transaction: enlistResource returns 'false'.");
110            }
111        } catch (final IllegalStateException e) {
112            // This can happen if the transaction is already timed out
113            throw new SQLException("Unable to enlist connection in the transaction", e);
114        } catch (final RollbackException e) {
115            // transaction was rolled back... proceed as if there never was a transaction
116        } catch (final SystemException e) {
117            throw new SQLException("Unable to enlist connection the transaction", e);
118        }
119
120        this.sharedConnection = sharedConnection;
121    }
122
123    /**
124     * Adds a listener for transaction completion events.
125     *
126     * @param listener
127     *            the listener to add
128     * @throws SQLException
129     *             if a problem occurs adding the listener to the transaction
130     */
131    public void addTransactionContextListener(final TransactionContextListener listener) throws SQLException {
132        try {
133            if (!isActive()) {
134                final Transaction transaction = this.transactionRef.get();
135                listener.afterCompletion(TransactionContext.this,
136                        transaction == null ? false : transaction.getStatus() == Status.STATUS_COMMITTED);
137                return;
138            }
139            final Synchronization s = new Synchronization() {
140                @Override
141                public void beforeCompletion() {
142                    // empty
143                }
144
145                @Override
146                public void afterCompletion(final int status) {
147                    listener.afterCompletion(TransactionContext.this, status == Status.STATUS_COMMITTED);
148                }
149            };
150            if (transactionSynchronizationRegistry != null) {
151                transactionSynchronizationRegistry.registerInterposedSynchronization(s);
152            } else {
153                getTransaction().registerSynchronization(s);
154            }
155        } catch (final RollbackException e) {
156            // JTA spec doesn't let us register with a transaction marked rollback only
157            // just ignore this and the tx state will be cleared another way.
158        } catch (final Exception e) {
159            throw new SQLException("Unable to register transaction context listener", e);
160        }
161    }
162
163    /**
164     * True if the transaction is active or marked for rollback only.
165     *
166     * @return true if the transaction is active or marked for rollback only; false otherwise
167     * @throws SQLException
168     *             if a problem occurs obtaining the transaction status
169     */
170    public boolean isActive() throws SQLException {
171        try {
172            final Transaction transaction = this.transactionRef.get();
173            if (transaction == null) {
174                return false;
175            }
176            final int status = transaction.getStatus();
177            return status == Status.STATUS_ACTIVE || status == Status.STATUS_MARKED_ROLLBACK;
178        } catch (final SystemException e) {
179            throw new SQLException("Unable to get transaction status", e);
180        }
181    }
182
183    private Transaction getTransaction() throws SQLException {
184        final Transaction transaction = this.transactionRef.get();
185        if (transaction == null) {
186            throw new SQLException("Unable to enlist connection because the transaction has been garbage collected");
187        }
188        return transaction;
189    }
190
191    /**
192     * Sets the transaction complete flag to true.
193     *
194     * @since 2.4.0
195     */
196    public void completeTransaction() {
197        this.transactionComplete = true;
198    }
199
200    /**
201     * Gets the transaction complete flag to true.
202     *
203     * @return The transaction complete flag.
204     *
205     * @since 2.4.0
206     */
207    public boolean isTransactionComplete() {
208        return this.transactionComplete;
209    }
210}