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