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     */
018    package org.apache.commons.dbcp.managed;
019    
020    import org.apache.commons.dbcp.DelegatingConnection;
021    import org.apache.commons.pool.ObjectPool;
022    
023    import java.sql.Connection;
024    import java.sql.SQLException;
025    
026    /**
027     * ManagedConnection is responsible for managing a database connection in a transactional environment
028     * (typically called "Container Managed").  A managed connection operates like any other connection
029     * when no global transaction (a.k.a. XA transaction or JTA Transaction) is in progress.  When a
030     * global transaction is active a single physical connection to the database is used by all
031     * ManagedConnections accessed in the scope of the transaction.  Connection sharing means that all
032     * data access during a transaction has a consistent view of the database.  When the global transaction
033     * is committed or rolled back the enlisted connections are committed or rolled back.  Typically upon
034     * transaction completion, a connection returns to the auto commit setting in effect before being
035     * enlisted in the transaction, but some vendors do not properly implement this.
036     *
037     * When enlisted in a transaction the setAutoCommit(), commit(), rollback(), and setReadOnly() methods
038     * throw a SQLException.  This is necessary to assure that the transaction completes as a single unit.
039     *
040     * @author Dain Sundstrom
041     * @version $Revision: 892307 $
042     */
043    public class ManagedConnection extends DelegatingConnection {
044        private final ObjectPool pool;
045        private final TransactionRegistry transactionRegistry;
046        private final boolean accessToUnderlyingConnectionAllowed;
047        private TransactionContext transactionContext;
048        private boolean isSharedConnection;
049    
050        public ManagedConnection(ObjectPool pool, TransactionRegistry transactionRegistry, boolean accessToUnderlyingConnectionAllowed) throws SQLException {
051            super(null);
052            this.pool = pool;
053            this.transactionRegistry = transactionRegistry;
054            this.accessToUnderlyingConnectionAllowed = accessToUnderlyingConnectionAllowed;
055            updateTransactionStatus();
056        }
057    
058        protected void checkOpen() throws SQLException {
059            super.checkOpen();
060            updateTransactionStatus();
061        }
062    
063        private void updateTransactionStatus() throws SQLException {
064            // if there is a is an active transaction context, assure the transaction context hasn't changed
065            if (transactionContext != null) {
066                if (transactionContext.isActive()) {
067                    if (transactionContext != transactionRegistry.getActiveTransactionContext()) {
068                        throw new SQLException("Connection can not be used while enlisted in another transaction");
069                    }
070                    return;
071                } else {
072                    // transaction should have been cleared up by TransactionContextListener, but in
073                    // rare cases another lister could have registered which uses the connection before
074                    // our listener is called.  In that rare case, trigger the transaction complete call now
075                    transactionComplete();
076                }
077            }
078    
079            // the existing transaction context ended (or we didn't have one), get the active transaction context
080            transactionContext = transactionRegistry.getActiveTransactionContext();
081    
082            // if there is an active transaction context and it already has a shared connection, use it
083            if (transactionContext != null && transactionContext.getSharedConnection() != null) {
084                // A connection for the connection factory has already been enrolled
085                // in the transaction, replace our delegate with the enrolled connection
086    
087                // return current connection to the pool
088                Connection connection = getDelegateInternal();
089                setDelegate(null);
090                if (connection != null) {
091                    try {
092                        pool.returnObject(connection);
093                    } catch (Exception ignored) {
094                        // whatever... try to invalidate the connection
095                        try {
096                            pool.invalidateObject(connection);
097                        } catch (Exception ignore) {
098                            // no big deal
099                        }
100                    }
101                }
102    
103                // add a listener to the transaction context
104                transactionContext.addTransactionContextListener(new CompletionListener());
105    
106                // set our delegate to the shared connection
107                setDelegate(transactionContext.getSharedConnection());
108    
109                // remember that we are using a shared connection so it can be cleared after the
110                // transaction completes
111                isSharedConnection = true;
112            } else {
113                // if our delegate is null, create one
114                if (getDelegateInternal() == null) {
115                    try {
116                        // borrow a new connection from the pool
117                        Connection connection = (Connection) pool.borrowObject();
118                        setDelegate(connection);
119                    } catch (Exception e) {
120                        throw (SQLException) new SQLException("Unable to acquire a new connection from the pool").initCause(e);
121                    }
122                }
123    
124                // if we have a transaction, out delegate becomes the shared delegate
125                if (transactionContext != null) {
126                    // add a listener to the transaction context
127                    transactionContext.addTransactionContextListener(new CompletionListener());
128    
129                    // register our connection as the shared connection
130                    try {
131                        transactionContext.setSharedConnection(getDelegateInternal());
132                    } catch (SQLException e) {
133                        // transaction is hosed
134                        transactionContext = null;
135                        throw e;
136                    }
137                }
138            }
139        }
140    
141        public void close() throws SQLException {
142            if (!_closed) {
143                try {
144                    // don't actually close the connection if in a transaction
145                    // the connection will be closed by the transactionComplete method
146                    if (transactionContext == null) {
147                        getDelegateInternal().close();
148                    }
149                } finally {
150                    _closed = true;
151                }
152            }
153        }
154    
155        /**
156         * Delegates to {@link ManagedConnection#transactionComplete()} 
157         * for transaction completion events. 
158         */
159        protected class CompletionListener implements TransactionContextListener {
160            public void afterCompletion(TransactionContext completedContext, boolean commited) {
161                if (completedContext == transactionContext) {
162                    transactionComplete();
163                }
164            }
165        }
166    
167        protected void transactionComplete() {
168            transactionContext = null;
169    
170            // if we were using a shared connection, clear the reference now that the transaction has completed
171            if (isSharedConnection) {
172                // for now, just set the delegate to null, it will be created later if needed
173                setDelegate(null);
174                isSharedConnection = false;
175            }
176    
177            // if this connection was closed during the transaction and there is still a delegate present close it
178            Connection delegate = getDelegateInternal();
179            if (_closed && delegate != null) {
180                try {
181                    setDelegate(null);
182    
183                    // don't actually close the connection if in a transaction
184                    if (!delegate.isClosed()) {
185                        // don't use super.close() because it calls passivate() which marks the
186                        // the connection as closed without returning it to the pool
187                        delegate.close();
188                    }
189                } catch (SQLException ignored) {
190                    // not a whole lot we can do here as connection is closed
191                    // and this is a transaction callback so there is no
192                    // way to report the error
193                } finally {
194                    _closed = true;
195                }
196            }
197    
198        }
199    
200        //
201        // The following methods can't be used while enlisted in a transaction
202        //
203    
204        public void setAutoCommit(boolean autoCommit) throws SQLException {
205            if (transactionContext != null) {
206                throw new SQLException("Auto-commit can not be set while enrolled in a transaction");
207            }
208            super.setAutoCommit(autoCommit);
209        }
210    
211    
212        public void commit() throws SQLException {
213            if (transactionContext != null) {
214                throw new SQLException("Commit can not be set while enrolled in a transaction");
215            }
216            super.commit();
217        }
218    
219        public void rollback() throws SQLException {
220            if (transactionContext != null) {
221                throw new SQLException("Commit can not be set while enrolled in a transaction");
222            }
223            super.rollback();
224        }
225    
226    
227        public void setReadOnly(boolean readOnly) throws SQLException {
228            if (transactionContext != null) {
229                throw new SQLException("Read-only can not be set while enrolled in a transaction");
230            }
231            super.setReadOnly(readOnly);
232        }
233    
234        //
235        // Methods for accessing the delegate connection
236        //
237    
238        /**
239         * If false, getDelegate() and getInnermostDelegate() will return null.
240         * @return if false, getDelegate() and getInnermostDelegate() will return null
241         */
242        public boolean isAccessToUnderlyingConnectionAllowed() {
243            return accessToUnderlyingConnectionAllowed;
244        }
245    
246        public Connection getDelegate() {
247            if (isAccessToUnderlyingConnectionAllowed()) {
248                return getDelegateInternal();
249            } else {
250                return null;
251            }
252        }
253    
254        public Connection getInnermostDelegate() {
255            if (isAccessToUnderlyingConnectionAllowed()) {
256                return super.getInnermostDelegateInternal();
257            } else {
258                return null;
259            }
260        }
261    }