/* * 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 // Note: Using strings rather than compiled-in class references to // avoid failing when run using JDBC-all Jar, which excludes // org.apache.hadoop.io.Text. // Note: org.apache.hadoop.io.Text should no longer appear (see // DRILL-3347, but leaving warning in for now in case Text returns). || rawActualType.getName().equals( "org.apache.hadoop.io.Text" ) || rawActualType.getName().equals( "org.joda.time.Period" ) || 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