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