AbstractCreateStatementInterceptor.java

/*
 * 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.tomcat.jdbc.pool.interceptor;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.Statement;
import java.util.Arrays;

import org.apache.juli.logging.Log;
import org.apache.juli.logging.LogFactory;
import org.apache.tomcat.jdbc.pool.ConnectionPool;
import org.apache.tomcat.jdbc.pool.JdbcInterceptor;
import org.apache.tomcat.jdbc.pool.PooledConnection;
import org.apache.tomcat.jdbc.pool.util.MessageFactory;

/**
 * Abstraction interceptor. This component intercepts all calls to create some type of SQL statement. By extending this
 * class, one can intercept queries and update statements by overriding the
 * {@link #createStatement(Object, Method, Object[], Object, long)} method.
 */
public abstract class AbstractCreateStatementInterceptor extends JdbcInterceptor {
    private static final Log log = LogFactory.getLog(AbstractCreateStatementInterceptor.class);

    /**
     * {@link java.sql.Connection#createStatement()}
     */
    protected static final String CREATE_STATEMENT = "createStatement";
    /**
     * Index for {@link #CREATE_STATEMENT} in {@link #STATEMENT_TYPES}
     */
    protected static final int CREATE_STATEMENT_IDX = 0;
    /**
     * {@link java.sql.Connection#prepareStatement(String)}
     */
    protected static final String PREPARE_STATEMENT = "prepareStatement";
    /**
     * Index for {@link #PREPARE_STATEMENT} in {@link #STATEMENT_TYPES}
     */
    protected static final int PREPARE_STATEMENT_IDX = 1;
    /**
     * {@link java.sql.Connection#prepareCall(String)}
     */
    protected static final String PREPARE_CALL = "prepareCall";
    /**
     * Index for {@link #PREPARE_CALL} in {@link #STATEMENT_TYPES}
     */
    protected static final int PREPARE_CALL_IDX = 2;

    /**
     * {@link Statement#getResultSet()}
     */
    protected static final String GET_RESULTSET = "getResultSet";

    /**
     * {@link Statement#getGeneratedKeys()}
     */
    protected static final String GET_GENERATED_KEYS = "getGeneratedKeys";

    /**
     * {@link ResultSet#getStatement()}
     */
    protected static final String GET_STATEMENT = "getStatement";

    /**
     * Index for {@link #GET_STATEMENT} in {@link #STATEMENT_TYPES}
     */
    protected static final int RESULTSET_IDX = 3;

    /**
     * Array of statement type method names
     */
    protected static final String[] STATEMENT_TYPES = { CREATE_STATEMENT, PREPARE_STATEMENT, PREPARE_CALL };
    /**
     * Count of statement types in {@link #STATEMENT_TYPES}
     */
    protected static final int STATEMENT_TYPE_COUNT = STATEMENT_TYPES.length;

    /**
     * {@link java.sql.Statement#execute}
     */
    protected static final String EXECUTE = "execute";
    /**
     * {@link java.sql.Statement#executeQuery}
     */
    protected static final String EXECUTE_QUERY = "executeQuery";
    /**
     * {@link java.sql.Statement#executeUpdate}
     */
    protected static final String EXECUTE_UPDATE = "executeUpdate";
    /**
     * {@link java.sql.Statement#executeBatch()}
     */
    protected static final String EXECUTE_BATCH = "executeBatch";
    /**
     * {@link java.sql.Statement#executeLargeUpdate} and its overloads, and
     * {@link java.sql.PreparedStatement#executeLargeUpdate()}
     */
    protected static final String EXECUTE_LARGE_UPDATE = "executeLargeUpdate";
    /**
     * {@link java.sql.Statement#executeLargeBatch()}
     */
    protected static final String EXECUTE_LARGE_BATCH = "executeLargeBatch";

    /**
     * Array of execute type method names
     */
    protected static final String[] EXECUTE_TYPES = { EXECUTE, EXECUTE_QUERY, EXECUTE_UPDATE, EXECUTE_BATCH,
            EXECUTE_LARGE_UPDATE, EXECUTE_LARGE_BATCH };

    /**
     * the constructors that are used to create statement proxies. The cache is
     * shared by all subclasses loaded through the same class loader, so every
     * use of an index must pass an interface that the cached proxy implements,
     * in practice always the same one; {@link #getConstructor(int, Class)}
     * enforces this. The canonical entries are populated by this class's
     * initializer, which the JVM runs once and whose writes it publishes to
     * every thread that uses the class, so reading a slot through
     * {@link #getConstructor(int, Class)} needs no locking; writing an element
     * directly from a subclass bypasses that guarantee and is the subclass's
     * own responsibility.
     */
    protected static final Constructor<?>[] constructors = new Constructor[STATEMENT_TYPE_COUNT + 1];

    static {
        // Class initialization (JLS 12.4.2) runs once per class loader and its
        // writes are visible to every thread once it completes, so populating
        // the canonical slots here makes them safely published without any
        // monitor on the hot statement path.
        populateConstructor(CREATE_STATEMENT_IDX, Statement.class);
        populateConstructor(PREPARE_STATEMENT_IDX, PreparedStatement.class);
        populateConstructor(PREPARE_CALL_IDX, CallableStatement.class);
        populateConstructor(RESULTSET_IDX, ResultSet.class);
    }

    /**
     * Computes the proxy constructor for one index of {@link #constructors}
     * and stores it in the cache. On failure the slot is left empty and only
     * logged; {@link #getConstructor(int, Class)} then computes the entry on
     * demand, so a failure here degrades to the pre-existing behavior rather
     * than failing class initialization.
     *
     * @param idx   - the index of the constructor
     * @param clazz - the interface that the proxy will implement
     */
    @SuppressWarnings("deprecation")
    private static void populateConstructor(int idx, Class<?> clazz) {
        try {
            Class<?> proxyClass = Proxy.getProxyClass(AbstractCreateStatementInterceptor.class.getClassLoader(),
                    new Class[] { clazz });
            constructors[idx] = proxyClass.getConstructor(new Class[] { InvocationHandler.class });
        } catch (ReflectiveOperationException | IllegalArgumentException | LinkageError e) {
            // An exception escaping the class initializer would surface as an
            // ExceptionInInitializerError at every use of every subclass.
            log.warn(MessageFactory.get("abstractCreateStatementInterceptor.unableToCreateConstructorCacheEntry",
                    idx, clazz.getName()), e);
        }
    }

    /**
     * Default constructor
     */
    public AbstractCreateStatementInterceptor() {
        super();
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        if (compare(CLOSE_VAL, method)) {
            closeInvoked();
            return super.invoke(proxy, method, args);
        } else {
            boolean process = false;
            process = isStatement(method, process);
            if (process) {
                long start = System.currentTimeMillis();
                Object statement = super.invoke(proxy, method, args);
                long delta = System.currentTimeMillis() - start;
                return createStatement(proxy, method, args, statement, delta);
            } else {
                return super.invoke(proxy, method, args);
            }
        }
    }

    /**
     * Creates a constructor for a proxy class, if one doesn't already exist.
     * The canonical entries are published by this class's initializer, so on
     * the normal path the method only reads a safely published slot and is
     * safe for concurrent use; {@code newInstance} on the returned
     * constructor runs as it always has, outside this method.
     *
     * @param idx   - the index of the constructor
     * @param clazz - the interface that the proxy will implement
     *
     * @return - returns a constructor used to create new instances
     *
     * @throws NoSuchMethodException Constructor not found
     * @throws IllegalStateException The index is already cached for a proxy class that does not implement
     *                                   {@code clazz}. Every use of an index in this shared cache must pass
     *                                   an interface that the cached proxy implements.
     */
    /*
     * The cache, this method and the class itself are all reachable from subclasses, in-tree or third party. The
     * cache is shared by every subclass loaded through the same class loader, so each index must always be
     * paired with an interface that the cached proxy implements, in practice always the same one; a cache hit
     * whose cached proxy does not implement the requested interface is rejected below rather than silently
     * returning a proxy that does not implement what the caller asked for. Given the comments in the jdbc-pool
     * code regarding caching for performance, continue to use Proxy.getProxyClass(). This will need to be
     * revisited if that method is marked for removal.
     */
    @SuppressWarnings("deprecation")
    protected Constructor<?> getConstructor(int idx, Class<?> clazz) throws NoSuchMethodException {
        if (constructors[idx] == null) {
            // Only reachable if the class initializer failed to populate this
            // slot, or a subclass nulled the element directly; compute on
            // demand, as before the cache was populated eagerly.
            Class<?> proxyClass = Proxy.getProxyClass(AbstractCreateStatementInterceptor.class.getClassLoader(),
                    new Class[] { clazz });
            constructors[idx] = proxyClass.getConstructor(new Class[] { InvocationHandler.class });
        } else if (!clazz.isAssignableFrom(constructors[idx].getDeclaringClass())) {
            throw new IllegalStateException(MessageFactory.get(
                    "abstractCreateStatementInterceptor.constructorCacheIndexPopulated",
                    idx, Arrays.toString(constructors[idx].getDeclaringClass().getInterfaces()), clazz.getName()));
        }
        return constructors[idx];
    }

    /**
     * This method will be invoked after a successful statement creation. This method can choose to return a wrapper
     * around the statement or return the statement itself. If this method returns a wrapper then it should return a
     * wrapper object that implements one of the following interfaces. {@link java.sql.Statement},
     * {@link java.sql.PreparedStatement} or {@link java.sql.CallableStatement}
     *
     * @param proxy     the actual proxy object
     * @param method    the method that was called. It will be one of the methods defined in {@link #STATEMENT_TYPES}
     * @param args      the arguments to the method
     * @param statement the statement that the underlying connection created
     * @param time      Elapsed time
     *
     * @return a {@link java.sql.Statement} object
     */
    public abstract Object createStatement(Object proxy, Method method, Object[] args, Object statement, long time);

    /**
     * Method invoked when the operation {@link java.sql.Connection#close()} is invoked.
     */
    public abstract void closeInvoked();

    /**
     * Returns true if the method that is being invoked matches one of the statement types.
     *
     * @param method  the method being invoked on the proxy
     * @param process boolean result used for recursion
     *
     * @return returns true if the method name matched
     */
    protected boolean isStatement(Method method, boolean process) {
        return process(STATEMENT_TYPES, method, process);
    }

    /**
     * Returns true if the method that is being invoked matches one of the execute types.
     *
     * @param method  the method being invoked on the proxy
     * @param process boolean result used for recursion
     *
     * @return returns true if the method name matched
     */
    protected boolean isExecute(Method method, boolean process) {
        return process(EXECUTE_TYPES, method, process);
    }

    /**
     * Returns true if the method that is being invoked matches one of the method names passed in
     *
     * @param names   list of method names that we want to intercept
     * @param method  the method being invoked on the proxy
     * @param process boolean result used for recursion
     *
     * @return returns true if the method name matched
     */
    protected boolean process(String[] names, Method method, boolean process) {
        final String name = method.getName();
        for (int i = 0; (!process) && i < names.length; i++) {
            process = compare(names[i], name);
        }
        return process;
    }

    /**
     * no-op for this interceptor. no state is stored.
     */
    @Override
    public void reset(ConnectionPool parent, PooledConnection con) {
        // NOOP
    }
}