View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *      http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.apache.commons.dbcp2.managed;
18  
19  import java.lang.ref.WeakReference;
20  import java.sql.Connection;
21  import java.sql.SQLException;
22  import java.util.Objects;
23  
24  import javax.transaction.RollbackException;
25  import javax.transaction.Status;
26  import javax.transaction.Synchronization;
27  import javax.transaction.SystemException;
28  import javax.transaction.Transaction;
29  import javax.transaction.TransactionSynchronizationRegistry;
30  import javax.transaction.xa.XAResource;
31  
32  /**
33   * TransactionContext represents the association between a single XAConnectionFactory and a Transaction. This context
34   * contains a single shared connection which should be used by all ManagedConnections for the XAConnectionFactory, the
35   * ability to listen for the transaction completion event, and a method to check the status of the transaction.
36   *
37   * @since 2.0
38   */
39  public class TransactionContext {
40      private final TransactionRegistry transactionRegistry;
41      private final WeakReference<Transaction> transactionRef;
42      private final TransactionSynchronizationRegistry transactionSynchronizationRegistry;
43      private Connection sharedConnection;
44      private boolean transactionComplete;
45  
46      /**
47       * Provided for backwards compatibility
48       *
49       * @param transactionRegistry the TransactionRegistry used to obtain the XAResource for the
50       * shared connection
51       * @param transaction the transaction
52       */
53      public TransactionContext(final TransactionRegistry transactionRegistry, final Transaction transaction) {
54          this (transactionRegistry, transaction, null);
55      }
56  
57      /**
58       * Creates a TransactionContext for the specified Transaction and TransactionRegistry. The TransactionRegistry is
59       * used to obtain the XAResource for the shared connection when it is enlisted in the transaction.
60       *
61       * @param transactionRegistry
62       *            the TransactionRegistry used to obtain the XAResource for the shared connection
63       * @param transaction
64       *            the transaction
65       * @param transactionSynchronizationRegistry
66       *              The optional TSR to register synchronizations with
67       * @since 2.6.0
68       */
69      public TransactionContext(final TransactionRegistry transactionRegistry, final Transaction transaction,
70                                final TransactionSynchronizationRegistry transactionSynchronizationRegistry) {
71          Objects.requireNonNull(transactionRegistry, "transactionRegistry");
72          Objects.requireNonNull(transaction, "transaction");
73          this.transactionRegistry = transactionRegistry;
74          this.transactionRef = new WeakReference<>(transaction);
75          this.transactionComplete = false;
76          this.transactionSynchronizationRegistry = transactionSynchronizationRegistry;
77      }
78  
79      /**
80       * Adds a listener for transaction completion events.
81       *
82       * @param listener
83       *            the listener to add
84       * @throws SQLException
85       *             if a problem occurs adding the listener to the transaction
86       */
87      public void addTransactionContextListener(final TransactionContextListener listener) throws SQLException {
88          try {
89              if (!isActive()) {
90                  final Transaction transaction = this.transactionRef.get();
91                  listener.afterCompletion(this, transaction != null && transaction.getStatus() == Status.STATUS_COMMITTED);
92                  return;
93              }
94              final Synchronization s = new SynchronizationAdapter() {
95                  @Override
96                  public void afterCompletion(final int status) {
97                      listener.afterCompletion(TransactionContext.this, status == Status.STATUS_COMMITTED);
98                  }
99              };
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 }