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