All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.apache.avalon.excalibur.datasource.AbstractJdbcConnection Maven / Gradle / Ivy

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

package org.apache.avalon.excalibur.datasource;

import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.sql.Array;
import java.sql.CallableStatement;
import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.ParameterMetaData;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import org.apache.avalon.excalibur.pool.Pool;
import org.apache.avalon.excalibur.pool.Poolable;
import org.apache.avalon.framework.activity.Disposable;
import org.apache.avalon.framework.logger.AbstractLogEnabled;
import org.apache.avalon.framework.logger.Logger;

/**
 * The Connection object used in conjunction with the JdbcDataSource
 * object.
 *
 * @author Avalon Development Team
 * @version CVS $Revision: 1.4 $ $Date: 2004/02/28 11:47:14 $
 * @since 4.1
 */
public class AbstractJdbcConnection
        extends AbstractLogEnabled
        implements PoolSettable, Disposable, ProxiedJdbcConnection
{
    protected Connection m_connection;

    private Object m_proxy;
    protected Pool m_pool;
    
    /** Flag to keep track of whether or not an error has been thrown since the last
     *   time the DB was pinged. */
    protected boolean m_encounteredError;

    /** The maximum time since a connection was last used before it will be pinged. */
    protected int m_testAge;
    protected PreparedStatement m_testStatement;
    protected SQLException m_testException;
    protected long m_lastUsed = System.currentTimeMillis();
    private static final Map m_methods;

    static
    {
        m_methods = new HashMap();
        Method[] methods = AbstractJdbcConnection.class.getDeclaredMethods();

        for( int i = 0; i < methods.length; i++ )
        {
            m_methods.put( methods[i].getName(), methods[i] );
        }
    }

    /**
     * Contains Statements created on the original jdbc connection
     * between a {@link JdbcDataSource#getConnection} and {@link
     * Connection#close}. The statements are registered using
     * {@link #registerAllocatedStatement} and deallocated in
     * {@link #close}. LinkedList was chosen because access
     * to elements is sequential through Iterator and the number
     * of elements is not known in advance. Synchronization is
     * done on the Link instance itself.
     */
    final private List m_allocatedStatements = new LinkedList();

    /**
     * @deprecated Use the version with keepAlive specified
     */
    public AbstractJdbcConnection( final Connection connection, final boolean oradb )
    {
        this( connection, ( oradb ) ? "select 1 from dual" : "select 1" );
    }

    /**
     * @param connection a driver specific JDBC connection to be wrapped.
     * @param keepAlive a query which will be used to check the statis of the connection after it
     *                  has been idle.  A null value will cause the keep alive feature to
     *                  be disabled.
     */
    public AbstractJdbcConnection( final Connection connection, final String keepAlive )
    {
        this( connection, keepAlive, 5000 );
    }

    /**
     * @param connection a driver specific JDBC connection to be wrapped.
     * @param keepAlive a query which will be used to check the status of the connection after it
     *                  has been idle.  A null value will cause the keep alive feature to
     *                  be disabled.
     * @param keepAliveAge the maximum age in milliseconds since a connection was last
     *                     used before it must be pinged using the keepAlive query.  Ignored
     *                     if keepAlive is null.
     */
    public AbstractJdbcConnection( final Connection connection,
                                   final String keepAlive,
                                   final int keepAliveAge )
    {
        m_connection = connection;

        // subclasses can override initialize()
        this.initialize();

        m_testAge = keepAliveAge;
        if( null == keepAlive || "".equals( keepAlive.trim() ) )
        {
            m_testStatement = null;
            m_testException = null;
        }
        else
        {
            try
            {
                // test statement is allocated directly from the
                // underlying connection, it is special and should not
                // be closed during recycling session
                m_testStatement = m_connection.prepareStatement( keepAlive );
            }
            catch( final SQLException se )
            {
                m_testStatement = null;
                m_testException = se;
            }
        }
    }

    public void initialize()
    {
    }

    public void enableLogging( final Logger log )
    {
        super.enableLogging( log );

        if( m_testStatement == null && m_testException != null )
        {
            if( getLogger().isWarnEnabled() )
            {
                getLogger().warn( "Could not prepare test statement, connection recycled on basis of time.", m_testException );
            }
            m_testException = null;
        }
    }

    public void setPool( Pool pool )
    {
        m_pool = pool;
    }

    public void recycle()
    {
        m_testException = null;
        try
        {
            clearAllocatedStatements();
            m_connection.clearWarnings();
        }
        catch( SQLException se )
        {
            // ignore
        }
    }

    public void setProxiedConnection( Object proxy )
    {
        m_proxy = proxy;
    }

    public Connection getConnection()
    {
        return m_connection;
    }

    public boolean isClosed()
            throws SQLException
    {
        if( m_connection.isClosed() )
        {
            return true;
        }

        if ( m_testStatement == null )
        {
            // No test statement was configured so it is not possible to ping the DB to
            //  revalidate the connection.
            if ( m_encounteredError )
            {
                // There is no way to guarantee that this connection is still good.
                //  Cause the connection to be invalidated.
                return true;
            }
        }
        else
        {
            // A ping statement was configured.
            long age = System.currentTimeMillis() - m_lastUsed;
            boolean ping = false;
            if ( age > m_testAge )
            {
                if( getLogger().isDebugEnabled() )
                {
                    getLogger().debug( "Pinging database after " + age + "ms of inactivity." );
                }
                ping = true;
            }
            else if ( m_encounteredError )
            {
                // If an error was encountered since the last time the DB has been pinged we want
                //  to force a ping.
                // If a client is attempting to query a DB once every couple seconds then the
                //  connection will never be unused for long enough to cause a ping on its own.
                //  This means that if any reocurring error, like the socket being closed, occurrs
                //  the pool would normally never invalidate the connection.  Ideally the underlying
                //  Connection's isClosed() method should return false in such a case, but it does
                //  not always do so. (Oracle for example)
                //
                // The encounteredError flag is only set if an error is thrown from a Connection
                //  method.  It will not be set if an exception is thrown by a Statement or other
                //  object obtained from the Connection.  We are mainly worried about low level
                //  socket problems here however so this should not be a problem.  A failed query
                //  caused by bad SQL does not mean the connection might fail for future calls.
                getLogger().debug( "Pinging database after a previously thrown error." );
                ping = true;
            }
            
            // Always reset the flag here in case the connection was old and there was an error.
            m_encounteredError = false;
            
            if( ping )
            {
                try
                {
                    ResultSet rs = m_testStatement.executeQuery();
                    rs.close();
                }
                catch( final SQLException se )
                {
                    getLogger().debug( "Ping of connection failed.", se );
                    this.dispose();
                    return true;
                }
            }
        }

        return false;
    }

    public void close()
            throws SQLException
    {
        // IMPORTANT - never simply call dispose within this method.  The
        //  pool will have no way of knowing that the connection was disposed
        //  and blocking pools will eventually run out of resources thinking
        //  that all of the connections are in use.
        try
        {
            // Always mark the time the connection was placed back in the pool
            //  as its last used time.
            m_lastUsed = System.currentTimeMillis();

            try
            {
                clearAllocatedStatements();
                m_connection.clearWarnings();
            }
            catch( SQLException se )
            {
                // This can be ignored here.
            }
        }
        finally
        {
            // Always put the connection back into the pool
            m_pool.put( (Poolable)m_proxy );
        }
    }

    /**
     * Closes statements that were registered and removes all
     * statements from the list of allocated ones.  If any statement
     * fails to properly close, the rest of the statements is ignored.
     * But the registration list if cleared in any case.
     * 

* Holds m_allocatedStatements locked the whole time. This should * not be a problem because connections are inherently single * threaded objects and any attempt to use them from a different * thread while it is being closed is a violation of the contract. * * @throws SQLException of the first Statement.close() */ protected void clearAllocatedStatements() throws SQLException { synchronized( m_allocatedStatements ) { try { final Iterator iterator = m_allocatedStatements.iterator(); while( iterator.hasNext() ) { Statement stmt = (Statement)iterator.next(); stmt.close(); } } finally { m_allocatedStatements.clear(); } } } /** * Adds the statement to the list of this connection. Used by * subclasses to ensure release of statements when connection is * logically terminated and returned to the pool. */ protected void registerAllocatedStatement( Statement stmt ) { synchronized( m_allocatedStatements ) { m_allocatedStatements.add( stmt ); } } public void dispose() { try { m_connection.close(); } catch( final SQLException se ) { if( getLogger().isDebugEnabled() ) { getLogger().debug( "Could not close connection", se ); } } } public boolean equals( Object obj ) { if( Proxy.isProxyClass( obj.getClass() ) ) { final InvocationHandler handler = Proxy.getInvocationHandler(obj ); if( handler instanceof ProxiedJdbcConnection ) { return m_connection.equals( ( (ProxiedJdbcConnection)handler ).getConnection() ); } } return false; } public Object invoke( Object proxy, Method method, Object[] args ) throws Throwable { // NOTE - getLogger() will return null in here before the enableLogging method // has been called. Object retVal = null; Method executeMethod = (Method)m_methods.get( method.getName() ); try { if( null == executeMethod ) { retVal = method.invoke( m_connection, args ); } else { retVal = executeMethod.invoke( this, args ); } } catch( InvocationTargetException e ) { // Remember that an error of some sort was thrown so we can make sure to retest // the connection before it is once again used by a client. m_encounteredError = true; throw e.getTargetException(); } // See if the return value is an object that needs to be proxied. if ( retVal != null ) { // The order of the classes below is important to make sure that all implemented // interfaces are taken into account. if ( retVal instanceof CallableStatement ) { retVal = Proxy.newProxyInstance( CallableStatement.class.getClassLoader(), new Class[] { CallableStatement.class }, new ProxiedObject( retVal ) ); } else if ( retVal instanceof PreparedStatement ) { retVal = Proxy.newProxyInstance( PreparedStatement.class.getClassLoader(), new Class[] { PreparedStatement.class }, new ProxiedObject( retVal ) ); } else if ( retVal instanceof Statement ) { retVal = Proxy.newProxyInstance( Statement.class.getClassLoader(), new Class[] { Statement.class }, new ProxiedObject( retVal ) ); } else if ( retVal instanceof DatabaseMetaData ) { retVal = Proxy.newProxyInstance( DatabaseMetaData.class.getClassLoader(), new Class[] { DatabaseMetaData.class }, new ProxiedObject( retVal ) ); } else { // Leave the retVal alone and unproxied. } } return retVal; } private class ProxiedObject implements InvocationHandler { Object m_originalObject; private ProxiedObject( Object originalObject ) { m_originalObject = originalObject; } public Object invoke( Object proxy, Method method, Object[] args ) throws Throwable { // We want to pass all method calls through to the original object but // we want to catch and make a not of any exceptions thrown along the // way. This is critical to make sure that connections that have gone // bad are not reused without being retested. Object retVal; try { retVal = method.invoke( m_originalObject, args ); } catch( InvocationTargetException e ) { // Remember that an error of some sort was thrown so we can make sure to retest // the connection before it is once again used by a client. m_encounteredError = true; throw e.getTargetException(); } // See if the return value is an object that needs to be proxied. if ( retVal != null ) { // The order of the classes below is important to make sure that all implemented // interfaces are taken into account. if ( retVal instanceof Array ) { retVal = Proxy.newProxyInstance( Array.class.getClassLoader(), new Class[] { Array.class }, new ProxiedObject( retVal ) ); } else if ( ( retVal instanceof ResultSet ) && ( retVal instanceof ResultSetMetaData ) ) { // The HSQLDB driver needs this to make things work correctly. retVal = Proxy.newProxyInstance( ResultSet.class.getClassLoader(), new Class[] { ResultSet.class, ResultSetMetaData.class }, new ProxiedObject( retVal ) ); } else if ( retVal instanceof ResultSet ) { retVal = Proxy.newProxyInstance( ResultSet.class.getClassLoader(), new Class[] { ResultSet.class }, new ProxiedObject( retVal ) ); } else if ( retVal instanceof ResultSetMetaData ) { retVal = Proxy.newProxyInstance( ResultSetMetaData.class.getClassLoader(), new Class[] { ResultSetMetaData.class }, new ProxiedObject( retVal ) ); } else if ( retVal instanceof ParameterMetaData ) { retVal = Proxy.newProxyInstance( ParameterMetaData.class.getClassLoader(), new Class[] { ParameterMetaData.class }, new ProxiedObject( retVal ) ); } else { // Leave the retVal alone and unproxied. } } return retVal; } } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy