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