    001/*
    002 * Licensed to the Apache Software Foundation (ASF) under one or more
    003 * contributor license agreements.  See the NOTICE file distributed with
    004 * this work for additional information regarding copyright ownership.
    005 * The ASF licenses this file to You under the Apache License, Version 2.0
    006 * (the "License"); you may not use this file except in compliance with
    007 * the License.  You may obtain a copy of the License at
    008 *
    009 *      https://www.apache.org/licenses/LICENSE-2.0
    010 *
    011 * Unless required by applicable law or agreed to in writing, software
    012 * distributed under the License is distributed on an "AS IS" BASIS,
    013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    014 * See the License for the specific language governing permissions and
    015 * limitations under the License.
    016 */
    017package org.apache.commons.dbcp2.managed;
    018
    019import java.lang.ref.WeakReference;
    020import java.sql.Connection;
    021import java.sql.SQLException;
    022import java.util.Objects;
    023
    024import javax.transaction.RollbackException;
    025import javax.transaction.Status;
    026import javax.transaction.Synchronization;
    027import javax.transaction.SystemException;
    028import javax.transaction.Transaction;
    029import javax.transaction.TransactionSynchronizationRegistry;
    030import javax.transaction.xa.XAResource;
    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     * Provided for backwards compatibility
    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(final TransactionRegistry transactionRegistry, final Transaction transaction) {
    054        this (transactionRegistry, transaction, null);
    055    }
    056
    057    /**
    058     * Creates a TransactionContext for the specified Transaction and TransactionRegistry. The TransactionRegistry is
    059     * used to obtain the XAResource for the shared connection when it is enlisted in the transaction.
    060     *
    061     * @param transactionRegistry
    062     *            the TransactionRegistry used to obtain the XAResource for the shared connection
    063     * @param transaction
    064     *            the transaction
    065     * @param transactionSynchronizationRegistry
    066     *              The optional TSR to register synchronizations with
    067     * @since 2.6.0
    068     */
    069    public TransactionContext(final TransactionRegistry transactionRegistry, final Transaction transaction,
    070                              final TransactionSynchronizationRegistry transactionSynchronizationRegistry) {
    071        Objects.requireNonNull(transactionRegistry, "transactionRegistry");
    072        Objects.requireNonNull(transaction, "transaction");
    073        this.transactionRegistry = transactionRegistry;
    074        this.transactionRef = new WeakReference<>(transaction);
    075        this.transactionComplete = false;
    076        this.transactionSynchronizationRegistry = transactionSynchronizationRegistry;
    077    }
    078
    079    /**
    080     * Adds a listener for transaction completion events.
    081     *
    082     * @param listener
    083     *            the listener to add
    084     * @throws SQLException
    085     *             if a problem occurs adding the listener to the transaction
    086     */
    087    public void addTransactionContextListener(final TransactionContextListener listener) throws SQLException {
    088        try {
    089            if (!isActive()) {
    090                final Transaction transaction = this.transactionRef.get();
    091                listener.afterCompletion(this, transaction != null && transaction.getStatus() == Status.STATUS_COMMITTED);
    092                return;
    093            }
    094            final Synchronization s = new SynchronizationAdapter() {
    095                @Override
    096                public void afterCompletion(final int status) {
    097                    listener.afterCompletion(TransactionContext.this, status == Status.STATUS_COMMITTED);
    098                }
    099            };
    100            if (transactionSynchronizationRegistry != null) {
    101                transactionSynchronizationRegistry.registerInterposedSynchronization(s);
    102            } else {
    103                getTransaction().registerSynchronization(s);
    104            }
    105        } catch (final RollbackException ignored) {
    106            // JTA spec doesn't let us register with a transaction marked rollback only
    107            // just ignore this and the tx state will be cleared another way.
    108        } catch (final Exception e) {
    109            throw new SQLException("Unable to register transaction context listener", e);
    110        }
    111    }
    112
    113    /**
    114     * Sets the transaction complete flag to true.
    115     *
    116     * @since 2.4.0
    117     */
    118    public void completeTransaction() {
    119        this.transactionComplete = true;
    120    }
    121
    122    /**
    123     * Gets the connection shared by all ManagedConnections in the transaction. Specifically, connection using the same
    124     * XAConnectionFactory from which the TransactionRegistry was obtained.
    125     *
    126     * @return the shared connection for this transaction
    127     */
    128    public Connection getSharedConnection() {
    129        return sharedConnection;
    130    }
    131
    132    private Transaction getTransaction() throws SQLException {
    133        final Transaction transaction = this.transactionRef.get();
    134        if (transaction == null) {
    135            throw new SQLException("Unable to enlist connection because the transaction has been garbage collected");
    136        }
    137        return transaction;
    138    }
    139
    140    /**
    141     * True if the transaction is active or marked for rollback only.
    142     *
    143     * @return true if the transaction is active or marked for rollback only; false otherwise
    144     * @throws SQLException
    145     *             if a problem occurs obtaining the transaction status
    146     */
    147    public boolean isActive() throws SQLException {
    148        try {
    149            final Transaction transaction = this.transactionRef.get();
    150            if (transaction == null) {
    151                return false;
    152            }
    153            final int status = transaction.getStatus();
    154            return status == Status.STATUS_ACTIVE || status == Status.STATUS_MARKED_ROLLBACK;
    155        } catch (final SystemException e) {
    156            throw new SQLException("Unable to get transaction status", e);
    157        }
    158    }
    159
    160    /**
    161     * Gets the transaction complete flag to true.
    162     *
    163     * @return The transaction complete flag.
    164     * @since 2.4.0
    165     */
    166    public boolean isTransactionComplete() {
    167        return this.transactionComplete;
    168    }
    169
    170    /**
    171     * Sets the shared connection for this transaction. The shared connection is enlisted in the transaction.
    172     *
    173     * @param sharedConnection
    174     *            the shared connection
    175     * @throws SQLException
    176     *             if a shared connection is already set, if XAResource for the connection could not be found in the
    177     *             transaction registry, or if there was a problem enlisting the connection in the transaction
    178     */
    179    public void setSharedConnection(final Connection sharedConnection) throws SQLException {
    180        if (this.sharedConnection != null) {
    181            throw new IllegalStateException("A shared connection is already set");
    182        }
    183
    184        // This is the first use of the connection in this transaction, so we must
    185        // enlist it in the transaction
    186        final Transaction transaction = getTransaction();
    187        try {
    188            final XAResource xaResource = transactionRegistry.getXAResource(sharedConnection);
    189            if (!transaction.enlistResource(xaResource)) {
    190                throw new SQLException("Unable to enlist connection in transaction: enlistResource returns 'false'.");
    191            }
    192        } catch (final IllegalStateException e) {
    193            // This can happen if the transaction is already timed out
    194            throw new SQLException("Unable to enlist connection in the transaction", e);
    195        } catch (final RollbackException ignored) {
    196            // transaction was rolled back... proceed as if there never was a transaction
    197        } catch (final SystemException e) {
    198            throw new SQLException("Unable to enlist connection the transaction", e);
    199        }
    200
    201        this.sharedConnection = sharedConnection;
    202    }
    203}