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