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