/*

Derby - Class org.apache.derbyDemo.vtis.core.QueryVTIHelper

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.derbyDemo.vtis.core;

import java.io.*;
import java.lang.reflect.*;
import java.sql.*;
import java.util.*;

/**
 * <p>
 * This is a set of helper methods executing a query against an external database. This
 * class maintains a cache of connections to external databases. Each connection
 * is identified by its connection URL. In addition to materializing external
 * data sets, this class provides a database procedure for closing an external connection.
 * </p>
 *
  */
public  abstract    class   QueryVTIHelper
{
    ///////////////////////////////////////////////////////////////////////////////////
    //
    // CONSTANTS
    //
    ///////////////////////////////////////////////////////////////////////////////////

    ///////////////////////////////////////////////////////////////////////////////////
    //
    // STATE
    //
    ///////////////////////////////////////////////////////////////////////////////////

    private static  Hashtable<String,Connection>    _openConnections = new Hashtable<String,Connection>();

    ///////////////////////////////////////////////////////////////////////////////////
    //
    // CONSTRUCTORS
    //
    ///////////////////////////////////////////////////////////////////////////////////

    ///////////////////////////////////////////////////////////////////////////////////
    //
    // DATABASE PROCEDURES
    //
    ///////////////////////////////////////////////////////////////////////////////////

    /**
     * <p>
     * Register this method as a database procedure, then use it to register
     * all of the QueryRow table functions in a class.
     * </p>
     */
    public  static  void  registerQueryRowVTIs( String className, String connectionURL )
        throws Exception
    {
        // find public static methods which return ResultSet
        Class           theClass = Class.forName( className );
        Method[]        methods = theClass.getMethods();
        int             count = methods.length;
        Method          candidate = null;
        QueryRow        queryRowAnnotation = null;

        for ( int i = 0; i < count; i++ )
        {
            candidate = methods[ i ];

            int         modifiers = candidate.getModifiers();

            if (
                Modifier.isPublic( modifiers ) &&
                Modifier.isStatic( modifiers ) &&
                candidate.getReturnType() == ResultSet.class
                )
            {
                queryRowAnnotation = candidate.getAnnotation( QueryRow.class );

                if ( queryRowAnnotation != null )
                {
                    VTIHelper.unregisterVTI( candidate );
                    
                    registerVTI
                        (
                         candidate,
                         queryRowAnnotation.jdbcDriverName(),
                         connectionURL,
                         queryRowAnnotation.query(),
                         new String[] {}
                         );
                }
            }            
        }
    }
    
    ///////////////////////////////////////////////////////////////////////////////////
    //
    // PUBLIC BEHAVIOR
    //
    ///////////////////////////////////////////////////////////////////////////////////

    /**
     * <p>
     * Close the connection associated with an URL.
     * </p>
     */
    public  static  void closeConnection( String connectionURL )
        throws SQLException
    {
        Connection      conn = _openConnections.get( connectionURL );

        if ( conn != null ) { conn.close(); }

        _openConnections.remove( connectionURL );
    }

    ///////////////////////////////////////////////////////////////////////////////////
    //
    // PUBLIC BEHAVIOR
    //
    ///////////////////////////////////////////////////////////////////////////////////

    /**
     * <p>
     * Register a method as a Derby Table Function. We assume the following:
     * </p>
     *
     * <ul>
     * <li>The method is public and static.</li>
     * <li>The method returns a ResultSet.</li>
     * </ul>
     *
     */
    public  static  void  registerVTI
        ( Method method, String jdbcDriverName, String connectionURL, String query, String[] queryArgs )
        throws Exception
    {
        QueryRow            annotation = method.getAnnotation( QueryRow.class );
        ResultSet           rs = instantiateVTI( jdbcDriverName, connectionURL, query, queryArgs );
        ResultSetMetaData   rsmd = rs.getMetaData();
        String[]            columnNames = getColumnNames( rsmd );
        String[]            columnTypes = getColumnTypes( rsmd );

        VTIHelper.registerVTI( method, columnNames, columnTypes, true );

        rs.close();
    }
    
    /**
     * <p>
     * Create a VTI ResultSet. It is assumed that our caller is a
     * QueryRow-annotated method with no arguments.
     * </p>
     *
     */
    public  static  ResultSet  instantiateQueryRowVTI( String connectionURL )
        throws SQLException
    {
        QueryRow          annotation = null;
        
        try {
            // look up the method on top of us
            StackTraceElement[]     stack = (new Throwable()).getStackTrace();
            StackTraceElement       caller = stack[ 1 ];
            Class<?>                callerClass = Class.forName( caller.getClassName() );
            String                  methodName = caller.getMethodName();
            Method                  method = callerClass.getMethod
                ( methodName, new Class[] { String.class } );
            
            annotation = method.getAnnotation( QueryRow.class );
        } catch (Throwable t) { throw new SQLException( t.getMessage() ); }

        String              jdbcDriverName = annotation.jdbcDriverName();
        String              query = annotation.query();

        return instantiateVTI( jdbcDriverName, connectionURL, query, new String[] {} );
    }
    
    /**
     * <p>
     * Create an external ResultSet given a driver, a connection url, and a query.
     * </p>
     *
     */
    public static  ResultSet  instantiateVTI( String jdbcDriverName, String connectionURL, String query, String[] queryArgs )
        throws SQLException
    {
        Connection      conn = getConnection( jdbcDriverName, connectionURL );
        int             count = 0;

        if ( queryArgs != null ) { count = queryArgs.length; }

        VTIHelper.print( query );
        
        PreparedStatement           ps = conn.prepareStatement( query );
        //ParameterMetaData           pmd = ps.getParameterMetaData();

        for ( int i = 0; i < count; i++ )
        {
            String      arg = queryArgs[ i ];
            int         param = i + 1;

            if ( arg == null )
            {
                VTIHelper.print( "Setting parameter " + param + " to null" );
                //ps.setNull( param, pmd.getParameterType( param ) );
                ps.setNull( param, Types.VARCHAR );
            }
            else {
                VTIHelper.print( "Setting parameter " + param + " to " + arg );
                ps.setString( param, arg );
            }
        }

        return ps.executeQuery();
    }
    
    ///////////////////////////////////////////////////////////////////////////////////
    //
    // MINIONS
    //
    ///////////////////////////////////////////////////////////////////////////////////

    /**
     * <p>
     * Get the column names from an external ResultSet.
     * </p>
     *
     */
    private static  String[]    getColumnNames( ResultSetMetaData rsmd )
        throws SQLException
    {
        int                     columnCount = rsmd.getColumnCount();
        String[]            columnNames = new String[ columnCount ];

        for ( int i = 0; i < columnCount; i++ ) { columnNames[ i ] = rsmd.getColumnName( i + 1 ); }

        return columnNames;
    }

    /**
     * <p>
     * Get the column types from an external ResultSet.
     * </p>
     *
     */
    private static  String[]    getColumnTypes( ResultSetMetaData rsmd )
        throws SQLException
    {
        int                     columnCount = rsmd.getColumnCount();
        String[]            columnTypes = new String[ columnCount ];

        for ( int i = 0; i < columnCount; i++ )
        { columnTypes[ i ] = getColumnType( rsmd, i + 1 ); }

        return columnTypes;
    }

    /**
     * <p>
     * Get the type of an external database's column as a Derby type name.
     * </p>
     *
     */
    private static  String    getColumnType( ResultSetMetaData rsmd, int idx )
        throws SQLException
    {
        int         jdbcType = rsmd.getColumnType( idx );
        int         precision = rsmd.getPrecision( idx );
        int         scale = rsmd.getScale( idx );

        switch( jdbcType )
        {
        case    Types.BIGINT:                           return "bigint";
        case    Types.BINARY:                           return "char " + precisionToLength( precision ) + "  for bit data";
        case    Types.BIT:                                  return "smallint";
        case    Types.BLOB:                             return "blob";
        case    Types.BOOLEAN:                      return "smallint";
        case    Types.CHAR:                             return "char" + precisionToLength( precision );
        case    Types.CLOB:                             return "clob";
        case    Types.DATE:                             return "date";
        case    Types.DECIMAL:                      return "decimal" + precisionAndScale( precision, scale );
        case    Types.DOUBLE:                       return "double";
        case    Types.FLOAT:                            return "float";
        case    Types.INTEGER:                      return "integer";
        case    Types.LONGVARBINARY:        return "long varchar for bit data";
        case    Types.LONGVARCHAR:          return "long varchar";
        case    Types.NUMERIC:                      return "numeric" + precisionAndScale( precision, scale );
        case    Types.REAL:                             return "real";
        case    Types.SMALLINT:                     return "smallint";
        case    Types.TIME:                             return "time";
        case    Types.TIMESTAMP:                return "timestamp";
        case    Types.TINYINT:                      return "smallint";
        case    Types.VARBINARY:                return "varchar " + precisionToLength( precision ) + "  for bit data";
        case    Types.VARCHAR:                      return "varchar" + precisionToLength( precision );
 
        default:
            throw new SQLException
                ( "Unknown external data type. JDBC type = " + jdbcType + ", external type name = " + rsmd.getColumnTypeName( idx ) );
        }
    }

    /**
     * <p>
     * Turns precision into a length designator.
     * </p>
     *
     */
    private static  String  precisionToLength( int precision )
    {
        return "( " + precision + " )";
    }
    
    /**
     * <p>
     * Build a precision and scale designator.
     * </p>
     *
     */
    private static  String  precisionAndScale( int precision, int scale )
    {
        return "( " + precision + ", " + scale + " )";
    }
    
    ///////////////////////////////////////////////////////////////
    //
    // CONNECTION MANAGEMENT MINIONS
    //
    ///////////////////////////////////////////////////////////////

    /**
     * <p>
     * Look up an open connection. If it doesn't exist, create a Connection.
     * </p>
     *
     */
    protected static  Connection  getConnection( String jdbcDriverName, String connectionURL )
        throws SQLException
    {
        Connection      conn = _openConnections.get( connectionURL );
        
        if ( conn == null )
        {
            // as necessary, fault in the jdbc driver which accesses the external dbms

            try {
                Class.forName( jdbcDriverName );
            }
            catch (ClassNotFoundException e)
            {
                throw new SQLException( "Could not find " + jdbcDriverName + " on the classpath." );
            }
            
            try {
                conn = DriverManager.getConnection( connectionURL );
                _openConnections.put( connectionURL, conn );
            }
            catch (SQLException s)
            {
                SQLException    t = new SQLException( "Could not open a connection to " + connectionURL + ". Perhaps the foreign database is not up and running? Details: " + s.getMessage() );

                t.setNextException( s );

                throw t;
            }
        }

        return conn;
    }
    
 
}
