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.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 XAResource.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 XAResource.XA_OK;
211         }
212 
213         /**
214          * Always returns a zero length Xid array. The LocalXAConnectionFactory can not 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 == XAResource.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 == XAResource.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      * @return The connection factory.
372      * @since 2.6.0
373      */
374     public ConnectionFactory getConnectionFactory() {
375         return connectionFactory;
376     }
377 
378     @Override
379     public TransactionRegistry getTransactionRegistry() {
380         return transactionRegistry;
381     }
382 
383 }