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 }