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.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 (XAException) new XAException().initCause(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 /**
183 * This method does nothing since the LocalXAConnection does not support two-phase-commit. This method will
184 * return XAResource.XA_RDONLY if the connection isReadOnly(). This assumes that the physical connection is
185 * wrapped with a proxy that prevents an application from changing the read-only flag while enrolled in a
186 * transaction.
187 *
188 * @param xid
189 * the id of the transaction branch for this connection
190 * @return XAResource.XA_RDONLY if the connection.isReadOnly(); XAResource.XA_OK otherwise
191 */
192 @Override
193 public synchronized int prepare(final Xid xid) {
194 // if the connection is read-only, then the resource is read-only
195 // NOTE: this assumes that the outer proxy throws an exception when application code
196 // attempts to set this in a transaction
197 try {
198 if (connection.isReadOnly()) {
199 // update the auto commit flag
200 connection.setAutoCommit(originalAutoCommit);
201
202 // tell the transaction manager we are read only
203 return XA_RDONLY;
204 }
205 } catch (final SQLException ignored) {
206 // no big deal
207 }
208
209 // this is a local (one phase) only connection, so we can't prepare
210 return XA_OK;
211 }
212
213 /**
214 * Always returns a zero length Xid array. The LocalXAConnectionFactory cannot support recovery, so no xids
215 * will ever be found.
216 *
217 * @param flag
218 * ignored since recovery is not supported
219 * @return always a zero length Xid array.
220 */
221 @Override
222 public Xid[] recover(final int flag) {
223 return EMPTY_XID_ARRAY;
224 }
225
226 /**
227 * Rolls back the transaction and restores the original auto commit setting.
228 *
229 * @param xid
230 * the id of the transaction branch for this connection
231 * @throws XAException
232 * if connection.rollback() throws an SQLException
233 */
234 @Override
235 public synchronized void rollback(final Xid xid) throws XAException {
236 Objects.requireNonNull(xid, "xid");
237 if (!checkCurrentXid().equals(xid)) {
238 throw new XAException("Invalid Xid: expected " + this.currentXid + ", but was " + xid);
239 }
240
241 try {
242 connection.rollback();
243 } catch (final SQLException e) {
244 throw (XAException) new XAException().initCause(e);
245 } finally {
246 try {
247 connection.setAutoCommit(originalAutoCommit);
248 } catch (final SQLException ignored) {
249 // Ignored.
250 }
251 this.currentXid = null;
252 }
253 }
254
255 /**
256 * Always returns false since we have no way to set a transaction timeout on a JDBC connection.
257 *
258 * @param transactionTimeout
259 * ignored since we have no way to set a transaction timeout on a JDBC connection
260 * @return always false
261 */
262 @Override
263 public boolean setTransactionTimeout(final int transactionTimeout) {
264 return false;
265 }
266
267 /**
268 * Signals that a connection has been enrolled in a transaction. This method saves off the current auto
269 * commit flag, and then disables auto commit. The original auto commit setting is restored when the transaction
270 * completes.
271 *
272 * @param xid
273 * the id of the transaction branch for this connection
274 * @param flag
275 * either XAResource.TMNOFLAGS or XAResource.TMRESUME
276 * @throws XAException
277 * if the connection is already enlisted in another transaction, or if auto-commit could not be
278 * disabled
279 */
280 @Override
281 public synchronized void start(final Xid xid, final int flag) throws XAException {
282 if (flag == TMNOFLAGS) {
283 // first time in this transaction
284
285 // make sure we aren't already in another tx
286 if (this.currentXid != null) {
287 throw new XAException("Already enlisted in another transaction with xid " + xid);
288 }
289
290 // save off the current auto commit flag, so it can be restored after the transaction completes
291 try {
292 originalAutoCommit = connection.getAutoCommit();
293 } catch (final SQLException ignored) {
294 // no big deal, just assume it was off
295 originalAutoCommit = true;
296 }
297
298 // update the auto commit flag
299 try {
300 connection.setAutoCommit(false);
301 } catch (final SQLException e) {
302 throw (XAException) new XAException("Count not turn off auto commit for a XA transaction")
303 .initCause(e);
304 }
305
306 this.currentXid = xid;
307 } else if (flag == TMRESUME) {
308 if (!xid.equals(this.currentXid)) {
309 throw new XAException("Attempting to resume in different transaction: expected " + this.currentXid
310 + ", but was " + xid);
311 }
312 } else {
313 throw new XAException("Unknown start flag " + flag);
314 }
315 }
316 }
317 private final TransactionRegistry transactionRegistry;
318
319 private final ConnectionFactory connectionFactory;
320
321 /**
322 * Creates an LocalXAConnectionFactory which uses the specified connection factory to create database connections.
323 * The connections are enlisted into transactions using the specified transaction manager.
324 *
325 * @param transactionManager
326 * the transaction manager in which connections will be enlisted
327 * @param connectionFactory
328 * the connection factory from which connections will be retrieved
329 */
330 public LocalXAConnectionFactory(final TransactionManager transactionManager,
331 final ConnectionFactory connectionFactory) {
332 this(transactionManager, null, connectionFactory);
333 }
334
335 /**
336 * Creates an LocalXAConnectionFactory which uses the specified connection factory to create database connections.
337 * The connections are enlisted into transactions using the specified transaction manager.
338 *
339 * @param transactionManager
340 * the transaction manager in which connections will be enlisted
341 * @param transactionSynchronizationRegistry
342 * the optional TSR to register synchronizations with
343 * @param connectionFactory
344 * the connection factory from which connections will be retrieved
345 * @since 2.8.0
346 */
347 public LocalXAConnectionFactory(final TransactionManager transactionManager,
348 final TransactionSynchronizationRegistry transactionSynchronizationRegistry,
349 final ConnectionFactory connectionFactory) {
350 Objects.requireNonNull(transactionManager, "transactionManager");
351 Objects.requireNonNull(connectionFactory, "connectionFactory");
352 this.transactionRegistry = new TransactionRegistry(transactionManager, transactionSynchronizationRegistry);
353 this.connectionFactory = connectionFactory;
354 }
355
356 @Override
357 public Connection createConnection() throws SQLException {
358 // create a new connection
359 final Connection connection = connectionFactory.createConnection();
360
361 // create a XAResource to manage the connection during XA transactions
362 final XAResource xaResource = new LocalXAResource(connection);
363
364 // register the XA resource for the connection
365 transactionRegistry.registerConnection(connection, xaResource);
366
367 return connection;
368 }
369
370 /**
371 * Gets the connection factory.
372 *
373 * @return The connection factory.
374 * @since 2.6.0
375 */
376 public ConnectionFactory getConnectionFactory() {
377 return connectionFactory;
378 }
379
380 /**
381 * Gets the transaction registry.
382 *
383 * @return The transaction registry.
384 */
385 @Override
386 public TransactionRegistry getTransactionRegistry() {
387 return transactionRegistry;
388 }
389
390 }