001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * https://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.apache.commons.dbcp2.managed; 018 019import java.sql.Connection; 020import java.sql.SQLException; 021import java.util.Objects; 022 023import javax.transaction.TransactionManager; 024import javax.transaction.TransactionSynchronizationRegistry; 025import javax.transaction.xa.XAException; 026import javax.transaction.xa.XAResource; 027import javax.transaction.xa.Xid; 028 029import org.apache.commons.dbcp2.ConnectionFactory; 030 031/** 032 * An implementation of XAConnectionFactory which manages non-XA connections in XA transactions. A non-XA connection 033 * commits and rolls back as part of the XA transaction, but is not recoverable since the connection does not implement 034 * the 2-phase protocol. 035 * 036 * @since 2.0 037 */ 038public class LocalXAConnectionFactory implements XAConnectionFactory { 039 040 /** 041 * LocalXAResource is a fake XAResource for non-XA connections. When a transaction is started the connection 042 * auto-commit is turned off. When the connection is committed or rolled back, the commit or rollback method is 043 * called on the connection and then the original auto-commit value is restored. 044 * <p> 045 * The LocalXAResource also respects the connection read-only setting. If the connection is read-only the commit 046 * method will not be called, and the prepare method returns the XA_RDONLY. 047 * </p> 048 * <p> 049 * It is assumed that the wrapper around a managed connection disables the setAutoCommit(), commit(), rollback() and 050 * setReadOnly() methods while a transaction is in progress. 051 * </p> 052 * 053 * @since 2.0 054 */ 055 protected static class LocalXAResource implements XAResource { 056 private static final Xid[] EMPTY_XID_ARRAY = {}; 057 private final Connection connection; 058 private Xid currentXid; // @GuardedBy("this") 059 private boolean originalAutoCommit; // @GuardedBy("this") 060 061 /** 062 * Constructs a new instance for a given connection. 063 * 064 * @param localTransaction A connection. 065 */ 066 public LocalXAResource(final Connection localTransaction) { 067 this.connection = localTransaction; 068 } 069 070 private Xid checkCurrentXid() throws XAException { 071 if (this.currentXid == null) { 072 throw new XAException("There is no current transaction"); 073 } 074 return currentXid; 075 } 076 077 /** 078 * Commits the transaction and restores the original auto commit setting. 079 * 080 * @param xid 081 * the id of the transaction branch for this connection 082 * @param flag 083 * ignored 084 * @throws XAException 085 * if connection.commit() throws an SQLException 086 */ 087 @Override 088 public synchronized void commit(final Xid xid, final boolean flag) throws XAException { 089 Objects.requireNonNull(xid, "xid"); 090 if (!checkCurrentXid().equals(xid)) { 091 throw new XAException("Invalid Xid: expected " + this.currentXid + ", but was " + xid); 092 } 093 094 try { 095 // make sure the connection isn't already closed 096 if (connection.isClosed()) { 097 throw new XAException("Connection is closed"); 098 } 099 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}