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