001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.commons.dbcp2;
018
019import java.lang.management.ManagementFactory;
020import java.sql.Connection;
021import java.sql.PreparedStatement;
022import java.sql.ResultSet;
023import java.sql.SQLException;
024import java.util.Collection;
025import java.util.concurrent.Executor;
026
027import javax.management.InstanceAlreadyExistsException;
028import javax.management.MBeanRegistrationException;
029import javax.management.MBeanServer;
030import javax.management.NotCompliantMBeanException;
031import javax.management.ObjectName;
032
033import org.apache.commons.pool2.ObjectPool;
034
035/**
036 * A delegating connection that, rather than closing the underlying connection, returns itself to an {@link ObjectPool}
037 * when closed.
038 *
039 * @since 2.0
040 */
041public class PoolableConnection extends DelegatingConnection<Connection> implements PoolableConnectionMXBean {
042
043    private static MBeanServer MBEAN_SERVER;
044
045    static {
046        try {
047            MBEAN_SERVER = ManagementFactory.getPlatformMBeanServer();
048        } catch (final NoClassDefFoundError | Exception ex) {
049            // ignore - JMX not available
050        }
051    }
052
053    /** The pool to which I should return. */
054    private final ObjectPool<PoolableConnection> pool;
055
056    private final ObjectNameWrapper jmxObjectName;
057
058    // Use a prepared statement for validation, retaining the last used SQL to
059    // check if the validation query has changed.
060    private PreparedStatement validationPreparedStatement;
061    private String lastValidationSql;
062
063    /**
064     * Indicate that unrecoverable SQLException was thrown when using this connection. Such a connection should be
065     * considered broken and not pass validation in the future.
066     */
067    private boolean fatalSqlExceptionThrown;
068
069    /**
070     * SQL_STATE codes considered to signal fatal conditions. Overrides the defaults in
071     * {@link Utils#DISCONNECTION_SQL_CODES} (plus anything starting with {@link Utils#DISCONNECTION_SQL_CODE_PREFIX}).
072     */
073    private final Collection<String> disconnectionSqlCodes;
074
075    /** Whether or not to fast fail validation after fatal connection errors */
076    private final boolean fastFailValidation;
077
078    /**
079     *
080     * @param conn
081     *            my underlying connection
082     * @param pool
083     *            the pool to which I should return when closed
084     * @param jmxName
085     *            JMX name
086     */
087    public PoolableConnection(final Connection conn, final ObjectPool<PoolableConnection> pool,
088            final ObjectName jmxName) {
089        this(conn, pool, jmxName, null, true);
090    }
091
092    /**
093     *
094     * @param conn
095     *            my underlying connection
096     * @param pool
097     *            the pool to which I should return when closed
098     * @param jmxObjectName
099     *            JMX name
100     * @param disconnectSqlCodes
101     *            SQL_STATE codes considered fatal disconnection errors
102     * @param fastFailValidation
103     *            true means fatal disconnection errors cause subsequent validations to fail immediately (no attempt to
104     *            run query or isValid)
105     */
106    public PoolableConnection(final Connection conn, final ObjectPool<PoolableConnection> pool,
107            final ObjectName jmxObjectName, final Collection<String> disconnectSqlCodes,
108            final boolean fastFailValidation) {
109        super(conn);
110        this.pool = pool;
111        this.jmxObjectName = ObjectNameWrapper.wrap(jmxObjectName);
112        this.disconnectionSqlCodes = disconnectSqlCodes;
113        this.fastFailValidation = fastFailValidation;
114
115        if (jmxObjectName != null) {
116            try {
117                MBEAN_SERVER.registerMBean(this, jmxObjectName);
118            } catch (InstanceAlreadyExistsException | MBeanRegistrationException | NotCompliantMBeanException e) {
119                // For now, simply skip registration
120            }
121        }
122    }
123
124    /**
125     * Abort my underlying {@link Connection}.
126     *
127     * @since 2.9.0
128     */
129    @Override
130    public void abort(final Executor executor) throws SQLException {
131        if (jmxObjectName != null) {
132            jmxObjectName.unregisterMBean();
133        }
134        super.abort(executor);
135    }
136
137    /**
138     * Returns me to my pool.
139     */
140    @Override
141    public synchronized void close() throws SQLException {
142        if (isClosedInternal()) {
143            // already closed
144            return;
145        }
146
147        boolean isUnderlyingConnectionClosed;
148        try {
149            isUnderlyingConnectionClosed = getDelegateInternal().isClosed();
150        } catch (final SQLException e) {
151            try {
152                pool.invalidateObject(this);
153            } catch (final IllegalStateException ise) {
154                // pool is closed, so close the connection
155                passivate();
156                getInnermostDelegate().close();
157            } catch (final Exception ie) {
158                // DO NOTHING the original exception will be rethrown
159            }
160            throw new SQLException("Cannot close connection (isClosed check failed)", e);
161        }
162
163        /*
164         * Can't set close before this code block since the connection needs to be open when validation runs. Can't set
165         * close after this code block since by then the connection will have been returned to the pool and may have
166         * been borrowed by another thread. Therefore, the close flag is set in passivate().
167         */
168        if (isUnderlyingConnectionClosed) {
169            // Abnormal close: underlying connection closed unexpectedly, so we
170            // must destroy this proxy
171            try {
172                pool.invalidateObject(this);
173            } catch (final IllegalStateException e) {
174                // pool is closed, so close the connection
175                passivate();
176                getInnermostDelegate().close();
177            } catch (final Exception e) {
178                throw new SQLException("Cannot close connection (invalidating pooled object failed)", e);
179            }
180        } else {
181            // Normal close: underlying connection is still open, so we
182            // simply need to return this proxy to the pool
183            try {
184                pool.returnObject(this);
185            } catch (final IllegalStateException e) {
186                // pool is closed, so close the connection
187                passivate();
188                getInnermostDelegate().close();
189            } catch (final SQLException | RuntimeException e) {
190                throw e;
191            } catch (final Exception e) {
192                throw new SQLException("Cannot close connection (return to pool failed)", e);
193            }
194        }
195    }
196
197    /**
198     * @return The disconnection SQL codes.
199     * @since 2.6.0
200     */
201    public Collection<String> getDisconnectionSqlCodes() {
202        return disconnectionSqlCodes;
203    }
204
205    /**
206     * Expose the {@link #toString()} method via a bean getter so it can be read as a property via JMX.
207     */
208    @Override
209    public String getToString() {
210        return toString();
211    }
212
213    @Override
214    protected void handleException(final SQLException e) throws SQLException {
215        fatalSqlExceptionThrown |= isDisconnectionSqlException(e);
216        super.handleException(e);
217    }
218
219    /**
220     * {@inheritDoc}
221     * <p>
222     * This method should not be used by a client to determine whether or not a connection should be return to the
223     * connection pool (by calling {@link #close()}). Clients should always attempt to return a connection to the pool
224     * once it is no longer required.
225     */
226    @Override
227    public boolean isClosed() throws SQLException {
228        if (isClosedInternal()) {
229            return true;
230        }
231
232        if (getDelegateInternal().isClosed()) {
233            // Something has gone wrong. The underlying connection has been
234            // closed without the connection being returned to the pool. Return
235            // it now.
236            close();
237            return true;
238        }
239
240        return false;
241    }
242
243    /**
244     * Checks the SQLState of the input exception and any nested SQLExceptions it wraps.
245     * <p>
246     * If {@link #disconnectionSqlCodes} has been set, sql states are compared to those in the
247     * configured list of fatal exception codes. If this property is not set, codes are compared against the default
248     * codes in {@link Utils#DISCONNECTION_SQL_CODES} and in this case anything starting with #{link
249     * Utils.DISCONNECTION_SQL_CODE_PREFIX} is considered a disconnection.
250     * </p>
251     *
252     * @param e
253     *            SQLException to be examined
254     * @return true if the exception signals a disconnection
255     */
256    private boolean isDisconnectionSqlException(final SQLException e) {
257        boolean fatalException = false;
258        final String sqlState = e.getSQLState();
259        if (sqlState != null) {
260            fatalException = disconnectionSqlCodes == null
261                    ? sqlState.startsWith(Utils.DISCONNECTION_SQL_CODE_PREFIX)
262                            || Utils.DISCONNECTION_SQL_CODES.contains(sqlState)
263                    : disconnectionSqlCodes.contains(sqlState);
264            if (!fatalException) {
265                final SQLException nextException = e.getNextException();
266                if (nextException != null && nextException != e) {
267                    fatalException = isDisconnectionSqlException(e.getNextException());
268                }
269            }
270        }
271        return fatalException;
272    }
273
274    /**
275     * @return Whether to fail-fast.
276     * @since 2.6.0
277     */
278    public boolean isFastFailValidation() {
279        return fastFailValidation;
280    }
281
282    @Override
283    protected void passivate() throws SQLException {
284        super.passivate();
285        setClosedInternal(true);
286        if (getDelegateInternal() instanceof PoolingConnection) {
287            ((PoolingConnection) getDelegateInternal()).connectionReturnedToPool();
288        }
289    }
290
291    /**
292     * Actually close my underlying {@link Connection}.
293     */
294    @Override
295    public void reallyClose() throws SQLException {
296        if (jmxObjectName != null) {
297            jmxObjectName.unregisterMBean();
298        }
299
300        if (validationPreparedStatement != null) {
301            Utils.closeQuietly(validationPreparedStatement);
302        }
303
304        super.closeInternal();
305    }
306
307    /**
308     * Validates the connection, using the following algorithm:
309     * <ol>
310     * <li>If {@code fastFailValidation} (constructor argument) is {@code true} and this connection has previously
311     * thrown a fatal disconnection exception, a {@code SQLException} is thrown.</li>
312     * <li>If {@code sql} is null, the driver's #{@link Connection#isValid(int) isValid(timeout)} is called. If it
313     * returns {@code false}, {@code SQLException} is thrown; otherwise, this method returns successfully.</li>
314     * <li>If {@code sql} is not null, it is executed as a query and if the resulting {@code ResultSet} contains at
315     * least one row, this method returns successfully. If not, {@code SQLException} is thrown.</li>
316     * </ol>
317     *
318     * @param sql
319     *            The validation SQL query.
320     * @param timeoutSeconds
321     *            The validation timeout in seconds.
322     * @throws SQLException
323     *             Thrown when validation fails or an SQLException occurs during validation
324     */
325    public void validate(final String sql, int timeoutSeconds) throws SQLException {
326        if (fastFailValidation && fatalSqlExceptionThrown) {
327            throw new SQLException(Utils.getMessage("poolableConnection.validate.fastFail"));
328        }
329
330        if (sql == null || sql.isEmpty()) {
331            if (timeoutSeconds < 0) {
332                timeoutSeconds = 0;
333            }
334            if (!isValid(timeoutSeconds)) {
335                throw new SQLException("isValid() returned false");
336            }
337            return;
338        }
339
340        if (!sql.equals(lastValidationSql)) {
341            lastValidationSql = sql;
342            // Has to be the innermost delegate else the prepared statement will
343            // be closed when the pooled connection is passivated.
344            validationPreparedStatement = getInnermostDelegateInternal().prepareStatement(sql);
345        }
346
347        if (timeoutSeconds > 0) {
348            validationPreparedStatement.setQueryTimeout(timeoutSeconds);
349        }
350
351        try (ResultSet rs = validationPreparedStatement.executeQuery()) {
352            if (!rs.next()) {
353                throw new SQLException("validationQuery didn't return a row");
354            }
355        } catch (final SQLException sqle) {
356            throw sqle;
357        }
358    }
359}