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