/*
 * Derby - org.apache.derbyTesting.functionTests.tests.jdbc4.VerifySignatures
 *
   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.derbyTesting.functionTests.tests.jdbc4;

import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.sql.Blob;
import java.sql.CallableStatement;
import java.sql.Clob;
import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.DriverManager;
import java.sql.ParameterMetaData;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.sql.Savepoint;
import java.sql.Statement;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import javax.sql.ConnectionPoolDataSource;
import javax.sql.DataSource;
import javax.sql.PooledConnection;
import javax.sql.XAConnection;
import javax.sql.XADataSource;
import junit.framework.Test;
import org.apache.derbyTesting.functionTests.util.TestUtil;
import org.apache.derbyTesting.junit.BaseTestCase;
import org.apache.derbyTesting.junit.J2EEDataSource;
import org.apache.derbyTesting.junit.JDBCDataSource;
import org.apache.derbyTesting.junit.TestConfiguration;

/**
 * JUnit test which checks that all methods specified by the
 * interfaces in JDBC 4.0 are implemented. The test requires JVM 1.6
 * to run.
 * Even though this class uses JDBC it extends BaseTestCase as
 * it handles getting connections itself and thus does not
 * need the utility methods or connecion handlng provided by
 * BaseJDBCTestCase.
 */
public class VerifySignatures extends BaseTestCase {

    /**
     * All the java.sql and javax.sql interfaces specified by JDBC 4.0.
     */
    private final static Class[] JDBC_INTERFACES = {
        java.sql.Array.class,
        java.sql.Blob.class,
        java.sql.CallableStatement.class,
        java.sql.Clob.class,
        java.sql.Connection.class,
        java.sql.DatabaseMetaData.class,
        java.sql.Driver.class,
        java.sql.NClob.class,
        java.sql.ParameterMetaData.class,
        java.sql.PreparedStatement.class,
        java.sql.Ref.class,
        java.sql.ResultSet.class,
        java.sql.ResultSetMetaData.class,
        java.sql.RowId.class,
        java.sql.Savepoint.class,
        java.sql.SQLData.class,
        java.sql.SQLInput.class,
        java.sql.SQLOutput.class,
        java.sql.SQLXML.class,
        java.sql.Statement.class,
        java.sql.Struct.class,
        java.sql.Wrapper.class,
        javax.sql.CommonDataSource.class,
        javax.sql.ConnectionEventListener.class,
        javax.sql.ConnectionPoolDataSource.class,
        javax.sql.DataSource.class,
        javax.sql.PooledConnection.class,
        javax.sql.RowSet.class,
        javax.sql.RowSetInternal.class,
        javax.sql.RowSetListener.class,
        javax.sql.RowSetMetaData.class,
        javax.sql.RowSetReader.class,
        javax.sql.RowSetWriter.class,
        javax.sql.StatementEventListener.class,
        javax.sql.XAConnection.class,
        javax.sql.XADataSource.class,
    };

    /**
     * Creates a new instance.
     */
    public VerifySignatures(String name) {
        super(name);
    }

    /**
     * Build a suite of tests to be run.
     *
     * @return a test suite
     */
    public static Test suite()  {
        return TestConfiguration.defaultSuite(VerifySignatures.class);
    }
    
    public void testAllJDBCObjects()
      throws NoSuchMethodException, SQLException
    {
        // set of all implementation/interface pairs found
        Set<ClassInfo> classes = new HashSet<ClassInfo>();

        collectClassesFromDataSource(classes);
        collectClassesFromConnectionPoolDataSource(classes);
        collectClassesFromXADataSource(classes);
        addClass(classes,
                 DriverManager.getDriver(TestConfiguration.getCurrent().getJDBCUrl()).getClass(),
                 java.sql.Driver.class);

        // all interfaces for which tests have been generated
        Set<Class> interfaces = new HashSet<Class>();

        for (ClassInfo pair : classes) {
            // some methods are defined in many interfaces, so collect
            // them in a set first to avoid duplicates
            Set<Method> methods = new HashSet<Method>();
            for (Class iface : getAllInterfaces(pair.jdbcInterface)) {
                interfaces.add(iface);
                for (Method method : iface.getMethods()) {
                    methods.add(method);
                }
            }
            for (Method method : methods) {
                checkImplementationMethod(pair.derbyImplementation,
                        method);
            }
        }
        
        // Now ensure all interfaces were covered.
        // get the declared set of interfaces Derby is
        // expected to implement.
        Set<Class> jdbcInterfaces = getInterfacesToCheck();
        
        // remove from it all that were tested.
        jdbcInterfaces.removeAll(interfaces);
        
        // and the resultin set should be empty if we tested all!
        assertTrue("Unchecked interfaces: " + jdbcInterfaces,
                   jdbcInterfaces.isEmpty());
    }

    /**
     * Obtain a connection from a <code>DataSource</code> object and
     * perform JDBC operations on it. Collect the classes of all JDBC
     * objects that are found.
     *
     * @param classes set into which classes are collected
     * @exception SQLException if a database error occurs
     */
    private static void collectClassesFromDataSource(Set<ClassInfo> classes)
        throws SQLException
    {
        DataSource ds = JDBCDataSource.getDataSource();
        JDBCDataSource.setBeanProperty(ds, "connectionAttributes",
                                       "create=true");
        addClass(classes, ds.getClass(), javax.sql.DataSource.class);
        collectClassesFromConnection(ds.getConnection
                                     (TestConfiguration.getCurrent().getUserName(),
                                             TestConfiguration.getCurrent().getUserPassword()),
                                     classes);
    }

    /**
     * Obtain a connection from a <code>ConnectionPoolDataSource</code>
     * object and perform JDBC operations on it. Collect the classes
     * of all JDBC objects that are found.
     *
     * @param classes set into which classes are collected
     * @exception SQLException if a database error occurs
     */
    private static void
        collectClassesFromConnectionPoolDataSource(Set<ClassInfo> classes)
        throws SQLException
    {
        ConnectionPoolDataSource cpds = J2EEDataSource.getConnectionPoolDataSource();
        addClass(classes,
                 cpds.getClass(), javax.sql.ConnectionPoolDataSource.class);

        PooledConnection pc =
            cpds.getPooledConnection(TestConfiguration.getCurrent().getUserName(),
                    TestConfiguration.getCurrent().getUserPassword());
        addClass(classes, pc.getClass(), javax.sql.PooledConnection.class);

        collectClassesFromConnection(pc.getConnection(), classes);

        pc.close();
    }

    /**
     * Obtain a connection from an <code>XADataSource</code> object
     * and perform JDBC operations on it. Collect the classes of all
     * JDBC objects that are found.
     *
     * @param classes set into which classes are collected
     * @exception SQLException if a database error occurs
     */
    private static void collectClassesFromXADataSource(Set<ClassInfo> classes)
        throws SQLException
    {
        XADataSource xads = J2EEDataSource.getXADataSource();
        addClass(classes, xads.getClass(), javax.sql.XADataSource.class);

        XAConnection xaconn = xads.getXAConnection(TestConfiguration.getCurrent().getUserName(),
                TestConfiguration.getCurrent().getUserPassword());
        addClass(classes, xaconn.getClass(), javax.sql.XAConnection.class);

        collectClassesFromConnection(xaconn.getConnection(), classes);
    }

    /**
     * Perform JDBC operations on a <code>Connection</code>. Collect
     * the classes of all JDBC objects that are found.
     *
     * @param conn connection to a database
     * @param classes set into which classes are collected
     * @exception SQLException if a database error occurs
     */
    private static void collectClassesFromConnection(Connection conn,
                                                     Set<ClassInfo> classes)
        throws SQLException
    {
        conn.setAutoCommit(false);
        addClass(classes, conn.getClass(), java.sql.Connection.class);

        Savepoint sp = conn.setSavepoint();
        addClass(classes, sp.getClass(), java.sql.Savepoint.class);
        conn.releaseSavepoint(sp);

        DatabaseMetaData dmd = conn.getMetaData();
        addClass(classes, dmd.getClass(), java.sql.DatabaseMetaData.class);

        collectClassesFromStatement(conn, classes);
        collectClassesFromPreparedStatement(conn, classes);
        collectClassesFromCallableStatement(conn, classes);
        conn.rollback();
        conn.close();
    }

    /**
     * Perform JDBC operations on a <code>Statement</code>. Collect
     * the classes of all JDBC objects that are found.
     *
     * @param conn connection to a database
     * @param classes set into which classes are collected
     * @exception SQLException if a database error occurs
     */
    private static void
        collectClassesFromStatement(Connection conn, Set<ClassInfo> classes)
        throws SQLException
    {
        Statement stmt = conn.createStatement();
        addClass(classes, stmt.getClass(), java.sql.Statement.class);

        stmt.execute("CREATE TABLE t (id INT PRIMARY KEY, " +
                     "b BLOB(10), c CLOB(10))");
        stmt.execute("INSERT INTO t (id, b, c) VALUES (1, "+
                     "CAST (" + TestUtil.stringToHexLiteral("101010001101") +
                     "AS BLOB(10)), CAST ('hello' AS CLOB(10)))");

        ResultSet rs = stmt.executeQuery("SELECT id, b, c FROM t");
        addClass(classes, rs.getClass(), java.sql.ResultSet.class);
        rs.next();
        Blob b = rs.getBlob(2);
        addClass(classes, b.getClass(), java.sql.Blob.class);
        Clob c = rs.getClob(3);
        addClass(classes, c.getClass(), java.sql.Clob.class);
        ResultSetMetaData rsmd = rs.getMetaData();
        addClass(classes, rsmd.getClass(), java.sql.ResultSetMetaData.class);
        rs.close();

        stmt.close();
        conn.rollback();
    }

    /**
     * Perform JDBC operations on a <code>PreparedStatement</code>.
     * Collect the classes of all JDBC objects that are found.
     *
     * @param conn connection to a database
     * @param classes set into which classes are collected
     * @exception SQLException if a database error occurs
     */
    private static void
        collectClassesFromPreparedStatement(Connection conn,
                                            Set<ClassInfo> classes)
        throws SQLException
    {
        PreparedStatement ps = conn.prepareStatement("VALUES(1)");
        addClass(classes, ps.getClass(), java.sql.PreparedStatement.class);
        ResultSet rs = ps.executeQuery();
        addClass(classes, rs.getClass(), java.sql.ResultSet.class);
        rs.close();

        ParameterMetaData pmd = ps.getParameterMetaData();
        addClass(classes, pmd.getClass(), java.sql.ParameterMetaData.class);

        ps.close();
    }

    /**
     * Perform JDBC operations on a <code>CallableStatement</code>.
     * Collect the classes of all JDBC objects that are found.
     *
     * @param conn connection to a database
     * @param classes set into which classes are collected
     * @exception SQLException if a database error occurs
     */
    private static void
        collectClassesFromCallableStatement(Connection conn,
                                            Set<ClassInfo> classes)
        throws SQLException
    {
        CallableStatement cs =
            conn.prepareCall("CALL SYSCS_UTIL.SYSCS_SET_RUNTIMESTATISTICS(0)");
        addClass(classes, cs.getClass(), java.sql.CallableStatement.class);

        ParameterMetaData pmd = cs.getParameterMetaData();
        addClass(classes, pmd.getClass(), java.sql.ParameterMetaData.class);

        cs.close();
    }

    /**
     * Adds a <code>ClassInfo</code> object to a set.
     *
     * @param classes set to which the class should be added
     * @param implementation Derby implementation class
     * @param iface JDBC interface supposed to be implemented
     */
    private static void addClass(Set<ClassInfo> classes,
                                 Class implementation, Class iface) {
        classes.add(new ClassInfo(implementation, iface));
    }

    /**
     * Get the set consisting of an interface and all its
     * super-interfaces.
     *
     * @param iface an interface
     * @return the set consisting of <code>iface</code> and all its
     * super-interfaces
     */
    private static Set<Class> getAllInterfaces(Class iface) {
        Set<Class> set = new HashSet<Class>();
        set.add(iface);
        for (Class superIface : iface.getInterfaces()) {
            set.add(superIface);
            set.addAll(getAllInterfaces(superIface));
        }
        return set;
    }
   
    /**
     * checks that a class implements a specific method.
     * @param derbyImplementation The Derby implementation class which is tested
     * @param ifaceMethod The method that should be implemented.
     */
    private static void checkImplementationMethod(
            Class<?> derbyImplementation, Method ifaceMethod)
        throws NoSuchMethodException
    {
        
            assertFalse("Implementation class is interface",
                        derbyImplementation.isInterface());

            Method impMethod =
                derbyImplementation.getMethod(ifaceMethod.getName(),
                                              ifaceMethod.getParameterTypes());
            StringBuilder sb = new StringBuilder();
            Class[] pts = ifaceMethod.getParameterTypes();
            sb.append("(");
            for (int i=0; i < pts.length; i++) {
                sb.append(pts[i].getName());
                if (i < pts.length -1) {
                    sb.append(", ");
                }
            }
            sb.append(")");
            assertEquals(
                    "Incorrect return type for " + ifaceMethod.getName() +
                    sb.toString(),
                    ifaceMethod.getReturnType(),
                    impMethod.getReturnType());

            int modifiers = impMethod.getModifiers();
            assertTrue("Non-public method", Modifier.isPublic(modifiers));
            assertFalse("Abstract method", Modifier.isAbstract(modifiers));
            assertFalse("Static method", Modifier.isStatic(modifiers));

            Class[] declaredExceptions = ifaceMethod.getExceptionTypes();
            for (Class exception : impMethod.getExceptionTypes()) {
                if (RuntimeException.class.isAssignableFrom(exception)) {
                    continue;
                }
                assertNotNull("Incompatible throws clause",
                              findCompatibleClass(exception,
                                                  declaredExceptions));
            }
        }

        /**
         * Search an array of classes for a class that is identical to
         * or a super-class of the specified exception class.
         *
         * @param exception an exception class
         * @param declared an array of exception classes declared to
         * be thrown by a method
         * @return a class that is compatible with the specified
         * exception class, or <code>null</code> if no compatible
         * class is found
         */
        private static Class findCompatibleClass(Class exception, Class[] declared)
        {
            for (Class<?> dec : declared) {
                if (dec.isAssignableFrom(exception)) {
                    return dec;
                }
            }
            return null;
        }

        /**
         * Returns the declared set of JDBC interfaces that
         * Derby implements.
         *
         */
        private static Set<Class> getInterfacesToCheck() {

            Set<Class> jdbcInterfaces = new HashSet<Class>(Arrays.asList(JDBC_INTERFACES));

            // Remove the interfaces that we know we haven't checked.

            // Interfaces that Derby doesn't implement:
            jdbcInterfaces.remove(java.sql.Array.class);
            jdbcInterfaces.remove(java.sql.NClob.class);
            jdbcInterfaces.remove(java.sql.Ref.class);
            jdbcInterfaces.remove(java.sql.SQLData.class);
            jdbcInterfaces.remove(java.sql.SQLInput.class);
            jdbcInterfaces.remove(java.sql.SQLOutput.class);
            jdbcInterfaces.remove(java.sql.SQLXML.class);
            jdbcInterfaces.remove(java.sql.Struct.class);
            jdbcInterfaces.remove(javax.sql.RowSet.class);
            jdbcInterfaces.remove(javax.sql.RowSetInternal.class);
            jdbcInterfaces.remove(javax.sql.RowSetListener.class);
            jdbcInterfaces.remove(javax.sql.RowSetMetaData.class);
            jdbcInterfaces.remove(javax.sql.RowSetReader.class);
            jdbcInterfaces.remove(javax.sql.RowSetWriter.class);
            jdbcInterfaces.remove(java.sql.RowId.class);

            // The event listener interfaces are implemented in
            // application code, not in Derby code.
            jdbcInterfaces.remove(javax.sql.ConnectionEventListener.class);
            jdbcInterfaces.remove(javax.sql.StatementEventListener.class);
            
            return jdbcInterfaces;
        }

    /**
     * Data structure holding a Derby implementation class and the
     * JDBC interface it is supposed to implement.
     */
    private static class ClassInfo {
        /** Derby implementation class. */
        Class derbyImplementation;
        /** JDBC interface which should be implemented. */
        Class jdbcInterface;

        /**
         * Creates a new <code>ClassInfo</code> instance.
         *
         * @param imp the Derby implementation class
         * @param iface the JDBC interface
         */
        ClassInfo(Class imp, Class iface) {
            derbyImplementation = imp;
            jdbcInterface = iface;
        }

        /**
         * Checks whether this object is equal to another object.
         *
         * @param x another object
         * @return <code>true</code> if the objects are equal,
         * <code>false</code> otherwise
         */
        public boolean equals(Object x) {
            if (x instanceof ClassInfo) {
                ClassInfo ci = (ClassInfo) x;
                return
                    derbyImplementation.equals(ci.derbyImplementation) &&
                    jdbcInterface.equals(ci.jdbcInterface);
            }
            return false;
        }

        /**
         * Calculate hash code.
         *
         * @return hash code
         */
        public int hashCode() {
            return derbyImplementation.hashCode() ^ jdbcInterface.hashCode();
        }
    }
}
