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 org.apache.commons.dbcp2.ConnectionFactory; 021 022import javax.transaction.TransactionManager; 023import javax.transaction.TransactionSynchronizationRegistry; 024import javax.transaction.xa.XAException; 025import javax.transaction.xa.XAResource; 026import javax.transaction.xa.Xid; 027import java.sql.Connection; 028import java.sql.SQLException; 029import java.util.Objects; 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 * LocalXAResource is a fake XAResource for non-XA connections. When a transaction is started the connection 041 * auto-commit is turned off. When the connection is committed or rolled back, the commit or rollback method is 042 * called on the connection and then the original auto-commit value is restored. 043 * <p> 044 * The LocalXAResource also respects the connection read-only setting. If the connection is read-only the commit 045 * method will not be called, and the prepare method returns the XA_RDONLY. 046 * </p> 047 * It is assumed that the wrapper around a managed connection disables the setAutoCommit(), commit(), rollback() and 048 * setReadOnly() methods while a transaction is in progress. 049 * 050 * @since 2.0 051 */ 052 protected static class LocalXAResource implements XAResource { 053 private final Connection connection; 054 private Xid currentXid; // @GuardedBy("this") 055 private boolean originalAutoCommit; // @GuardedBy("this") 056 057 public LocalXAResource(final Connection localTransaction) { 058 this.connection = localTransaction; 059 } 060 061 /** 062 * Commits the transaction and restores the original auto commit setting. 063 * 064 * @param xid 065 * the id of the transaction branch for this connection 066 * @param flag 067 * ignored 068 * @throws XAException 069 * if connection.commit() throws an SQLException 070 */ 071 @Override 072 public synchronized void commit(final Xid xid, final boolean flag) throws XAException { 073 Objects.requireNonNull(xid, "xid is null"); 074 if (this.currentXid == null) { 075 throw new XAException("There is no current transaction"); 076 } 077 if (!this.currentXid.equals(xid)) { 078 throw new XAException("Invalid Xid: expected " + this.currentXid + ", but was " + xid); 079 } 080 081 try { 082 // make sure the connection isn't already closed 083 if (connection.isClosed()) { 084 throw new XAException("Connection is closed"); 085 } 086 087 // A read only connection should not be committed 088 if (!connection.isReadOnly()) { 089 connection.commit(); 090 } 091 } catch (final SQLException e) { 092 throw (XAException) new XAException().initCause(e); 093 } finally { 094 try { 095 connection.setAutoCommit(originalAutoCommit); 096 } catch (final SQLException e) { 097 // ignore 098 } 099 this.currentXid = null; 100 } 101 } 102 103 /** 104 * This method does nothing. 105 * 106 * @param xid 107 * the id of the transaction branch for this connection 108 * @param flag 109 * ignored 110 * @throws XAException 111 * if the connection is already enlisted in another transaction 112 */ 113 @Override 114 public synchronized void end(final Xid xid, final int flag) throws XAException { 115 Objects.requireNonNull(xid, "xid is null"); 116 if (!this.currentXid.equals(xid)) { 117 throw new XAException("Invalid Xid: expected " + this.currentXid + ", but was " + xid); 118 } 119 120 // This notification tells us that the application server is done using this 121 // connection for the time being. The connection is still associated with an 122 // open transaction, so we must still wait for the commit or rollback method 123 } 124 125 /** 126 * Clears the currently associated transaction if it is the specified xid. 127 * 128 * @param xid 129 * the id of the transaction to forget 130 */ 131 @Override 132 public synchronized void forget(final Xid xid) { 133 if (xid != null && xid.equals(currentXid)) { 134 currentXid = null; 135 } 136 } 137 138 /** 139 * Always returns 0 since we have no way to set a transaction timeout on a JDBC connection. 140 * 141 * @return always 0 142 */ 143 @Override 144 public int getTransactionTimeout() { 145 return 0; 146 } 147 148 /** 149 * Gets the current xid of the transaction branch associated with this XAResource. 150 * 151 * @return the current xid of the transaction branch associated with this XAResource. 152 */ 153 public synchronized Xid getXid() { 154 return currentXid; 155 } 156 157 /** 158 * Returns true if the specified XAResource == this XAResource. 159 * 160 * @param xaResource 161 * the XAResource to test 162 * @return true if the specified XAResource == this XAResource; false otherwise 163 */ 164 @Override 165 public boolean isSameRM(final XAResource xaResource) { 166 return this == xaResource; 167 } 168 169 /** 170 * This method does nothing since the LocalXAConnection does not support two-phase-commit. This method will 171 * return XAResource.XA_RDONLY if the connection isReadOnly(). This assumes that the physical connection is 172 * wrapped with a proxy that prevents an application from changing the read-only flag while enrolled in a 173 * transaction. 174 * 175 * @param xid 176 * the id of the transaction branch for this connection 177 * @return XAResource.XA_RDONLY if the connection.isReadOnly(); XAResource.XA_OK otherwise 178 */ 179 @Override 180 public synchronized int prepare(final Xid xid) { 181 // if the connection is read-only, then the resource is read-only 182 // NOTE: this assumes that the outer proxy throws an exception when application code 183 // attempts to set this in a transaction 184 try { 185 if (connection.isReadOnly()) { 186 // update the auto commit flag 187 connection.setAutoCommit(originalAutoCommit); 188 189 // tell the transaction manager we are read only 190 return XAResource.XA_RDONLY; 191 } 192 } catch (final SQLException ignored) { 193 // no big deal 194 } 195 196 // this is a local (one phase) only connection, so we can't prepare 197 return XAResource.XA_OK; 198 } 199 200 /** 201 * Always returns a zero length Xid array. The LocalXAConnectionFactory can not support recovery, so no xids 202 * will ever be found. 203 * 204 * @param flag 205 * ignored since recovery is not supported 206 * @return always a zero length Xid array. 207 */ 208 @Override 209 public Xid[] recover(final int flag) { 210 return new Xid[0]; 211 } 212 213 /** 214 * Rolls back the transaction and restores the original auto commit setting. 215 * 216 * @param xid 217 * the id of the transaction branch for this connection 218 * @throws XAException 219 * if connection.rollback() throws an SQLException 220 */ 221 @Override 222 public synchronized void rollback(final Xid xid) throws XAException { 223 Objects.requireNonNull(xid, "xid is null"); 224 if (!this.currentXid.equals(xid)) { 225 throw new XAException("Invalid Xid: expected " + this.currentXid + ", but was " + xid); 226 } 227 228 try { 229 connection.rollback(); 230 } catch (final SQLException e) { 231 throw (XAException) new XAException().initCause(e); 232 } finally { 233 try { 234 connection.setAutoCommit(originalAutoCommit); 235 } catch (final SQLException e) { 236 // Ignore. 237 } 238 this.currentXid = null; 239 } 240 } 241 242 /** 243 * Always returns false since we have no way to set a transaction timeout on a JDBC connection. 244 * 245 * @param transactionTimeout 246 * ignored since we have no way to set a transaction timeout on a JDBC connection 247 * @return always false 248 */ 249 @Override 250 public boolean setTransactionTimeout(final int transactionTimeout) { 251 return false; 252 } 253 254 /** 255 * Signals that a the connection has been enrolled in a transaction. This method saves off the current auto 256 * commit flag, and then disables auto commit. The original auto commit setting is restored when the transaction 257 * completes. 258 * 259 * @param xid 260 * the id of the transaction branch for this connection 261 * @param flag 262 * either XAResource.TMNOFLAGS or XAResource.TMRESUME 263 * @throws XAException 264 * if the connection is already enlisted in another transaction, or if auto-commit could not be 265 * disabled 266 */ 267 @Override 268 public synchronized void start(final Xid xid, final int flag) throws XAException { 269 if (flag == XAResource.TMNOFLAGS) { 270 // first time in this transaction 271 272 // make sure we aren't already in another tx 273 if (this.currentXid != null) { 274 throw new XAException("Already enlisted in another transaction with xid " + xid); 275 } 276 277 // save off the current auto commit flag so it can be restored after the transaction completes 278 try { 279 originalAutoCommit = connection.getAutoCommit(); 280 } catch (final SQLException ignored) { 281 // no big deal, just assume it was off 282 originalAutoCommit = true; 283 } 284 285 // update the auto commit flag 286 try { 287 connection.setAutoCommit(false); 288 } catch (final SQLException e) { 289 throw (XAException) new XAException("Count not turn off auto commit for a XA transaction") 290 .initCause(e); 291 } 292 293 this.currentXid = xid; 294 } else if (flag == XAResource.TMRESUME) { 295 if (!xid.equals(this.currentXid)) { 296 throw new XAException("Attempting to resume in different transaction: expected " + this.currentXid 297 + ", but was " + xid); 298 } 299 } else { 300 throw new XAException("Unknown start flag " + flag); 301 } 302 } 303 } 304 private final TransactionRegistry transactionRegistry; 305 306 private final ConnectionFactory connectionFactory; 307 308 /** 309 * Creates an LocalXAConnectionFactory which uses the specified connection factory to create database connections. 310 * The connections are enlisted into transactions using the specified transaction manager. 311 * 312 * @param transactionManager 313 * the transaction manager in which connections will be enlisted 314 * @param connectionFactory 315 * the connection factory from which connections will be retrieved 316 */ 317 public LocalXAConnectionFactory(final TransactionManager transactionManager, 318 final ConnectionFactory connectionFactory) { 319 this(transactionManager, null, connectionFactory); 320 } 321 322 /** 323 * Creates an LocalXAConnectionFactory which uses the specified connection factory to create database connections. 324 * The connections are enlisted into transactions using the specified transaction manager. 325 * 326 * @param transactionManager 327 * the transaction manager in which connections will be enlisted 328 * @param transactionSynchronizationRegistry 329 * the optional TSR to register synchronizations with 330 * @param connectionFactory 331 * the connection factory from which connections will be retrieved 332 * @since 2.8.0 333 */ 334 public LocalXAConnectionFactory(final TransactionManager transactionManager, 335 final TransactionSynchronizationRegistry transactionSynchronizationRegistry, 336 final ConnectionFactory connectionFactory) { 337 Objects.requireNonNull(transactionManager, "transactionManager is null"); 338 Objects.requireNonNull(connectionFactory, "connectionFactory is null"); 339 this.transactionRegistry = new TransactionRegistry(transactionManager, transactionSynchronizationRegistry); 340 this.connectionFactory = connectionFactory; 341 } 342 343 @Override 344 public Connection createConnection() throws SQLException { 345 // create a new connection 346 final Connection connection = connectionFactory.createConnection(); 347 348 // create a XAResource to manage the connection during XA transactions 349 final XAResource xaResource = new LocalXAResource(connection); 350 351 // register the xa resource for the connection 352 transactionRegistry.registerConnection(connection, xaResource); 353 354 return connection; 355 } 356 357 /** 358 * @return The connection factory. 359 * @since 2.6.0 360 */ 361 public ConnectionFactory getConnectionFactory() { 362 return connectionFactory; 363 } 364 365 @Override 366 public TransactionRegistry getTransactionRegistry() { 367 return transactionRegistry; 368 } 369 370}