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.concurrent.locks.Lock;
023import java.util.concurrent.locks.ReentrantLock;
024
025import org.apache.commons.dbcp2.DelegatingConnection;
026import org.apache.commons.pool2.ObjectPool;
027
028/**
029 * ManagedConnection is responsible for managing a database connection in a transactional environment (typically called
030 * "Container Managed"). A managed connection operates like any other connection when no global transaction (a.k.a. XA
031 * transaction or JTA Transaction) is in progress. When a global transaction is active a single physical connection to
032 * the database is used by all ManagedConnections accessed in the scope of the transaction. Connection sharing means
033 * that all data access during a transaction has a consistent view of the database. When the global transaction is
034 * committed or rolled back the enlisted connections are committed or rolled back. Typically upon transaction
035 * completion, a connection returns to the auto commit setting in effect before being enlisted in the transaction, but
036 * some vendors do not properly implement this.
037 * <p>
038 * When enlisted in a transaction the setAutoCommit(), commit(), rollback(), and setReadOnly() methods throw a
039 * SQLException. This is necessary to assure that the transaction completes as a single unit.
040 * </p>
041 *
042 * @param <C>
043 *            the Connection type
044 *
045 * @since 2.0
046 */
047public class ManagedConnection<C extends Connection> extends DelegatingConnection<C> {
048
049    /**
050     * Delegates to {@link ManagedConnection#transactionComplete()} for transaction completion events.
051     *
052     * @since 2.0
053     */
054    protected class CompletionListener implements TransactionContextListener {
055        @Override
056        public void afterCompletion(final TransactionContext completedContext, final boolean committed) {
057            if (completedContext == transactionContext) {
058                transactionComplete();
059            }
060        }
061    }
062    private final ObjectPool<C> pool;
063    private final TransactionRegistry transactionRegistry;
064    private final boolean accessToUnderlyingConnectionAllowed;
065    private TransactionContext transactionContext;
066    private boolean isSharedConnection;
067
068    private final Lock lock;
069
070    /**
071     * Constructs a new instance responsible for managing a database connection in a transactional environment.
072     *
073     * @param pool
074     *            The connection pool.
075     * @param transactionRegistry
076     *            The transaction registry.
077     * @param accessToUnderlyingConnectionAllowed
078     *            Whether or not to allow access to the underlying Connection.
079     * @throws SQLException
080     *             Thrown when there is problem managing transactions.
081     */
082    public ManagedConnection(final ObjectPool<C> pool, final TransactionRegistry transactionRegistry,
083            final boolean accessToUnderlyingConnectionAllowed) throws SQLException {
084        super(null);
085        this.pool = pool;
086        this.transactionRegistry = transactionRegistry;
087        this.accessToUnderlyingConnectionAllowed = accessToUnderlyingConnectionAllowed;
088        this.lock = new ReentrantLock();
089        updateTransactionStatus();
090    }
091
092    @Override
093    protected void checkOpen() throws SQLException {
094        super.checkOpen();
095        updateTransactionStatus();
096    }
097
098    @Override
099    public void close() throws SQLException {
100        if (!isClosedInternal()) {
101            // Don't actually close the connection if in a transaction. The
102            // connection will be closed by the transactionComplete method.
103            //
104            // DBCP-484 we need to make sure setClosedInternal(true) being
105            // invoked if transactionContext is not null as this value will
106            // be modified by the transactionComplete method which could run
107            // in the different thread with the transaction calling back.
108            lock.lock();
109            try {
110                if (transactionContext == null || transactionContext.isTransactionComplete()) {
111                    super.close();
112                }
113            } finally {
114                try {
115                    setClosedInternal(true);
116                } finally {
117                    lock.unlock();
118                }
119            }
120        }
121    }
122
123    @Override
124    public void commit() throws SQLException {
125        if (transactionContext != null) {
126            throw new SQLException("Commit can not be set while enrolled in a transaction");
127        }
128        super.commit();
129    }
130
131    @Override
132    public C getDelegate() {
133        if (isAccessToUnderlyingConnectionAllowed()) {
134            return getDelegateInternal();
135        }
136        return null;
137    }
138
139    //
140    // The following methods can't be used while enlisted in a transaction
141    //
142
143    @Override
144    public Connection getInnermostDelegate() {
145        if (isAccessToUnderlyingConnectionAllowed()) {
146            return super.getInnermostDelegateInternal();
147        }
148        return null;
149    }
150
151    /**
152     * @return The transaction context.
153     * @since 2.6.0
154     */
155    public TransactionContext getTransactionContext() {
156        return transactionContext;
157    }
158
159    /**
160     * @return The transaction registry.
161     * @since 2.6.0
162     */
163    public TransactionRegistry getTransactionRegistry() {
164        return transactionRegistry;
165    }
166
167    /**
168     * If false, getDelegate() and getInnermostDelegate() will return null.
169     *
170     * @return if false, getDelegate() and getInnermostDelegate() will return null
171     */
172    public boolean isAccessToUnderlyingConnectionAllowed() {
173        return accessToUnderlyingConnectionAllowed;
174    }
175
176    //
177    // Methods for accessing the delegate connection
178    //
179
180    @Override
181    public void rollback() throws SQLException {
182        if (transactionContext != null) {
183            throw new SQLException("Commit can not be set while enrolled in a transaction");
184        }
185        super.rollback();
186    }
187
188    @Override
189    public void setAutoCommit(final boolean autoCommit) throws SQLException {
190        if (transactionContext != null) {
191            throw new SQLException("Auto-commit can not be set while enrolled in a transaction");
192        }
193        super.setAutoCommit(autoCommit);
194    }
195
196    @Override
197    public void setReadOnly(final boolean readOnly) throws SQLException {
198        if (transactionContext != null) {
199            throw new SQLException("Read-only can not be set while enrolled in a transaction");
200        }
201        super.setReadOnly(readOnly);
202    }
203
204    /**
205     * Completes the transaction.
206     */
207    protected void transactionComplete() {
208        lock.lock();
209        try {
210            transactionContext.completeTransaction();
211        } finally {
212            lock.unlock();
213        }
214
215        // If we were using a shared connection, clear the reference now that
216        // the transaction has completed
217        if (isSharedConnection) {
218            setDelegate(null);
219            isSharedConnection = false;
220        }
221
222        // autoCommit may have been changed directly on the underlying connection
223        clearCachedState();
224
225        // If this connection was closed during the transaction and there is
226        // still a delegate present close it
227        final Connection delegate = getDelegateInternal();
228        if (isClosedInternal() && delegate != null) {
229            try {
230                setDelegate(null);
231
232                if (!delegate.isClosed()) {
233                    delegate.close();
234                }
235            } catch (final SQLException ignored) {
236                // Not a whole lot we can do here as connection is closed
237                // and this is a transaction callback so there is no
238                // way to report the error.
239            }
240        }
241    }
242
243    private void updateTransactionStatus() throws SQLException {
244        // if there is a is an active transaction context, assure the transaction context hasn't changed
245        if (transactionContext != null && !transactionContext.isTransactionComplete()) {
246            if (transactionContext.isActive()) {
247                if (transactionContext != transactionRegistry.getActiveTransactionContext()) {
248                    throw new SQLException("Connection can not be used while enlisted in another transaction");
249                }
250                return;
251            }
252            // transaction should have been cleared up by TransactionContextListener, but in
253            // rare cases another lister could have registered which uses the connection before
254            // our listener is called. In that rare case, trigger the transaction complete call now
255            transactionComplete();
256        }
257
258        // the existing transaction context ended (or we didn't have one), get the active transaction context
259        transactionContext = transactionRegistry.getActiveTransactionContext();
260
261        // if there is an active transaction context and it already has a shared connection, use it
262        if (transactionContext != null && transactionContext.getSharedConnection() != null) {
263            // A connection for the connection factory has already been enrolled
264            // in the transaction, replace our delegate with the enrolled connection
265
266            // return current connection to the pool
267            @SuppressWarnings("resource")
268            final C connection = getDelegateInternal();
269            setDelegate(null);
270            if (connection != null && transactionContext.getSharedConnection() != connection) {
271                try {
272                    pool.returnObject(connection);
273                } catch (final Exception ignored) {
274                    // whatever... try to invalidate the connection
275                    try {
276                        pool.invalidateObject(connection);
277                    } catch (final Exception ignore) {
278                        // no big deal
279                    }
280                }
281            }
282
283            // add a listener to the transaction context
284            transactionContext.addTransactionContextListener(new CompletionListener());
285
286            // Set our delegate to the shared connection. Note that this will
287            // always be of type C since it has been shared by another
288            // connection from the same pool.
289            @SuppressWarnings("unchecked")
290            final C shared = (C) transactionContext.getSharedConnection();
291            setDelegate(shared);
292
293            // remember that we are using a shared connection so it can be cleared after the
294            // transaction completes
295            isSharedConnection = true;
296        } else {
297            C connection = getDelegateInternal();
298            // if our delegate is null, create one
299            if (connection == null) {
300                try {
301                    // borrow a new connection from the pool
302                    connection = pool.borrowObject();
303                    setDelegate(connection);
304                } catch (final Exception e) {
305                    throw new SQLException("Unable to acquire a new connection from the pool", e);
306                }
307            }
308
309            // if we have a transaction, out delegate becomes the shared delegate
310            if (transactionContext != null) {
311                // add a listener to the transaction context
312                transactionContext.addTransactionContextListener(new CompletionListener());
313
314                // register our connection as the shared connection
315                try {
316                    transactionContext.setSharedConnection(connection);
317                } catch (final SQLException e) {
318                    // transaction is hosed
319                    transactionContext = null;
320                    try {
321                        pool.invalidateObject(connection);
322                    } catch (final Exception e1) {
323                        // we are try but no luck
324                    }
325                    throw e;
326                }
327            }
328        }
329        // autoCommit may have been changed directly on the underlying
330        // connection
331        clearCachedState();
332    }
333}