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}