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 * http://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 (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}