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