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