Repository: drill Updated Branches: refs/heads/master 27b4aae20 -> 7c5a1f57c
DRILL-2343: Add tracing proxy JDBC driver for tracing JDBC method calls. Overview of org.apache.drill.jdbc.proxy classes/interfaces: Entry point: - TracingProxyDriver - Tracing proxy JDBC driver class. Class description Javadoc has usage instructions. Other core types: - ProxiesManager - creates and tracks java.lang.reflect.Proxy-based proxy objects - TracingInvocationHandler - java.lang.reflect.Proxy invocation handler; maps reflective/proxy invocations to InvocationReporter method call/return/throw event calls - InvocationReporter - defines method call/return/throw event calls - InvocationReporterImpl - implements rendering of method call/return/throw event calls (including rendering of parameter/return/exception values) - ProxySetupSQLException Unit tests: - TracingProxyDriverClassLoadingTest - test of loading proxied driver class - TracingProxyDriverTest - basic test of proxying (pass-through) and tracing output Other: - exec/jdbc-all/pom.xml - has change to keep TracingProxyDriver and depended-on classes in JDBC-all Jar file. Project: http://git-wip-us.apache.org/repos/asf/drill/repo Commit: http://git-wip-us.apache.org/repos/asf/drill/commit/7c5a1f57 Tree: http://git-wip-us.apache.org/repos/asf/drill/tree/7c5a1f57 Diff: http://git-wip-us.apache.org/repos/asf/drill/diff/7c5a1f57 Branch: refs/heads/master Commit: 7c5a1f57cc51e74c9e3ab965350bd04062b41049 Parents: 27b4aae Author: dbarclay <dbarc...@maprtech.com> Authored: Mon Feb 9 10:52:11 2015 -0800 Committer: Parth Chandra <par...@apache.org> Committed: Mon May 11 22:04:10 2015 -0700 ---------------------------------------------------------------------- exec/jdbc-all/pom.xml | 2 + .../drill/jdbc/proxy/InvocationReporter.java | 80 +++ .../jdbc/proxy/InvocationReporterImpl.java | 519 +++++++++++++++++++ .../apache/drill/jdbc/proxy/ProxiesManager.java | 117 +++++ .../jdbc/proxy/ProxySetupSQLException.java | 37 ++ .../jdbc/proxy/TracingInvocationHandler.java | 112 ++++ .../drill/jdbc/proxy/TracingProxyDriver.java | 288 ++++++++++ .../apache/drill/jdbc/proxy/package-info.java | 23 + .../TracingProxyDriverClassLoadingTest.java | 135 +++++ .../jdbc/proxy/TracingProxyDriverTest.java | 257 +++++++++ 10 files changed, 1570 insertions(+) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/drill/blob/7c5a1f57/exec/jdbc-all/pom.xml ---------------------------------------------------------------------- diff --git a/exec/jdbc-all/pom.xml b/exec/jdbc-all/pom.xml index 388f8ba..307006f 100644 --- a/exec/jdbc-all/pom.xml +++ b/exec/jdbc-all/pom.xml @@ -224,10 +224,12 @@ <option>-keep class org.apache.drill.jdbc.Driver { *; }</option> <option>-keep class org.apache.drill.jdbc.DrillJdbc40Factory { *; }</option> <option>-keep class org.apache.drill.jdbc.DrillJdbc41Factory { *; }</option> + <option>-keep class org.apache.drill.jdbc.proxy.TracingProxyDriver { *; }</option> <option>-keep class org.apache.drill.common.config.CommonConstants { *; }</option> <option>-keep class org.apache.drill.common.config.ConfigProvider { *; }</option> <option>-keep class org.apache.drill.common.config.DrillConfig { *; }</option> <option>-keep class org.apache.drill.common.config.NestedConfig { *; }</option> + <option>-keep class ch.qos.logback.** { *; }</option> <option>-keep class org.slf4j.** { *; }</option> <option>-keep class * implements com.fasterxml.jackson.databind.cfg.ConfigFeature { *; }</option> http://git-wip-us.apache.org/repos/asf/drill/blob/7c5a1f57/exec/jdbc/src/main/java/org/apache/drill/jdbc/proxy/InvocationReporter.java ---------------------------------------------------------------------- diff --git a/exec/jdbc/src/main/java/org/apache/drill/jdbc/proxy/InvocationReporter.java b/exec/jdbc/src/main/java/org/apache/drill/jdbc/proxy/InvocationReporter.java new file mode 100644 index 0000000..1aa8ff2 --- /dev/null +++ b/exec/jdbc/src/main/java/org/apache/drill/jdbc/proxy/InvocationReporter.java @@ -0,0 +1,80 @@ +/* + * 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.drill.jdbc.proxy; + +import java.lang.reflect.Method; + + +/** + * Reporter of method-invocation events. + */ +interface InvocationReporter +{ + + /** + * Reports a proxy setup message. + */ + void setupMessage( String message ); + + /** + * Reports an invocation of a proxied method. + * + * @param targetObject + * the target of the method invocation (the object on which the method + * was called) + * @param targetType + * the declared target type of the method (the interface whose method + * was called; not the actual type of {@code targetObject}) + * @param method + * the method that was called + * @param arguments + * the arguments that were passed + * (represented as for {@link Method#invoke}) + */ + void methodCalled( Object targetObject, Class<?> targetType, Method method, + Object[] arguments ); + + /** + * Reports the return from an invocation of a proxied method. + * + * @param targetObject same as for {@link #methodCalled} for the call + * @param targetType same as for {@link #methodCalled} for the call + * @param method same as for {@link #methodCalled} for the call + * @param arguments same as for {@link #methodCalled} for the call + * + * @param result the value returned from the method + * (represented as from {@link Method#invoke}) + * + */ + void methodReturned( Object targetObject, Class<?> targetType, Method method, + Object[] arguments, Object result ); + + /** + * Reports the throwing of an exception from an invocation of a proxied method. + * + * @param targetObject same as for {@link #methodCalled} for the call + * @param targetType same as for {@link #methodCalled} for the call + * @param method same as for {@link #methodCalled} for the call + * @param arguments same as for {@link #methodCalled} for the call + * + * @param exception the exception thrown by the method. + */ + void methodThrew( Object targetObject, Class<?> targetType, Method method, + Object[] arguments, Throwable exception ); + +} // interface InvocationReporter http://git-wip-us.apache.org/repos/asf/drill/blob/7c5a1f57/exec/jdbc/src/main/java/org/apache/drill/jdbc/proxy/InvocationReporterImpl.java ---------------------------------------------------------------------- diff --git a/exec/jdbc/src/main/java/org/apache/drill/jdbc/proxy/InvocationReporterImpl.java b/exec/jdbc/src/main/java/org/apache/drill/jdbc/proxy/InvocationReporterImpl.java new file mode 100644 index 0000000..031a147 --- /dev/null +++ b/exec/jdbc/src/main/java/org/apache/drill/jdbc/proxy/InvocationReporterImpl.java @@ -0,0 +1,519 @@ +/* + * 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.drill.jdbc.proxy; + +import java.lang.reflect.Method; +import java.sql.DriverPropertyInfo; + +import org.apache.commons.lang3.StringUtils; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashSet; +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.SortedSet; +import java.util.TreeSet; + + +/** + * Implementation of InvocationReporter. + * <p> + * Currently, just writes to System.err. + * </p> + */ +class InvocationReporterImpl implements InvocationReporter +{ + private static final String LINE_PREFIX = "TRACER: "; + private static final String SETUP_LINE_PREFIX = LINE_PREFIX + "SETUP: "; + private static final String WARNING_LINE_PREFIX = LINE_PREFIX + "WARNING: "; + private static final String CALL_LINE_PREFIX = LINE_PREFIX + "CALL: "; + private static final String RETURN_LINE_PREFIX = LINE_PREFIX + "RETURN: "; + private static final String THROW_LINE_PREFIX = LINE_PREFIX + "THROW: "; + + private static final Set<Package> JDBC_PACKAGES; + static { + // Load some class in each JDBC package below so package exists for + // getPackage(): + // Suppressed because we intentionally are only assigning to the variable. + @SuppressWarnings("unused") + Class<?> someReference; + someReference = java.sql.Connection.class; + someReference = javax.sql.PooledConnection.class; + someReference = javax.sql.rowset.BaseRowSet.class; + someReference = javax.sql.rowset.serial.SerialJavaObject.class; + someReference = javax.sql.rowset.spi.SyncFactory.class; + + Set<Package> set = new HashSet<>(); + set.add( Package.getPackage( "java.sql" ) ); + set.add( Package.getPackage( "javax.sql" ) ); + set.add( Package.getPackage( "javax.sql.rowset" ) ); + set.add( Package.getPackage( "javax.sql.rowset.serial" ) ); + set.add( Package.getPackage( "javax.sql.rowset.spi" ) ); + for ( Package p : set ) { + assert null != p + : "null Package; missing reference to class in that package?"; + } + JDBC_PACKAGES = Collections.unmodifiableSet( set ); + } + + /** Common packages whose names to suppress in rendered type names. */ + // (Is sorted for abbreviated-packages message to user.) + private static final SortedSet<Package> PACKAGES_TO_ABBREVIATE; + static { + SortedSet<Package> set = new TreeSet<Package>( + new Comparator<Package>() { + @Override + public int compare( Package o1, Package o2 ) { + return + null == o1 ? -1 + : null == o2 ? 1 + : o1.getName().compareTo( o2.getName() ); + } + } ); + set.addAll( JDBC_PACKAGES ); + set.add( Package.getPackage( "java.util" ) ); + set.add( Package.getPackage( "java.lang" ) ); + PACKAGES_TO_ABBREVIATE = Collections.unmodifiableSortedSet( set ); + } + + private int lastObjNum = 0; + private Map<Object, String> objectsToIdsMap = new IdentityHashMap<>(); + + + void reportAbbreviatedPackages() { + final List<String> names = new ArrayList<>(); + for ( Package p : PACKAGES_TO_ABBREVIATE ) { + names.add( p.getName() ); + } + setupMessage( "Abbreviating (unique) class names in packages " + + StringUtils.join( names, ", " ) + "." ); + } + + //////////////////// + // Line-output output methods: + + // TODO: When needed, allow output to something other then System.err + // (e.g., appending to file, System.out). Decide control--probably parameter + // in proxy URL and/or JVM system property (checked higher up and configuring + // this class). + + /** + * Prints a line to the tracing log. + * <p> + * Is it intended that all tracing output goes through this method. + * </p> + */ + private void printTraceLine( final String line ) { + System.err.println( line ); + } + + + /** + * For warnings such as warning about encountering a type that for which + * rendering isn't known to show values well. + */ + private void printWarningLine( final String line ) { + printTraceLine( WARNING_LINE_PREFIX + line ); + } + + + //////////////////// + // Type, value, exception, and arguments formatting methods: + + private String getObjectId( final Object object ) + { + String id; + if ( null == object ) { + id = "n/a"; + } + else { + id = objectsToIdsMap.get( object ); + if ( null == id ) { + ++lastObjNum; + id = Integer.toString( lastObjNum ); + objectsToIdsMap.put( object, id ); + } + } + return id; + } + + /** + * Renders a type name. Uses simple names for common types (JDBC interfaces + * and {code java.lang.*}). + */ + private String formatType( final Class<?> type ) { + final String result; + if ( type.isArray() ) { + result = formatType( type.getComponentType() ) + "[]"; + } else { + // Suppress package name for common (JDBC and java.lang) types, except + // when would be ambiguous (e.g., java.sql.Date vs. java.util.Date). + if ( PACKAGES_TO_ABBREVIATE.contains( type.getPackage() ) ) { + int sameSimpleNameCount = 0; + for ( Package p : PACKAGES_TO_ABBREVIATE ) { + try { + Class.forName( p.getName() + "." + type.getSimpleName() ); + sameSimpleNameCount++; + } + catch ( ClassNotFoundException e ) { + // Nothing to do. + } + } + if ( 1 == sameSimpleNameCount ) { + result = type.getSimpleName(); + } + else { + // Multiple classes with same simple name, so would be ambiguous to + // abbreviate, so use fully qualified name. + result = type.getName(); + } + } + else { + result = type.getName(); + } + } + return result; + } + + private String formatString( final String value ) { + return + "\"" + + ( ((String) value) + .replace( "\\", "\\\\" ) // first encode backslashes (esc. char.) + .replace( "\"", "\\\"" ) // then encode quotes (via backslash) + .replace( "\n", "\\n" ) // then encode newlines + // Anything else? + ) + + "\""; + } + + private String formatDriverPropertyInfo( final DriverPropertyInfo info ) { + return + "[ " + + "name = " + formatValue( info.name ) + + ", value = " + formatValue( info.value ) + + ", required = " + info.required + + ", choices = " + formatValue( info.choices ) + + ", description = " + formatValue( info.description ) + + " ]"; + } + + private String formatValue( final Object value ) { + final String result; + if ( null == value ) { + result = "null"; + } + else { + final Class<?> rawActualType = value.getClass(); + if ( String.class == rawActualType ) { + result = formatString( (String) value ); + } + else if ( rawActualType.isArray() + && ! rawActualType.getComponentType().isPrimitive() ) { + // Array of non-primitive type + + final StringBuilder buffer = new StringBuilder(); + /* Decide whether to includes this: + buffer.append( formatType( elemType ) ); + buffer.append( "[] " ); + */ + buffer.append( "{ " ); + boolean first = true; + for ( Object elemVal : (Object[]) value ) { + if ( ! first ) { + buffer.append( ", " ); + } + first = false; + buffer.append( formatValue( elemVal ) ); + } + buffer.append( " }" ); + result = buffer.toString(); + } + else if ( DriverPropertyInfo.class == rawActualType ) { + result = formatDriverPropertyInfo( (DriverPropertyInfo) value ); + } + else if ( + // Is type seen and whose toString() renders value well. + false + || rawActualType == java.lang.Boolean.class + || rawActualType == java.lang.Byte.class + || rawActualType == java.lang.Double.class + || rawActualType == java.lang.Float.class + || rawActualType == java.lang.Integer.class + || rawActualType == java.lang.Long.class + || rawActualType == java.lang.Short.class + || rawActualType == java.math.BigDecimal.class + || rawActualType == java.lang.Class.class + || rawActualType == java.sql.Date.class + || rawActualType == java.sql.Timestamp.class + ) { + result = value.toString(); + } + else if ( + // Is type seen and whose toString() has rendered value well--in cases + // seen so far. + false + || rawActualType == java.util.Properties.class + || rawActualType.isEnum() + ) { + result = value.toString(); + } + else if ( + // Is type to warn about (one case). + false + || rawActualType == org.apache.drill.jdbc.DrillResultSet.class + ) { + printWarningLine( + "Class " + rawActualType.getName() + " should be an interface." + + " (While it's a class, it can't be proxied, and some methods can't" + + " be traced.)" ); + result = value.toString(); + } + else if ( + // Is type to warn about (second case). + false + || rawActualType == org.apache.hadoop.io.Text.class + || rawActualType == org.joda.time.Period.class + || rawActualType == + org.apache.drill.exec.vector.accessor.sql.TimePrintMillis.class + ) { + printWarningLine( "Should " + rawActualType + + " be appearing at JDBC interface?" ); + result = value.toString(); + } + else { + // Is other type--unknown whether it already formats well. + // (No handled yet: byte[].) + printWarningLine( "Unnoted type encountered in formatting (value might" + + " not render well): " + rawActualType + "." ); + result = value.toString(); + } + } + return result; + } + + /** + * Renders a value with its corresponding <em>declared</em> type. + * + * @param declaredType + * the corresponding declared method parameter or return type + * @value value + * the value to render + */ + private String formatTypeAndValue( Class<?> declaredType, Object value ) { + final String declaredTypePart = "(" + formatType( declaredType ) + ") "; + + final String actualTypePart; + final String actualValuePart; + if ( null == value ) { + // Null--show no actual type or object ID. + actualTypePart = ""; + actualValuePart = formatValue( value ); + } + else { + // Non-null value--show at least some representation of value. + Class<?> rawActualType = value.getClass(); + Class<?> origActualType = + declaredType.isPrimitive() ? declaredType : rawActualType; + if ( String.class == rawActualType ) { + // String--show no actual type or object ID. + actualTypePart = ""; + actualValuePart = formatValue( value ); + } + else if ( origActualType.isPrimitive() ) { + // Primitive type--show no actual type or object ID. + actualTypePart = ""; + // (Remember--primitive type is wrapped here.) + actualValuePart = value.toString(); + } + else { + // Non-primitive, non-String value--include object ID. + final String idPrefix = "<id=" + getObjectId( value ) + "> "; + if ( declaredType.isInterface() + && JDBC_PACKAGES.contains( declaredType.getPackage() ) ) { + // JDBC interface implementation class--show no actual type or value + // (because object is proxied and therefore all uses will be traced). + actualTypePart = ""; + actualValuePart = idPrefix + "..."; + } + else if ( origActualType == declaredType ) { + // Actual type is same as declared--don't show redundant actual type. + actualTypePart = ""; + actualValuePart = idPrefix + formatValue( value ); + } + else { + // Other--show actual type and (try to) show value. + actualTypePart = "(" + formatType( rawActualType) + ") "; + actualValuePart = idPrefix + formatValue( value ); + } + } + } + final String result = declaredTypePart + actualTypePart + actualValuePart; + return result; + } + + /** + * Renders given type and value of target (receiver) of method call. + */ + private String formatTargetTypeAndValue( Class<?> declaredType, Object value ) { + return formatTypeAndValue( declaredType, value ); + } + + /** + * Renders given type and value of method call argument. + */ + // Expect JDBC interface types; (mostly) doesn't expect other JDBC types; + // expect mostly primitives and String. + private String formatArgTypeAndValue( Class<?> declaredType, Object value ) { + return formatTypeAndValue( declaredType, value ); + } + + /** + * Renders given type and value of method return value. + */ + // Expect declared type Object (need actual type); expect primitive types + // (maybe wrapper classes too?) + private String formatReturnTypeAndValue( Class<?> declaredType, Object value ) { + return formatTypeAndValue( declaredType, value ); + } + + /** + * Renders given exception. + * Includes test of chained exceptions. + */ + private String formatThrowable( final Throwable thrown ) { + final StringBuffer s = new StringBuffer(); + boolean first = true; + Throwable current = thrown; + while ( null != current ) { + if ( ! first ) { + s.append( " ==> "); + } + first = false; + + s.append( "(" ); + s.append( formatType( current.getClass() ) ); + s.append( ") "); + s.append( formatString( current.toString() ) ); + current = current.getCause(); + } + final String result = s.toString(); + return result; + } + + /** + * Renders corresponding given sequence of declared types and given sequence + * of values from method call. + */ + private String formatArgs( Class<?>[] declaredTypes, Object[] argValues ) + { + final String result; + if ( null == argValues ) { + result = "()"; + } + else { + final StringBuilder s = new StringBuilder(); + s.append( "( " ); + for ( int ax = 0; ax < argValues.length; ax++ ) { + if ( ax > 0 ) { + s.append( ", " ); + } + s.append( formatArgTypeAndValue( declaredTypes[ ax ], argValues[ ax ] ) ); + } + s.append( " )" ); + result = s.toString(); + } + return result; + } + + /** + * Renders the call part for a method call, method return, or exception-thrown + * event. + * @param target + * the target (receiver) of the method call + * @param targetType + * the interface containing called method + * @param method + * the called method + * @param argValues + * the argument values (represented as for {@link Method#invoke}) + * + */ + private String formatCallPart( final Object target, + final Class<?> targetType, + final Method method, + final Object[] argValues ) { + return + "(" + formatTargetTypeAndValue( targetType, target ) + ") . " + + method.getName() + formatArgs( method.getParameterTypes(), argValues ); + } + + + //////////////////// + // Invocation-level methods: + + @Override + public void setupMessage( final String message ) { + printTraceLine( SETUP_LINE_PREFIX + message ); + } + + @Override + public void methodCalled( final Object target, + final Class<?> targetType, + final Method method, + final Object[] args ) { + printTraceLine( CALL_LINE_PREFIX + + formatCallPart( target, targetType, method, args ) ); + } + + @Override + public void methodReturned( final Object target, + final Class<?> targetType, + final Method method, + final Object[] args, final + Object result ) { + final String callPart = + RETURN_LINE_PREFIX + formatCallPart( target, targetType, method, args ); + if ( void.class == method.getReturnType() ) { + assert null == result + : "unexpected non-null result value " + result + + " for method returning " + method.getReturnType(); + printTraceLine( callPart + ", RESULT: (none--void) " ); + } else { + printTraceLine( + callPart + ", RESULT: " + + formatReturnTypeAndValue( method.getReturnType(), result ) ); + } + } + + @Override + public void methodThrew( final Object target, + final Class<?> targetType, + final Method method, + final Object[] args, + final Throwable exception ) { + printTraceLine( THROW_LINE_PREFIX + + formatCallPart( target, targetType, method, args ) + + ", threw: " + formatThrowable( exception ) ); + } + +} // class SimpleInvocationReporter http://git-wip-us.apache.org/repos/asf/drill/blob/7c5a1f57/exec/jdbc/src/main/java/org/apache/drill/jdbc/proxy/ProxiesManager.java ---------------------------------------------------------------------- diff --git a/exec/jdbc/src/main/java/org/apache/drill/jdbc/proxy/ProxiesManager.java b/exec/jdbc/src/main/java/org/apache/drill/jdbc/proxy/ProxiesManager.java new file mode 100644 index 0000000..7d2da53 --- /dev/null +++ b/exec/jdbc/src/main/java/org/apache/drill/jdbc/proxy/ProxiesManager.java @@ -0,0 +1,117 @@ +/* + * 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.drill.jdbc.proxy; + +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Proxy; +import java.util.IdentityHashMap; +import java.util.Map; + + +/** + * Manager of proxy classes and instances. + * Managing includes creating proxies and tracking to re-use proxies. + */ +class ProxiesManager { + private final InvocationReporter reporter; + + private Map<Class<?>, Class<Proxy>> interfacesToProxyClassesMap = + new IdentityHashMap<>(); + + /** Map of proxied original objects from proxied JDBC driver to + * proxy objects to be returned by tracing proxy driver. */ + private Map<Object, Object> proxiedsToProxiesMap = new IdentityHashMap<>(); + + + public ProxiesManager( final InvocationReporter reporter ) { + this.reporter = reporter; + } + + private Class<Proxy> getProxyClassForInterface( final Class<?> interfaceType ) { + assert interfaceType.isInterface(); + + Class<Proxy> proxyReturnClass = interfacesToProxyClassesMap.get( interfaceType ); + if ( null == proxyReturnClass ) { + + // Suppressed because we know getProxyClass returns class extending Proxy. + @SuppressWarnings("unchecked") + Class<Proxy> newProxyReturnClass = + (Class<Proxy>) Proxy.getProxyClass( interfaceType.getClassLoader(), + new Class[] { interfaceType }); + interfacesToProxyClassesMap.put( interfaceType, proxyReturnClass ); + proxyReturnClass = newProxyReturnClass; + } + return proxyReturnClass; + } + + + /** + * Creates or retrieves proxy instance to be returned for given original + * instance. + * @param originalInstance + * the original object + * @param declaredType + * the declared type of source of the original object; interface type + */ + public <INTF> INTF getProxyInstanceForOriginal( final INTF originalInstance, + final Class<?> declaredType ) { + final INTF proxyInstance; + + assert declaredType.isAssignableFrom( originalInstance.getClass() ) + : "toBeProxied is of class (" + originalInstance.getClass().getName() + + ") that doesn't implement specified interface " + declaredType.getName(); + // Suppressed because checked immediately above. + @SuppressWarnings("unchecked") + final INTF existingProxy = (INTF) proxiedsToProxiesMap.get( originalInstance ); + + if ( null != existingProxy ) { + // Repeated occurrence of original--return same proxy instance as before. + proxyInstance = existingProxy; + } + else { + // Original we haven't seen yet--create proxy instance and return that. + + Class<Proxy> proxyReturnClass = getProxyClassForInterface( declaredType ); + + // Create tracing handler instance for this proxy/original pair. + final InvocationHandler callHandler = + new TracingInvocationHandler<INTF>( this, reporter, + originalInstance, declaredType ); + try { + // Suppressed because we know that proxy class implements INTF. + @SuppressWarnings("unchecked") + INTF newProxyInstance = + (INTF) + proxyReturnClass + .getConstructor( new Class[] { InvocationHandler.class } ) + .newInstance( new Object[] { callHandler } ); + proxiedsToProxiesMap.put( originalInstance, newProxyInstance ); + proxyInstance = newProxyInstance; + } + catch ( InstantiationException | IllegalAccessException + | IllegalArgumentException | InvocationTargetException + | NoSuchMethodException | SecurityException e ) { + throw new RuntimeException( + "Error creating proxy for " + declaredType + ": " + e , e ); + } + } + return proxyInstance; + } + +} // class ProxiesManager http://git-wip-us.apache.org/repos/asf/drill/blob/7c5a1f57/exec/jdbc/src/main/java/org/apache/drill/jdbc/proxy/ProxySetupSQLException.java ---------------------------------------------------------------------- diff --git a/exec/jdbc/src/main/java/org/apache/drill/jdbc/proxy/ProxySetupSQLException.java b/exec/jdbc/src/main/java/org/apache/drill/jdbc/proxy/ProxySetupSQLException.java new file mode 100644 index 0000000..e1bc582 --- /dev/null +++ b/exec/jdbc/src/main/java/org/apache/drill/jdbc/proxy/ProxySetupSQLException.java @@ -0,0 +1,37 @@ +/* + * 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.drill.jdbc.proxy; + +import java.sql.SQLNonTransientConnectionException; + +/** + * SQLException for tracing-proxy connection and setup problems. + */ +class ProxySetupSQLException extends SQLNonTransientConnectionException { + + private static final long serialVersionUID = 2015_02_08L; + + ProxySetupSQLException( String message, Throwable cause ) { + super( message, cause ); + } + + ProxySetupSQLException( String message ) { + super( message ); + } + +} // class ProxySetupSQLException http://git-wip-us.apache.org/repos/asf/drill/blob/7c5a1f57/exec/jdbc/src/main/java/org/apache/drill/jdbc/proxy/TracingInvocationHandler.java ---------------------------------------------------------------------- diff --git a/exec/jdbc/src/main/java/org/apache/drill/jdbc/proxy/TracingInvocationHandler.java b/exec/jdbc/src/main/java/org/apache/drill/jdbc/proxy/TracingInvocationHandler.java new file mode 100644 index 0000000..8c94309 --- /dev/null +++ b/exec/jdbc/src/main/java/org/apache/drill/jdbc/proxy/TracingInvocationHandler.java @@ -0,0 +1,112 @@ +/* + * 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.drill.jdbc.proxy; + +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +/** + * Tracing proxy-invocation handler. + * Reports invocations of methods of given proxied object to given invocation + * reporter. + */ +class TracingInvocationHandler<INTF> implements InvocationHandler +{ + private final ProxiesManager proxiesManager; + private final InvocationReporter callReporter; + private final INTF proxiedObject; + private final Class<?> proxiedInterface; + + + /** + * Constructs invocation handler for given object, treats ~as given (single) + * interface. + * + * @param proxiesManager + * the proxies manager to use for creating new proxy objects + * @param reporter + * the invocation report to use to report invocation events + * @param proxiedObject + * ... + * @param proxiedInterface + * ... + */ + TracingInvocationHandler( final ProxiesManager proxiesManager, + final InvocationReporter reporter, + final INTF proxiedObject, + final Class<?> proxiedInterface ) { + this.proxiesManager = proxiesManager; + this.callReporter = reporter; + this.proxiedObject = proxiedObject; + this.proxiedInterface = proxiedInterface; + } + + + @Override + public Object invoke( Object proxy, Method method, Object[] args ) + throws Throwable { + + // Report that method was called: + callReporter.methodCalled( proxiedObject, proxiedInterface, method, args ); + + final Object rawReturnedResult; + final Object netReturnedResult; + try { + // Invoke proxied original object's method: + rawReturnedResult = method.invoke( proxiedObject, args ); + + if ( null == rawReturnedResult ) { + netReturnedResult = null; + } + else { + Class<?> methodReturnType = method.getReturnType(); + + if ( ! methodReturnType.isInterface() ) { + // Declared type is not an interface type, so don't proxy the returned + // instance. (We could proxy and intercept some methods, but we can't + // intercept all, so intercepting only some would be misleading.) + netReturnedResult = rawReturnedResult; + } + else { + // Get the new or existing proxying instance for the returned instance. + netReturnedResult = + proxiesManager.getProxyInstanceForOriginal( rawReturnedResult, + methodReturnType ); + } + } + } + catch ( IllegalAccessException | IllegalArgumentException e ) { + throw new RuntimeException( + "Unexpected/unhandled error calling proxied method: " + e, e ); + } + catch ( InvocationTargetException e ) { + Throwable thrownResult = e.getCause(); + // Report that method threw exception: + callReporter.methodThrew( proxiedObject, proxiedInterface, method, args, + thrownResult ); + throw thrownResult; + } + + // Report that method returned: + callReporter.methodReturned( proxiedObject, proxiedInterface, method, args, + rawReturnedResult ); + return netReturnedResult; + } // invoke(...) + +} // class ProxyInvocationHandler<INTF> http://git-wip-us.apache.org/repos/asf/drill/blob/7c5a1f57/exec/jdbc/src/main/java/org/apache/drill/jdbc/proxy/TracingProxyDriver.java ---------------------------------------------------------------------- diff --git a/exec/jdbc/src/main/java/org/apache/drill/jdbc/proxy/TracingProxyDriver.java b/exec/jdbc/src/main/java/org/apache/drill/jdbc/proxy/TracingProxyDriver.java new file mode 100644 index 0000000..d575d4f --- /dev/null +++ b/exec/jdbc/src/main/java/org/apache/drill/jdbc/proxy/TracingProxyDriver.java @@ -0,0 +1,288 @@ +/* + * 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.drill.jdbc.proxy; + +import java.sql.Connection; +import java.sql.Driver; +import java.sql.DriverManager; +import java.sql.DriverPropertyInfo; +import java.sql.SQLException; +import java.sql.SQLFeatureNotSupportedException; +import java.util.Properties; + +import org.slf4j.Logger; + +import static org.slf4j.LoggerFactory.getLogger; + +/** + * Proxy driver for tracing calls to a JDBC driver. + * Reports calls and parameter values to, and return values and exceptions from, + * methods defined by JDBC interfaces. + * + * <p><strong>Invocation:</strong></p> + * <p> + * To set up a tracing version of a JDBC connection: + * </p> + * <ul> + * <li>Construct the proxying URL corresponding to the original URL. (See + * below).</li> + * <li>Replace the occurrence, in the JDBC-client code or tool, of the + * original URL with the corresponding proxying URL.</li> + * <li>Make sure that class {@link TracingProxyDriver} will be loaded (e.g., + * configure the client to use as the driver class).</li> + * </ul> + * <p> + * The proxying URL corresponding to an original URL is + * "<code>jdbc:proxy:<i>original.Driver</i>:<i>original_URL</i></code>", + * where: + * </p> + * <ul> + * <li> + * <code><i>original.Driver</i></code> is the fully qualified name of the + * driver class to proxy and trace (used to load the class to get it + * registered with JDBC's {@link DriverManager}, when or in case it's not + * already loaded); it can blank if the driver class will already be loaded, + * and + * </li> + * <li> + * <code><i>original_URL</i></code> is the original URL for the JDBC data + * source to now be traced. + * </li> + * </ul> + * <p> + * For example, for "{@code jdbc:drill:zk=local}", the tracing URL is + * "{@code jdbc:proxy:org.apache.drill.jdbc.Driver:jdbc:drill:zk=local}". + * </p> + * <p><strong>Output:</strong></p> + * <p> + * Currently, the tracing output line are simply written to {@link System.out} + * ({@code stdout} or "standard output"). + * </p> + */ +public class TracingProxyDriver implements java.sql.Driver { + + /** JDBC URL prefix that tracing proxy driver recognizes. */ + private static final String JDBC_URL_PREFIX = "jdbc:proxy:"; + + // TODO: Maybe split into static setup reporter vs. non-static tracing + // reporter, especially if output destination becomes configurable via + // something in the proxy/tracing URI. + // (Static because called in class initialization.) + private static final InvocationReporter reporter; + static { + InvocationReporterImpl simpleReporter = new InvocationReporterImpl(); + // Note: This is intended to be first line written to output: + simpleReporter.setupMessage( "Proxy driver " + TracingProxyDriver.class + + " initializing."); + simpleReporter.reportAbbreviatedPackages(); + reporter = simpleReporter; + } + + private final ProxiesManager proxiesManager = new ProxiesManager( reporter ); + + /** Most recent (and usually only) proxyDriver created by this (usually + * singleton) instance. */ + private Driver proxyDriver; + + + // Statically create and register an instance with JDBC's DriverManager. + static { + reporter.setupMessage( "Proxy driver registering with DriverManager." ); + final Driver proxyDriver; + try { + proxyDriver = new TracingProxyDriver(); + } + catch ( SQLException e ) { + throw new RuntimeException( + "Error in initializing " + TracingProxyDriver.class + ": " + e, e ); + } + try { + DriverManager.registerDriver( proxyDriver ); + } + catch ( SQLException e ) { + throw new RuntimeException( + "Error in registering " + TracingProxyDriver.class + ": " + e, e ); + } + } + + + public TracingProxyDriver() throws SQLException { + } + + private static class UrlHandler { + private static final String SYNTAX_TEXT = + "proxy URL syntax: \"" + JDBC_URL_PREFIX + "\" + \":\" " + + "+ optional original driver class name + \":\" " + + "+ proxied (original) JDBC URL"; + + private final String classSpec; + private final String proxiedURL; + private final Driver proxiedDriverForProxiedUrl; + private final Driver proxyDriver; + + + UrlHandler( ProxiesManager proxiesManager, String url ) + throws ProxySetupSQLException { + + final String variablePart = url.substring( JDBC_URL_PREFIX.length() ); + + final int classEndColonPos = variablePart.indexOf( ':' ); + if ( -1 == classEndColonPos ) { + throw new ProxySetupSQLException( + "Connection URL syntax error: no third colon in proxy URL \"" + url + + "\"; (" + SYNTAX_TEXT + ")" ); + } + // Either class name (load named class) or empty string (don't load any). + classSpec = variablePart.substring( 0, classEndColonPos ); + proxiedURL = variablePart.substring( 1 + classEndColonPos ); + + if ( ! "".equals( classSpec ) ) { + try { + Class.forName( classSpec); + } + catch ( ClassNotFoundException e ) { + throw new ProxySetupSQLException( + "Couldn't load class \"" + classSpec + "\"" + + " (from proxy driver URL \"" + url + "\" (between second and " + + "third colons)): " + e, + e ); + } + } + + try { + reporter.setupMessage( "Proxy calling DriverManager.getDriver(...) for" + + " proxied URL \"" + proxiedURL + "\"." ); + proxiedDriverForProxiedUrl = DriverManager.getDriver( proxiedURL ); + reporter.setupMessage( + "DriverManager.getDriver( \"" + proxiedURL + "\" ) returned a(n) " + + proxiedDriverForProxiedUrl.getClass().getName() + ": " + + proxiedDriverForProxiedUrl + "." ); + } + catch ( SQLException e ) { + final String message = + "Error getting driver from DriverManager for proxied URL \"" + + proxiedURL + "\" (from proxy driver URL \"" + url + "\"" + + " (after third colon)): " + e; + reporter.setupMessage( message ); + throw new ProxySetupSQLException( message, e ); + } + proxyDriver = + proxiesManager.getProxyInstanceForOriginal( proxiedDriverForProxiedUrl, + Driver.class ); + } + + public String getProxiedUrl() { + return proxiedURL; + } + + public Driver getProxiedDriver() { + return proxiedDriverForProxiedUrl; + } + + public Driver getProxyDriver() { + return proxyDriver; + } + + } // class UrlHandler + + + private void setProxyDriver( final Driver newProxyDriver, + final Driver proxiedDriver ) { + // Note if different proxy than before. + if ( null != this.proxyDriver && newProxyDriver != this.proxyDriver ) { + reporter.setupMessage( + "Note: Multiple drivers proxied; Driver-level methods such as " + + "getMajorVersion() will be routed to latest" + + " (" + proxiedDriver + ")." ); + } + this.proxyDriver = newProxyDriver; + } + + @Override + public boolean acceptsURL( String url ) throws SQLException { + reporter.setupMessage( "Proxy's acceptsURL(...) called with " + + ( null == url ? "null" : "\"" + url + "\"." ) ); + final boolean accepted; + if ( null == url || ! url.startsWith( JDBC_URL_PREFIX ) ) { + accepted = false; + } + else { + UrlHandler urlHandler = new UrlHandler( proxiesManager, url ); + setProxyDriver( urlHandler.getProxyDriver(), urlHandler.getProxiedDriver() ); + + accepted = true; // (If no exception in UrlHandler(...) + } + reporter.setupMessage( + "Proxy's acceptsURL( " + ( null == url ? "null" : "\"" + url + "\"" ) + + " ) returning " + accepted + "." ); + return accepted; + } + + @Override + public Connection connect( String url, Properties info ) + throws ProxySetupSQLException { + final Connection result; + reporter.setupMessage( "Proxy's connect(...) called with URL " + + ( null == url ? "null" : "\"" + url + "\"" ) + "." ); + + if ( null == url || ! url.startsWith( JDBC_URL_PREFIX ) ) { + result = null; // (Not a URL understood by this driver.) + } + else { + UrlHandler urlHandler = new UrlHandler( proxiesManager, url ); + setProxyDriver( urlHandler.getProxyDriver(), urlHandler.getProxiedDriver() ); + + // (Call connect() through proxy so it gets traced too.) + try { + result = proxyDriver.connect( urlHandler.getProxiedUrl(), info ); + } + catch ( SQLException e ) { + throw new ProxySetupSQLException( "Exception from proxied driver: " + e, + e ); + } + } + return result; + } + + @Override + public DriverPropertyInfo[] getPropertyInfo( String url, Properties info ) + throws SQLException { + return proxyDriver.getPropertyInfo( url, info ); + } + + @Override + public int getMajorVersion() { + return proxyDriver.getMajorVersion(); + } + + @Override + public int getMinorVersion() { + return proxyDriver.getMinorVersion(); + } + @Override + public boolean jdbcCompliant() { + return proxyDriver.jdbcCompliant(); + } + + @Override + public java.util.logging.Logger getParentLogger() + throws SQLFeatureNotSupportedException { + return proxyDriver.getParentLogger(); + } + +} // class TracingProxyDriver http://git-wip-us.apache.org/repos/asf/drill/blob/7c5a1f57/exec/jdbc/src/main/java/org/apache/drill/jdbc/proxy/package-info.java ---------------------------------------------------------------------- diff --git a/exec/jdbc/src/main/java/org/apache/drill/jdbc/proxy/package-info.java b/exec/jdbc/src/main/java/org/apache/drill/jdbc/proxy/package-info.java new file mode 100644 index 0000000..911b298 --- /dev/null +++ b/exec/jdbc/src/main/java/org/apache/drill/jdbc/proxy/package-info.java @@ -0,0 +1,23 @@ +/* + * 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. + */ +/** + * Tracing proxy JDBC driver. Traces calls to another JDBC driver. + * + * @see TracingProxyDriver + */ +package org.apache.drill.jdbc.proxy; http://git-wip-us.apache.org/repos/asf/drill/blob/7c5a1f57/exec/jdbc/src/test/java/org/apache/drill/jdbc/proxy/TracingProxyDriverClassLoadingTest.java ---------------------------------------------------------------------- diff --git a/exec/jdbc/src/test/java/org/apache/drill/jdbc/proxy/TracingProxyDriverClassLoadingTest.java b/exec/jdbc/src/test/java/org/apache/drill/jdbc/proxy/TracingProxyDriverClassLoadingTest.java new file mode 100644 index 0000000..13becac --- /dev/null +++ b/exec/jdbc/src/test/java/org/apache/drill/jdbc/proxy/TracingProxyDriverClassLoadingTest.java @@ -0,0 +1,135 @@ +/* + * 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.drill.jdbc.proxy; + +import org.apache.drill.test.DrillTest; + +import java.sql.Connection; +import java.sql.DatabaseMetaData; +import java.sql.Driver; +import java.sql.DriverManager; +import java.sql.SQLException; + +import org.junit.Ignore; +import org.junit.Test; + +import static org.junit.Assert.assertThat; +import static org.hamcrest.CoreMatchers.*; + + +// NOTE: Currently, must not inherit from anything that loads the Drill driver +// class (and must not be run in JVM where the Drill driver class has already +// been loaded). + +/** + * Test of TracingProxyDriver's loading of driver class. + */ +public class TracingProxyDriverClassLoadingTest extends DrillTest { + + @Ignore( "except when run in own JVM (so Drill Driver not already loaded)" ) + @Test + public void testClassLoading() throws SQLException, ClassNotFoundException { + + // Note: Throwing exceptions for test setup problems so they're JUnit + // errors (red in Eclipse's JUnit view), not just JUnit test failures + // (blue in Eclipse). + + // 1. Confirm that Drill driver is not loaded/registered. + try { + DriverManager.getDriver( "jdbc:drill:zk=local" ); + throw new IllegalStateException( + "Drill driver seems loaded already; can't test loading." ); + } + catch ( SQLException e ) { + // (Message as of JDK 1.7.) + assertThat( "Not expected messsage. (Did JDK change?)", + e.getMessage(), equalTo( "No suitable driver" ) ); + } + try { + DriverManager.getConnection( "jdbc:drill:zk=local", null ); + throw new IllegalStateException( + "Drill driver seems loaded already; can't test loading." ); + } + catch ( SQLException e ) { + // (Message form as of JDK 1.7.) + assertThat( "Not expected messsage. (Did JDK change?)", + e.getMessage(), + equalTo( "No suitable driver found for jdbc:drill:zk=local" ) ); + } + + // 2. Confirm that TracingProxyDriver is not loaded/registered. + try { + DriverManager.getDriver( "jdbc:proxy::jdbc:drill:zk=local" ); + throw new IllegalStateException( + "Proxy driver seems loaded already; can't test loading." ); + } + catch ( SQLException e ) { + assertThat( "Not expected messsage. (Did JDK change?)", + e.getMessage(), equalTo( "No suitable driver" ) ); + } + try { + DriverManager.getConnection( "jdbc:proxy::jdbc:drill:zk=local", null ); + throw new IllegalStateException( + "Proxy driver seems loaded already; can't test loading." ); + } + catch ( SQLException e ) { + assertThat( + "Not expected messsage. (Did JDK change?)", + e.getMessage(), + equalTo( "No suitable driver found for jdbc:proxy::jdbc:drill:zk=local" ) ); + } + + // 3. Load TracingProxyDriver. + Class.forName( "org.apache.drill.jdbc.proxy.TracingProxyDriver" ); + + // 4. Confirm that Drill driver still is not registered. + try { + DriverManager.getConnection( "jdbc:proxy::jdbc:drill:zk=local", null ); + throw new IllegalStateException( + "Drill driver seems loaded already; can't test loading." ); + } + catch ( ProxySetupSQLException e ) { + assertThat( + "Not expected messsage. (Was it just modified?)", + e.getMessage(), + equalTo( + "Error getting driver from DriverManager for proxied URL" + + " \"jdbc:drill:zk=local\" (from proxy driver URL" + + " \"jdbc:proxy::jdbc:drill:zk=local\" (after third colon))" + + ": java.sql.SQLException: No suitable driver" ) ); + } + + // 5. Test that TracingProxyDriver can load and use a specified Driver class. + final Driver driver = + DriverManager.getDriver( + "jdbc:proxy:org.apache.drill.jdbc.Driver:jdbc:drill:zk=local" ); + + assertThat( driver.acceptsURL( "jdbc:proxy::jdbc:drill:zk=local" ), + equalTo( true ) ); + assertThat( driver.acceptsURL( "jdbc:drill:zk=local" ), equalTo( false ) ); + + // 7. Test minimally that driver can get connection that works. + final Connection proxyConnection = + DriverManager.getConnection( "jdbc:proxy::jdbc:drill:zk=local", null ); + assertThat( proxyConnection, notNullValue() ); + + final DatabaseMetaData dbMetaData = proxyConnection.getMetaData(); + assertThat( dbMetaData, instanceOf( DatabaseMetaData.class ) ); + } + +} // class TracingProxyDriverClassLoadingTest http://git-wip-us.apache.org/repos/asf/drill/blob/7c5a1f57/exec/jdbc/src/test/java/org/apache/drill/jdbc/proxy/TracingProxyDriverTest.java ---------------------------------------------------------------------- diff --git a/exec/jdbc/src/test/java/org/apache/drill/jdbc/proxy/TracingProxyDriverTest.java b/exec/jdbc/src/test/java/org/apache/drill/jdbc/proxy/TracingProxyDriverTest.java new file mode 100644 index 0000000..389cbac --- /dev/null +++ b/exec/jdbc/src/test/java/org/apache/drill/jdbc/proxy/TracingProxyDriverTest.java @@ -0,0 +1,257 @@ +/* + * 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.drill.jdbc.proxy; + +import org.apache.drill.test.DrillTest; + +import java.io.ByteArrayOutputStream; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.PrintStream; +import java.io.StringWriter; +import java.nio.charset.StandardCharsets; +import java.sql.Connection; +import java.sql.DatabaseMetaData; +import java.sql.Driver; +import java.sql.DriverManager; +import java.sql.ResultSet; +import java.sql.ResultSetMetaData; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.Properties; + +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.hamcrest.CoreMatchers.*; + +/** + * Test of TracingProxyDriver other than loading of driver classes. + */ +public class TracingProxyDriverTest extends DrillTest { + + private static Driver proxyDriver; + private static Connection proxyConnection; + + @BeforeClass + public static void setUpTestCase() throws SQLException, + ClassNotFoundException { + Class.forName( "org.apache.drill.jdbc.proxy.TracingProxyDriver" ); + proxyDriver = + DriverManager.getDriver( + "jdbc:proxy:org.apache.drill.jdbc.Driver:jdbc:drill:zk=local" ); + proxyConnection = + DriverManager.getConnection( "jdbc:proxy::jdbc:drill:zk=local" ); + } + + @AfterClass + public static void tearDownTestCase() { + } + + @Test + public void testBasicProxying() throws SQLException { + try ( final Statement stmt = proxyConnection.createStatement() ) { + final ResultSet rs = + stmt.executeQuery( "SELECT * FROM INFORMATION_SCHEMA.CATALOGS" ); + assertTrue( rs.next() ); + assertThat( rs.getString( 1 ), equalTo( "DRILL" ) ); + } + } + + private static class StdErrCapturer { + private final PrintStream savedStdErr; + private final ByteArrayOutputStream buffer = new ByteArrayOutputStream(); + private final PrintStream capturingStream = new PrintStream( buffer ); + private boolean redirected; + + StdErrCapturer() { + savedStdErr = System.err; + } + + void redirect() { + assert ! redirected; + redirected = true; + System.setErr( capturingStream ); + } + + void unredirect() { + assert redirected; + redirected = false; + System.setErr( savedStdErr ); + } + + String getOutput() { + assert ! redirected; + return new String( buffer.toByteArray(), StandardCharsets.UTF_8 ); + } + } + + + @Test + public void testBasicReturnTrace() throws SQLException { + final StdErrCapturer nameThis = new StdErrCapturer(); + + try { + nameThis.redirect(); + proxyConnection.isClosed(); + } + finally { + nameThis.unredirect(); + } + + // Check captured System.err: + + final String output = nameThis.getOutput(); + final String[] lines = output.split( "\n" ); + assertThat( "Not 2 lines: \"\"\"" + output + "\"\"\"", + lines.length, equalTo( 2 ) ); + final String callLine = lines[ 0 ]; + final String returnLine = lines[ 1 ]; + + // Expect something like current: + // TRACER: CALL: ((Connection) <id=3> ...) . isClosed() + // TRACER: RETURN: ((Connection) <id=3> ...) . isClosed(), RESULT: (boolean) false + + assertThat( callLine, containsString( " CALL:" ) ); + assertThat( returnLine, containsString( " RETURN:" ) ); + assertThat( callLine, containsString( "(Connection)" ) ); + assertThat( returnLine, containsString( "(Connection)" ) ); + assertThat( callLine, containsString( "isClosed()" ) ); + assertThat( returnLine, containsString( "isClosed()" ) ); + assertThat( callLine, not( containsString( " (boolean) " ) ) ); + assertThat( returnLine, containsString( " (boolean) " ) ); + assertThat( callLine, not( containsString( "false" ) ) ); + assertThat( returnLine, containsString( "false" ) ); + } + + @Test + public void testBasicThrowTrace() throws SQLException { + final StdErrCapturer stdErrCapturer = new StdErrCapturer(); + + final Statement statement = proxyConnection.createStatement(); + statement.close(); + + try { + stdErrCapturer.redirect(); + statement.execute( "" ); + } + catch ( final SQLException e ) { + // "already closed" is expected + } + finally { + stdErrCapturer.unredirect(); + } + + // Check captured System.err: + + final String output = stdErrCapturer.getOutput(); + final String[] lines = output.split( "\n" ); + assertThat( "Not 2 lines: \"\"\"" + output + "\"\"\"", + lines.length, equalTo( 2 ) ); + final String callLine = lines[ 0 ]; + final String returnLine = lines[ 1 ]; + + // Expect something like current: + // TRACER: CALL: ((Statement) <id=6> ...) . execute( (String) "" ) + // TRACER: THROW: ((Statement) <id=6> ...) . execute( (String) "" ), th\ + // rew: (org.apache.drill.jdbc.AlreadyClosedSqlException) org.apache.dri\ + // ll.jdbc.AlreadyClosedSqlException: Statement is already closed. + + assertThat( callLine, containsString( " CALL:" ) ); + assertThat( returnLine, containsString( " THROW:" ) ); + assertThat( callLine, containsString( "(Statement)" ) ); + assertThat( returnLine, containsString( "(Statement)" ) ); + assertThat( callLine, containsString( "execute(" ) ); + assertThat( returnLine, containsString( "execute(" ) ); + assertThat( callLine, not( containsString( "threw:" ) ) ); + assertThat( returnLine, containsString( "threw:" ) ); + assertThat( callLine, not( anyOf( containsString( "exception" ), + containsString( "Exception" ) ) ) ); + assertThat( returnLine, anyOf( containsString( "exception" ), + containsString( "Exception" ) ) ); + assertThat( callLine, not( anyOf( containsString( "closed" ), + containsString( "Closed" ) ) ) ); + assertThat( returnLine, anyOf( containsString( "closed" ), + containsString( "Closed" ) ) ); + } + + // TODO: Clean up these assorted remnants; probably move into separate test + // methods. + @Test + public void testUnsortedMethods() throws SQLException { + + // Exercise these, even though we don't check results. + proxyDriver.getMajorVersion(); + proxyDriver.getMinorVersion(); + proxyDriver.jdbcCompliant(); + proxyDriver.getParentLogger(); + proxyDriver.getPropertyInfo( "jdbc:proxy::jdbc:drill:zk=local", new Properties() ); + + final DatabaseMetaData dbMetaData = proxyConnection.getMetaData(); + assertThat( dbMetaData, instanceOf( DatabaseMetaData.class ) ); + assertThat( dbMetaData, notNullValue() ); + + assertThat( dbMetaData.getConnection(), sameInstance( proxyConnection ) ); + + + dbMetaData.allTablesAreSelectable(); + try { + dbMetaData.ownUpdatesAreVisible( ResultSet.TYPE_FORWARD_ONLY ); + fail(); + } + catch ( SQLException | RuntimeException e ) { + // expected + } + + + final ResultSet catalogsResultSet = dbMetaData.getCatalogs(); + assertThat( catalogsResultSet, notNullValue() ); + assertThat( catalogsResultSet, instanceOf( ResultSet.class ) ); + + catalogsResultSet.next(); + catalogsResultSet.getString( 1 ); + catalogsResultSet.getObject( 1 ); + + final ResultSetMetaData rsMetaData = catalogsResultSet.getMetaData(); + assertThat( rsMetaData, notNullValue() ); + assertThat( rsMetaData, instanceOf( ResultSetMetaData.class ) ); + + int colCount = rsMetaData.getColumnCount(); + for ( int cx = 1; cx <= colCount; cx++ ) { + catalogsResultSet.getObject( cx ); + catalogsResultSet.getString( cx ); + try { + catalogsResultSet.getInt( cx ); + fail( "Expected some kind of string-to-int exception."); + } + catch ( SQLException e ) { + // expected; + } + + } + + assertThat( proxyConnection.getMetaData(), sameInstance( dbMetaData ) ); + assertThat( catalogsResultSet.getMetaData(), sameInstance( rsMetaData ) ); + } + +} // class ProxyDriverTest