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    *      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 }