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