/* * 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. * <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) and can be 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 an original URL of "{@code jdbc:drill:zk=local}", the * tracing URL is * "{@code jdbc:proxy:org.apache.drill.jdbc.Driver:jdbc:drill:zk=local}". * </p> * </li> * <li> * In the JDBC-client code or tool, replace the occurrence 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 it as the driver class). * </li> * </ul> * <p><strong>Output:</strong></p> * <p> * Currently, the tracing output lines 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