    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}