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}