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