/*! ****************************************************************************** * * Pentaho Data Integration * * Copyright (C) 2002-2017 by Pentaho : http://www.pentaho.com * ******************************************************************************* * * Licensed 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.pentaho.hadoop.shim.common.invocationhandler; /** * User: Dzmitry Stsiapanau Date: 01/17/2017 Time: 15:16 */ import org.pentaho.hadoop.shim.common.DriverProxyInvocationChain; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.InvocationHandler; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.Proxy; import java.sql.DatabaseMetaData; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; /** * DatabaseMetaDataInvocationHandler is a proxy handler class for java.sql.DatabaseMetaData. However the code in this * file is specifically for handling Hive JDBC calls, and therefore should not be used to proxy any other JDBC objects * besides those provided by Hive. */ public class DatabaseMetaDataInvocationHandler implements InvocationHandler { /** * The "real" database metadata object. */ DatabaseMetaData t; /** * The connection proxy associated with the DatabaseMetaData object */ ConnectionInvocationHandler c; /** * Instantiates a new database meta data invocation handler. * * @param t the database metadata object to proxy */ public DatabaseMetaDataInvocationHandler( DatabaseMetaData t, ConnectionInvocationHandler c ) { this.t = t; this.c = c; } /** * Intercepts methods called on the DatabaseMetaData object to possibly perform alternate processing. * * @param proxy the proxy * @param method the method * @param args the args * @return the object * @throws Throwable the throwable */ @Override public Object invoke( Object proxy, Method method, Object[] args ) throws Throwable { try { String methodName = method.getName(); if ( "getTables".equals( methodName ) ) { // For Hive/Impala drivers, we need to intercept the getTables method even though it doesn't // throw an exception, because the ResultSet is empty. The temp fix is to try an execute a // HiveQL query of "show tables". This only returns one (differently-named) column containing // the table/view name, vs. getTables() which returns much metadata. C'est la vie. if ( DriverProxyInvocationChain.getHive1DbMetaDataClass() != null && DriverProxyInvocationChain .getHive1DbMetaDataClass().isAssignableFrom( t.getClass() ) ) { return getTables( t, DriverProxyInvocationChain.getHive1DbMetaDataClass(), DriverProxyInvocationChain.getHive1StatementClass(), DriverProxyInvocationChain.getHive1ClientClass(), (String) args[ 0 ], (String) args[ 1 ], (String) args[ 2 ], (String[]) args[ 3 ], method, args ); } if ( DriverProxyInvocationChain.getHive2DbMetaDataClass() != null && DriverProxyInvocationChain .getHive2DbMetaDataClass().isAssignableFrom( t.getClass() ) ) { return getTables( t, DriverProxyInvocationChain.getHive2DbMetaDataClass(), DriverProxyInvocationChain.getHive2StatementClass(), DriverProxyInvocationChain.getHive2ClientClass(), (String) args[ 0 ], (String) args[ 1 ], (String) args[ 2 ], (String[]) args[ 3 ], method, args ); } } else if ( "getConnection".equals( methodName ) ) { // Return the connection return c; } else if ( "getIdentifierQuoteString".equals( methodName ) ) { // Need to intercept getIdentifierQuoteString() before trying the driver version, as our "fixed" // drivers return a single quote when it should be empty. return getIdentifierQuoteString(); } // try to invoke the method as-is Object o = method.invoke( t, args ); if ( o instanceof ResultSet ) { ResultSet r = (ResultSet) o; return (ResultSet) Proxy.newProxyInstance( r.getClass().getClassLoader(), new Class[] { ResultSet.class }, new ResultSetInvocationHandler( r ) ); } else { return o; } } catch ( Throwable t ) { if ( t instanceof InvocationTargetException ) { Throwable cause = t.getCause(); throw cause; } else { throw t; } } } /** * Returns the identifier quote string. This is HiveQL specific * * @return String the quote string for identifiers in HiveQL * @throws SQLException if any SQL error occurs */ public String getIdentifierQuoteString() throws SQLException { return ""; } /** * Gets the tables for the specified database. * * @param originalObject the original object * @param dbMetadataClass the db metadata class * @param statementClass the statement class * @param clientClass the client class * @param catalog the catalog * @param schemaPattern the schema pattern * @param tableNamePattern the table name pattern * @param types the types * @param method the original method * @param args the original args * @return the tables * @throws Exception the exception */ public ResultSet getTables( Object originalObject, Class<? extends DatabaseMetaData> dbMetadataClass, Class<? extends Statement> statementClass, Class<?> clientClass, String catalog, String schemaPattern, String tableNamePattern, String[] types, Method method, Object[] args ) throws Exception { boolean tables = false; if ( types == null ) { tables = true; } else { for ( String type : types ) { if ( "TABLE".equals( type ) ) { tables = true; } } } // If we're looking for tables, execute "show tables" query instead if ( tables ) { try { // try to invoke the method as-is Object o = method.invoke( originalObject, args ); if ( o instanceof ResultSet ) { ResultSet r = (ResultSet) o; ResultSet ret = (ResultSet) Proxy.newProxyInstance( r.getClass().getClassLoader(), new Class[] { ResultSet.class }, new ResultSetInvocationHandler( r ) ); if ( ret.isBeforeFirst() ) { return ret; } } } catch ( Exception e ) { // ignored } Statement showTables = null; // If we have a valid Connection, create and proxy the show tables statement if ( c != null ) { Statement st = c.createStatement( c.connection, null ); showTables = (Statement) Proxy.newProxyInstance( st.getClass().getClassLoader(), new Class[] { Statement.class }, new CaptureResultSetInvocationHandler<Statement>( st ) ); } else { Object client; Constructor<? extends Statement> hiveStatementCtor = (Constructor<? extends Statement>) statementClass.getDeclaredConstructor( clientClass ); // Try reflection and private member access first try { Field clientField = dbMetadataClass.getDeclaredField( "client" ); client = clientField.get( originalObject ); showTables = hiveStatementCtor.newInstance( clientClass.cast( client ) ); } catch ( Exception e ) { showTables = null; } if ( showTables == null ) { try { Method getClient = dbMetadataClass.getDeclaredMethod( "getClient" ); client = getClient.invoke( originalObject ); showTables = hiveStatementCtor.newInstance( clientClass.cast( client ) ); } catch ( Exception e ) { showTables = null; } } } // If we found a way to call "show tables", do it if ( showTables != null ) { ResultSet rs; if ( schemaPattern != null ) { rs = showTables.executeQuery( String.format( "show tables in %s", schemaPattern ) ); } else { rs = showTables.executeQuery( "show tables" ); } if ( rs != null ) { return (ResultSet) Proxy.newProxyInstance( rs.getClass().getClassLoader(), new Class[] { ResultSet.class }, new ResultSetInvocationHandler( rs ) ); } else { return null; } } else { throw new Exception( "Cannot execute SHOW TABLES query" ); } } else { Method getTables = dbMetadataClass.getDeclaredMethod( "getTables", String.class, String.class, String.class, String[].class ); ResultSet rs = (ResultSet) getTables.invoke( originalObject, catalog, schemaPattern, tableNamePattern, types ); return rs; } } }