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