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 *      http://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     *
165     * @since 2.4.0
166     */
167    public boolean isTransactionComplete() {
168        return this.transactionComplete;
169    }
170
171    /**
172     * Sets the shared connection for this transaction. The shared connection is enlisted in the transaction.
173     *
174     * @param sharedConnection
175     *            the shared connection
176     * @throws SQLException
177     *             if a shared connection is already set, if XAResource for the connection could not be found in the
178     *             transaction registry, or if there was a problem enlisting the connection in the transaction
179     */
180    public void setSharedConnection(final Connection sharedConnection) throws SQLException {
181        if (this.sharedConnection != null) {
182            throw new IllegalStateException("A shared connection is already set");
183        }
184
185        // This is the first use of the connection in this transaction, so we must
186        // enlist it in the transaction
187        final Transaction transaction = getTransaction();
188        try {
189            final XAResource xaResource = transactionRegistry.getXAResource(sharedConnection);
190            if (!transaction.enlistResource(xaResource)) {
191                throw new SQLException("Unable to enlist connection in transaction: enlistResource returns 'false'.");
192            }
193        } catch (final IllegalStateException e) {
194            // This can happen if the transaction is already timed out
195            throw new SQLException("Unable to enlist connection in the transaction", e);
196        } catch (final RollbackException ignored) {
197            // transaction was rolled back... proceed as if there never was a transaction
198        } catch (final SystemException e) {
199            throw new SQLException("Unable to enlist connection the transaction", e);
200        }
201
202        this.sharedConnection = sharedConnection;
203    }
204}