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