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