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