1 /*
2  * Licensed to the Apache Software Foundation (ASF) under one or more
3  * contributor license agreements.  See the NOTICE file distributed with
4  * this work for additional information regarding copyright ownership.
5  * The ASF licenses this file to You under the Apache License, Version 2.0
6  * (the "License"); you may not use this file except in compliance with
7  * the License.  You may obtain a copy of the License at
8  *
9  *      http://www.apache.org/licenses/LICENSE-2.0
10  *
11  * Unless required by applicable law or agreed to in writing, software
12  * distributed under the License is distributed on an "AS IS" BASIS,
13  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14  * See the License for the specific language governing permissions and
15  * limitations under the License.
16  */

17 package org.apache.tomcat.dbcp.dbcp2;
18
19 import java.lang.management.ManagementFactory;
20 import java.sql.Connection;
21 import java.sql.PreparedStatement;
22 import java.sql.ResultSet;
23 import java.sql.SQLException;
24 import java.util.Collection;
25
26 import javax.management.InstanceAlreadyExistsException;
27 import javax.management.MBeanRegistrationException;
28 import javax.management.MBeanServer;
29 import javax.management.NotCompliantMBeanException;
30 import javax.management.ObjectName;
31
32 import org.apache.tomcat.dbcp.pool2.ObjectPool;
33
34 /**
35  * A delegating connection that, rather than closing the underlying connection, returns itself to an {@link ObjectPool}
36  * when closed.
37  *
38  * @since 2.0
39  */

40 public class PoolableConnection extends DelegatingConnection<Connection> implements PoolableConnectionMXBean {
41
42     private static MBeanServer MBEAN_SERVER;
43
44     static {
45         try {
46             MBEAN_SERVER = ManagementFactory.getPlatformMBeanServer();
47         } catch (NoClassDefFoundError | Exception ex) {
48             // ignore - JMX not available
49         }
50     }
51
52     /** The pool to which I should return. */
53     private final ObjectPool<PoolableConnection> pool;
54
55     private final ObjectNameWrapper jmxObjectName;
56
57     // Use a prepared statement for validation, retaining the last used SQL to
58     // check if the validation query has changed.
59     private PreparedStatement validationPreparedStatement;
60     private String lastValidationSql;
61
62     /**
63      * Indicate that unrecoverable SQLException was thrown when using this connection. Such a connection should be
64      * considered broken and not pass validation in the future.
65      */

66     private boolean fatalSqlExceptionThrown = false;
67
68     /**
69      * SQL_STATE codes considered to signal fatal conditions. Overrides the defaults in
70      * {@link Utils#DISCONNECTION_SQL_CODES} (plus anything starting with {@link Utils#DISCONNECTION_SQL_CODE_PREFIX}).
71      */

72     private final Collection<String> disconnectionSqlCodes;
73
74     /** Whether or not to fast fail validation after fatal connection errors */
75     private final boolean fastFailValidation;
76
77     /**
78      *
79      * @param conn
80      *            my underlying connection
81      * @param pool
82      *            the pool to which I should return when closed
83      * @param jmxObjectName
84      *            JMX name
85      * @param disconnectSqlCodes
86      *            SQL_STATE codes considered fatal disconnection errors
87      * @param fastFailValidation
88      *            true means fatal disconnection errors cause subsequent validations to fail immediately (no attempt to
89      *            run query or isValid)
90      */

91     public PoolableConnection(final Connection conn, final ObjectPool<PoolableConnection> pool,
92             final ObjectName jmxObjectName, final Collection<String> disconnectSqlCodes,
93             final boolean fastFailValidation) {
94         super(conn);
95         this.pool = pool;
96         this.jmxObjectName = ObjectNameWrapper.wrap(jmxObjectName);
97         this.disconnectionSqlCodes = disconnectSqlCodes;
98         this.fastFailValidation = fastFailValidation;
99
100         if (jmxObjectName != null) {
101             try {
102                 MBEAN_SERVER.registerMBean(this, jmxObjectName);
103             } catch (InstanceAlreadyExistsException | MBeanRegistrationException | NotCompliantMBeanException e) {
104                 // For now, simply skip registration
105             }
106         }
107     }
108
109     /**
110      *
111      * @param conn
112      *            my underlying connection
113      * @param pool
114      *            the pool to which I should return when closed
115      * @param jmxName
116      *            JMX name
117      */

118     public PoolableConnection(final Connection conn, final ObjectPool<PoolableConnection> pool,
119             final ObjectName jmxName) {
120         this(conn, pool, jmxName, nulltrue);
121     }
122
123     @Override
124     protected void passivate() throws SQLException {
125         super.passivate();
126         setClosedInternal(true);
127     }
128
129     /**
130      * {@inheritDoc}
131      * <p>
132      * This method should not be used by a client to determine whether or not a connection should be return to the
133      * connection pool (by calling {@link #close()}). Clients should always attempt to return a connection to the pool
134      * once it is no longer required.
135      */

136     @Override
137     public boolean isClosed() throws SQLException {
138         if (isClosedInternal()) {
139             return true;
140         }
141
142         if (getDelegateInternal().isClosed()) {
143             // Something has gone wrong. The underlying connection has been
144             // closed without the connection being returned to the pool. Return
145             // it now.
146             close();
147             return true;
148         }
149
150         return false;
151     }
152
153     /**
154      * Returns me to my pool.
155      */

156     @Override
157     public synchronized void close() throws SQLException {
158         if (isClosedInternal()) {
159             // already closed
160             return;
161         }
162
163         boolean isUnderlyingConnectionClosed;
164         try {
165             isUnderlyingConnectionClosed = getDelegateInternal().isClosed();
166         } catch (final SQLException e) {
167             try {
168                 pool.invalidateObject(this);
169             } catch (final IllegalStateException ise) {
170                 // pool is closed, so close the connection
171                 passivate();
172                 getInnermostDelegate().close();
173             } catch (final Exception ie) {
174                 // DO NOTHING the original exception will be rethrown
175             }
176             throw new SQLException("Cannot close connection (isClosed check failed)", e);
177         }
178
179         /*
180          * Can't set close before this code block since the connection needs to be open when validation runs. Can't set
181          * close after this code block since by then the connection will have been returned to the pool and may have
182          * been borrowed by another thread. Therefore, the close flag is set in passivate().
183          */

184         if (isUnderlyingConnectionClosed) {
185             // Abnormal close: underlying connection closed unexpectedly, so we
186             // must destroy this proxy
187             try {
188                 pool.invalidateObject(this);
189             } catch (final IllegalStateException e) {
190                 // pool is closed, so close the connection
191                 passivate();
192                 getInnermostDelegate().close();
193             } catch (final Exception e) {
194                 throw new SQLException("Cannot close connection (invalidating pooled object failed)", e);
195             }
196         } else {
197             // Normal close: underlying connection is still open, so we
198             // simply need to return this proxy to the pool
199             try {
200                 pool.returnObject(this);
201             } catch (final IllegalStateException e) {
202                 // pool is closed, so close the connection
203                 passivate();
204                 getInnermostDelegate().close();
205             } catch (final SQLException e) {
206                 throw e;
207             } catch (final RuntimeException e) {
208                 throw e;
209             } catch (final Exception e) {
210                 throw new SQLException("Cannot close connection (return to pool failed)", e);
211             }
212         }
213     }
214
215     /**
216      * Actually close my underlying {@link Connection}.
217      */

218     @Override
219     public void reallyClose() throws SQLException {
220         if (jmxObjectName != null) {
221             jmxObjectName.unregisterMBean();
222         }
223
224         if (validationPreparedStatement != null) {
225             try {
226                 validationPreparedStatement.close();
227             } catch (final SQLException sqle) {
228                 // Ignore
229             }
230         }
231
232         super.closeInternal();
233     }
234
235     /**
236      * Expose the {@link #toString()} method via a bean getter so it can be read as a property via JMX.
237      */

238     @Override
239     public String getToString() {
240         return toString();
241     }
242
243     /**
244      * Validates the connection, using the following algorithm:
245      * <ol>
246      * <li>If {@code fastFailValidation} (constructor argument) is {@code true} and this connection has previously
247      * thrown a fatal disconnection exception, a {@code SQLException} is thrown.</li>
248      * <li>If {@code sql} is null, the driver's #{@link Connection#isValid(int) isValid(timeout)} is called. If it
249      * returns {@code false}, {@code SQLException} is thrown; otherwise, this method returns successfully.</li>
250      * <li>If {@code sql} is not null, it is executed as a query and if the resulting {@code ResultSet} contains at
251      * least one row, this method returns successfully. If not, {@code SQLException} is thrown.</li>
252      * </ol>
253      *
254      * @param sql
255      *            The validation SQL query.
256      * @param timeoutSeconds
257      *            The validation timeout in seconds.
258      * @throws SQLException
259      *             Thrown when validation fails or an SQLException occurs during validation
260      */

261     public void validate(final String sql, int timeoutSeconds) throws SQLException {
262         if (fastFailValidation && fatalSqlExceptionThrown) {
263             throw new SQLException(Utils.getMessage("poolableConnection.validate.fastFail"));
264         }
265
266         if (sql == null || sql.length() == 0) {
267             if (timeoutSeconds < 0) {
268                 timeoutSeconds = 0;
269             }
270             if (!isValid(timeoutSeconds)) {
271                 throw new SQLException("isValid() returned false");
272             }
273             return;
274         }
275
276         if (!sql.equals(lastValidationSql)) {
277             lastValidationSql = sql;
278             // Has to be the innermost delegate else the prepared statement will
279             // be closed when the pooled connection is passivated.
280             validationPreparedStatement = getInnermostDelegateInternal().prepareStatement(sql);
281         }
282
283         if (timeoutSeconds > 0) {
284             validationPreparedStatement.setQueryTimeout(timeoutSeconds);
285         }
286
287         try (ResultSet rs = validationPreparedStatement.executeQuery()) {
288             if (!rs.next()) {
289                 throw new SQLException("validationQuery didn't return a row");
290             }
291         } catch (final SQLException sqle) {
292             throw sqle;
293         }
294     }
295
296     /**
297      * Checks the SQLState of the input exception and any nested SQLExceptions it wraps.
298      * <p>
299      * If {@link #disconnectionSqlCodes} has been set, sql states are compared to those in the
300      * configured list of fatal exception codes. If this property is not set, codes are compared against the default
301      * codes in {@link Utils#DISCONNECTION_SQL_CODES} and in this case anything starting with #{link
302      * Utils.DISCONNECTION_SQL_CODE_PREFIX} is considered a disconnection.
303      * </p>
304      *
305      * @param e
306      *            SQLException to be examined
307      * @return true if the exception signals a disconnection
308      */

309     private boolean isDisconnectionSqlException(final SQLException e) {
310         boolean fatalException = false;
311         final String sqlState = e.getSQLState();
312         if (sqlState != null) {
313             fatalException = disconnectionSqlCodes == null
314                     ? sqlState.startsWith(Utils.DISCONNECTION_SQL_CODE_PREFIX)
315                             || Utils.DISCONNECTION_SQL_CODES.contains(sqlState)
316                     : disconnectionSqlCodes.contains(sqlState);
317             if (!fatalException) {
318                 final SQLException nextException = e.getNextException();
319                 if (nextException != null && nextException != e) {
320                     fatalException = isDisconnectionSqlException(e.getNextException());
321                 }
322             }
323         }
324         return fatalException;
325     }
326
327     @Override
328     protected void handleException(final SQLException e) throws SQLException {
329         fatalSqlExceptionThrown |= isDisconnectionSqlException(e);
330         super.handleException(e);
331     }
332
333     /**
334      * @return The disconnection SQL codes.
335      * @since 2.6.0
336      */

337     public Collection<String> getDisconnectionSqlCodes() {
338         return disconnectionSqlCodes;
339     }
340
341     /**
342      * @return Whether to fail-fast.
343      * @since 2.6.0
344      */

345     public boolean isFastFailValidation() {
346         return fastFailValidation;
347     }
348 }
349