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 * https://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.sql.Connection;
20 import java.sql.SQLException;
21 import java.util.Objects;
22
23 import javax.transaction.TransactionManager;
24 import javax.transaction.TransactionSynchronizationRegistry;
25 import javax.transaction.xa.XAException;
26 import javax.transaction.xa.XAResource;
27 import javax.transaction.xa.Xid;
28
29 import org.apache.commons.dbcp2.ConnectionFactory;
30
31 /**
32 * An implementation of XAConnectionFactory which manages non-XA connections in XA transactions. A non-XA connection
33 * commits and rolls back as part of the XA transaction, but is not recoverable since the connection does not implement
34 * the 2-phase protocol.
35 *
36 * @since 2.0
37 */
38 public class LocalXAConnectionFactory implements XAConnectionFactory {
39
40 /**
41 * LocalXAResource is a fake XAResource for non-XA connections. When a transaction is started the connection
42 * auto-commit is turned off. When the connection is committed or rolled back, the commit or rollback method is
43 * called on the connection and then the original auto-commit value is restored.
44 * <p>
45 * The LocalXAResource also respects the connection read-only setting. If the connection is read-only the commit
46 * method will not be called, and the prepare method returns the XA_RDONLY.
47 * </p>
48 * <p>
49 * It is assumed that the wrapper around a managed connection disables the setAutoCommit(), commit(), rollback() and
50 * setReadOnly() methods while a transaction is in progress.
51 * </p>
52 *
53 * @since 2.0
54 */
55 protected static class LocalXAResource implements XAResource {
56 private static final Xid[] EMPTY_XID_ARRAY = {};
57 private final Connection connection;
58 private Xid currentXid; // @GuardedBy("this")
59 private boolean originalAutoCommit; // @GuardedBy("this")
60
61 /**
62 * Constructs a new instance for a given connection.
63 *
64 * @param localTransaction A connection.
65 */
66 public LocalXAResource(final Connection localTransaction) {
67 this.connection = localTransaction;
68 }
69
70 private Xid checkCurrentXid() throws XAException {
71 if (this.currentXid == null) {
72 throw new XAException("There is no current transaction");
73 }
74 return currentXid;
75 }
76
77 /**
78 * Commits the transaction and restores the original auto commit setting.
79 *
80 * @param xid
81 * the id of the transaction branch for this connection
82 * @param flag
83 * ignored
84 * @throws XAException
85 * if connection.commit() throws an SQLException
86 */
87 @Override
88 public synchronized void commit(final Xid xid, final boolean flag) throws XAException {
89 Objects.requireNonNull(xid, "xid");
90 if (!checkCurrentXid().equals(xid)) {
91 throw new XAException("Invalid Xid: expected " + this.currentXid + ", but was " + xid);
92 }
93
94 try {
95 // make sure the connection isn't already closed
96 if (connection.isClosed()) {
97 throw new XAException("Connection is closed");
98 }
99
100 // A read only connection should not be committed
101 if (!connection.isReadOnly()) {
102 connection.commit();
103 }
104 } catch (final SQLException e) {
105 throw newXAException("Commit failed.", e);
106 } finally {
107 try {
108 connection.setAutoCommit(originalAutoCommit);
109 } catch (final SQLException ignored) {
110 // ignored
111 }
112 this.currentXid = null;
113 }
114 }
115
116 /**
117 * This method does nothing.
118 *
119 * @param xid
120 * the id of the transaction branch for this connection
121 * @param flag
122 * ignored
123 * @throws XAException
124 * if the connection is already enlisted in another transaction
125 */
126 @Override
127 public synchronized void end(final Xid xid, final int flag) throws XAException {
128 Objects.requireNonNull(xid, "xid");
129 if (!checkCurrentXid().equals(xid)) {
130 throw new XAException("Invalid Xid: expected " + this.currentXid + ", but was " + xid);
131 }
132
133 // This notification tells us that the application server is done using this
134 // connection for the time being. The connection is still associated with an
135 // open transaction, so we must still wait for the commit or rollback method
136 }
137
138 /**
139 * Clears the currently associated transaction if it is the specified xid.
140 *
141 * @param xid
142 * the id of the transaction to forget
143 */
144 @Override
145 public synchronized void forget(final Xid xid) {
146 if (xid != null && xid.equals(currentXid)) {
147 currentXid = null;
148 }
149 }
150
151 /**
152 * Always returns 0 since we have no way to set a transaction timeout on a JDBC connection.
153 *
154 * @return always 0
155 */
156 @Override
157 public int getTransactionTimeout() {
158 return 0;
159 }
160
161 /**
162 * Gets the current xid of the transaction branch associated with this XAResource.
163 *
164 * @return the current xid of the transaction branch associated with this XAResource.
165 */
166 public synchronized Xid getXid() {
167 return currentXid;
168 }
169
170 /**
171 * Returns true if the specified XAResource == this XAResource.
172 *
173 * @param xaResource
174 * the XAResource to test
175 * @return true if the specified XAResource == this XAResource; false otherwise
176 */
177 @Override
178 public boolean isSameRM(final XAResource xaResource) {
179 return this == xaResource;
180 }
181
182 private XAException newXAException(final String message, final SQLException cause) {
183 return (XAException) new XAException(message).initCause(cause);
184 }
185
186 /**
187 * This method does nothing since the LocalXAConnection does not support two-phase-commit. This method will
188 * return XAResource.XA_RDONLY if the connection isReadOnly(). This assumes that the physical connection is
189 * wrapped with a proxy that prevents an application from changing the read-only flag while enrolled in a
190 * transaction.
191 *
192 * @param xid
193 * the id of the transaction branch for this connection
194 * @return XAResource.XA_RDONLY if the connection.isReadOnly(); XAResource.XA_OK otherwise
195 */
196 @Override
197 public synchronized int prepare(final Xid xid) {
198 // if the connection is read-only, then the resource is read-only
199 // NOTE: this assumes that the outer proxy throws an exception when application code
200 // attempts to set this in a transaction
201 try {
202 if (connection.isReadOnly()) {
203 // update the auto commit flag
204 connection.setAutoCommit(originalAutoCommit);
205
206 // tell the transaction manager we are read only
207 return XA_RDONLY;
208 }
209 } catch (final SQLException ignored) {
210 // no big deal
211 }
212
213 // this is a local (one phase) only connection, so we can't prepare
214 return XA_OK;
215 }
216
217 /**
218 * Always returns a zero length Xid array. The LocalXAConnectionFactory cannot support recovery, so no xids
219 * will ever be found.
220 *
221 * @param flag
222 * ignored since recovery is not supported
223 * @return always a zero length Xid array.
224 */
225 @Override
226 public Xid[] recover(final int flag) {
227 return EMPTY_XID_ARRAY;
228 }
229
230 /**
231 * Rolls back the transaction and restores the original auto commit setting.
232 *
233 * @param xid
234 * the id of the transaction branch for this connection
235 * @throws XAException
236 * if connection.rollback() throws an SQLException
237 */
238 @Override
239 public synchronized void rollback(final Xid xid) throws XAException {
240 Objects.requireNonNull(xid, "xid");
241 if (!checkCurrentXid().equals(xid)) {
242 throw new XAException("Invalid Xid: expected " + this.currentXid + ", but was " + xid);
243 }
244
245 try {
246 connection.rollback();
247 } catch (final SQLException e) {
248 throw newXAException("Rollback failed.", e);
249 } finally {
250 try {
251 connection.setAutoCommit(originalAutoCommit);
252 } catch (final SQLException ignored) {
253 // Ignored.
254 }
255 this.currentXid = null;
256 }
257 }
258
259 /**
260 * Always returns false since we have no way to set a transaction timeout on a JDBC connection.
261 *
262 * @param transactionTimeout
263 * ignored since we have no way to set a transaction timeout on a JDBC connection
264 * @return always false
265 */
266 @Override
267 public boolean setTransactionTimeout(final int transactionTimeout) {
268 return false;
269 }
270
271 /**
272 * Signals that a connection has been enrolled in a transaction. This method saves off the current auto
273 * commit flag, and then disables auto commit. The original auto commit setting is restored when the transaction
274 * completes.
275 *
276 * @param xid
277 * the id of the transaction branch for this connection
278 * @param flag
279 * either XAResource.TMNOFLAGS or XAResource.TMRESUME
280 * @throws XAException
281 * if the connection is already enlisted in another transaction, or if auto-commit could not be
282 * disabled
283 */
284 @Override
285 public synchronized void start(final Xid xid, final int flag) throws XAException {
286 if (flag == TMNOFLAGS) {
287 // first time in this transaction
288 // make sure we aren't already in another tx
289 if (this.currentXid != null) {
290 throw new XAException("Already enlisted in another transaction with xid " + xid);
291 }
292 // save off the current auto commit flag, so it can be restored after the transaction completes
293 try {
294 originalAutoCommit = connection.getAutoCommit();
295 } catch (final SQLException ignored) {
296 // no big deal, just assume it was off
297 originalAutoCommit = true;
298 }
299 // update the auto commit flag
300 try {
301 connection.setAutoCommit(false);
302 } catch (final SQLException e) {
303 throw newXAException("Count not turn off auto commit for a XA transaction", e);
304 }
305 this.currentXid = xid;
306 } else if (flag == TMRESUME) {
307 if (!xid.equals(this.currentXid)) {
308 throw new XAException("Attempting to resume in different transaction: expected " + this.currentXid + ", but was " + xid);
309 }
310 } else {
311 throw new XAException("Unknown start flag " + flag);
312 }
313 }
314 }
315 private final TransactionRegistry transactionRegistry;
316
317 private final ConnectionFactory connectionFactory;
318
319 /**
320 * Creates an LocalXAConnectionFactory which uses the specified connection factory to create database connections.
321 * The connections are enlisted into transactions using the specified transaction manager.
322 *
323 * @param transactionManager
324 * the transaction manager in which connections will be enlisted
325 * @param connectionFactory
326 * the connection factory from which connections will be retrieved
327 */
328 public LocalXAConnectionFactory(final TransactionManager transactionManager,
329 final ConnectionFactory connectionFactory) {
330 this(transactionManager, null, connectionFactory);
331 }
332
333 /**
334 * Creates an LocalXAConnectionFactory which uses the specified connection factory to create database connections.
335 * The connections are enlisted into transactions using the specified transaction manager.
336 *
337 * @param transactionManager
338 * the transaction manager in which connections will be enlisted
339 * @param transactionSynchronizationRegistry
340 * the optional TSR to register synchronizations with
341 * @param connectionFactory
342 * the connection factory from which connections will be retrieved
343 * @since 2.8.0
344 */
345 public LocalXAConnectionFactory(final TransactionManager transactionManager,
346 final TransactionSynchronizationRegistry transactionSynchronizationRegistry,
347 final ConnectionFactory connectionFactory) {
348 Objects.requireNonNull(transactionManager, "transactionManager");
349 Objects.requireNonNull(connectionFactory, "connectionFactory");
350 this.transactionRegistry = new TransactionRegistry(transactionManager, transactionSynchronizationRegistry);
351 this.connectionFactory = connectionFactory;
352 }
353
354 @Override
355 public Connection createConnection() throws SQLException {
356 // create a new connection
357 final Connection connection = connectionFactory.createConnection();
358
359 // create a XAResource to manage the connection during XA transactions
360 final XAResource xaResource = new LocalXAResource(connection);
361
362 // register the XA resource for the connection
363 transactionRegistry.registerConnection(connection, xaResource);
364
365 return connection;
366 }
367
368 /**
369 * Gets the connection factory.
370 *
371 * @return The connection factory.
372 * @since 2.6.0
373 */
374 public ConnectionFactory getConnectionFactory() {
375 return connectionFactory;
376 }
377
378 /**
379 * Gets the transaction registry.
380 *
381 * @return The transaction registry.
382 */
383 @Override
384 public TransactionRegistry getTransactionRegistry() {
385 return transactionRegistry;
386 }
387
388 }