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 * @since 2.0
039 */
040public class PoolableConnection extends DelegatingConnection<Connection> implements PoolableConnectionMXBean {
041
042    private static MBeanServer MBEAN_SERVER;
043
044    static {
045        try {
046            MBEAN_SERVER = ManagementFactory.getPlatformMBeanServer();
047        } catch (NoClassDefFoundError | Exception ex) {
048            // ignore - JMX not available
049        }
050    }
051
052    /** The pool to which I should return. */
053    private final ObjectPool<PoolableConnection> pool;
054
055    private final ObjectNameWrapper jmxObjectName;
056
057    // Use a prepared statement for validation, retaining the last used SQL to
058    // check if the validation query has changed.
059    private PreparedStatement validationPreparedStatement;
060    private String lastValidationSql;
061
062    /**
063     * Indicate that unrecoverable SQLException was thrown when using this connection. Such a connection should be
064     * considered broken and not pass validation in the future.
065     */
066    private boolean fatalSqlExceptionThrown = false;
067
068    /**
069     * SQL_STATE codes considered to signal fatal conditions. Overrides the defaults in
070     * {@link Utils#DISCONNECTION_SQL_CODES} (plus anything starting with {@link Utils#DISCONNECTION_SQL_CODE_PREFIX}).
071     */
072    private final Collection<String> disconnectionSqlCodes;
073
074    /** Whether or not to fast fail validation after fatal connection errors */
075    private final boolean fastFailValidation;
076
077    /**
078     *
079     * @param conn
080     *            my underlying connection
081     * @param pool
082     *            the pool to which I should return when closed
083     * @param jmxObjectName
084     *            JMX name
085     * @param disconnectSqlCodes
086     *            SQL_STATE codes considered fatal disconnection errors
087     * @param fastFailValidation
088     *            true means fatal disconnection errors cause subsequent validations to fail immediately (no attempt to
089     *            run query or isValid)
090     */
091    public PoolableConnection(final Connection conn, final ObjectPool<PoolableConnection> pool,
092            final ObjectName jmxObjectName, final Collection<String> disconnectSqlCodes,
093            final boolean fastFailValidation) {
094        super(conn);
095        this.pool = pool;
096        this.jmxObjectName = ObjectNameWrapper.wrap(jmxObjectName);
097        this.disconnectionSqlCodes = disconnectSqlCodes;
098        this.fastFailValidation = fastFailValidation;
099
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, null, false);
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 #getDisconnectSqlCodes() disconnectSQLCodes} 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}