001 /* 002 * Copyright 2008-2009 the original author or authors. 003 * The contents of this file are subject to the Mozilla Public License 004 * Version 1.1 (the "License"); you may not use this file except in 005 * compliance with the License. You may obtain a copy of the License at 006 * http://www.mozilla.org/MPL/ 007 * 008 * Software distributed under the License is distributed on an "AS IS" 009 * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the 010 * License for the specific language governing rights and limitations 011 * under the License. 012 */ 013 014 package com.mtgi.analytics.sql; 015 016 import java.lang.reflect.InvocationHandler; 017 import java.lang.reflect.InvocationTargetException; 018 import java.lang.reflect.Method; 019 import java.lang.reflect.Proxy; 020 import java.sql.Connection; 021 import java.sql.PreparedStatement; 022 import java.sql.SQLException; 023 import java.sql.Statement; 024 025 import org.springframework.beans.factory.annotation.Required; 026 import org.springframework.jdbc.datasource.ConnectionProxy; 027 import org.springframework.jdbc.datasource.DelegatingDataSource; 028 029 import com.mtgi.analytics.BehaviorEvent; 030 import com.mtgi.analytics.BehaviorTrackingManager; 031 import com.mtgi.analytics.EventDataElement; 032 033 /** 034 * A datasource which adds SQL event logging to the behavior tracking database. Events are persisted 035 * to the required {@link #setTrackingManager(BehaviorTrackingManager) BehaviorTrackingManager}. Events 036 * are of type "jdbc" unless overridden with a call to {@link #setEventType(String)}. Event names are the 037 * Statement API call that executed the SQL (e.g. "execute", "executeQuery", "executeUpdate"), with event 038 * data containing the exact SQL and parameter values logged. 039 */ 040 public class BehaviorTrackingDataSource extends DelegatingDataSource { 041 042 private static Class<?>[] PROXY_TYPE = { BehaviorTrackingConnectionProxy.class }; 043 044 private String eventType = "jdbc"; 045 private BehaviorTrackingManager trackingManager; 046 047 public void setEventType(String eventType) { 048 this.eventType = eventType; 049 } 050 051 @Required 052 public void setTrackingManager(BehaviorTrackingManager trackingManager) { 053 this.trackingManager = trackingManager; 054 } 055 056 @Override 057 public Connection getConnection() throws SQLException { 058 Connection target = getTargetDataSource().getConnection(); 059 return (Connection)Proxy.newProxyInstance( 060 BehaviorTrackingDataSource.class.getClassLoader(), 061 PROXY_TYPE, 062 new ConnectionHandler(target)); 063 } 064 065 @Override 066 public Connection getConnection(String username, String password) throws SQLException { 067 Connection target = getTargetDataSource().getConnection(username, password); 068 return (Connection)Proxy.newProxyInstance( 069 BehaviorTrackingDataSource.class.getClassLoader(), 070 PROXY_TYPE, 071 new ConnectionHandler(target)); 072 } 073 074 private static final String findSqlArg(Object[] args) { 075 if (args != null && args.length > 0 && args[0] instanceof String) 076 return (String)args[0]; 077 return null; 078 } 079 080 /** base class for proxy invocation handlers, which provides a typical implementation for "equals" and "hashcode" */ 081 protected static abstract class HandlerStub implements InvocationHandler { 082 083 protected Object target; 084 085 public HandlerStub(Object target) { 086 this.target = target; 087 } 088 089 /** 090 * Standard implementation of equals / hashCode for proxy handlers. Returns a non-null result if 091 * <code>method</code> is an identity check that can be handled here; null otherwise. 092 */ 093 protected final Object invokeIdentity(Object proxy, String op, Object[] args) throws Throwable { 094 if (op.equals("equals")) { 095 return (proxy == args[0] ? Boolean.TRUE : Boolean.FALSE); 096 } else if (op.equals("hashCode")) { 097 return new Integer(hashCode()); 098 } 099 return null; 100 } 101 102 /** 103 * Invoke <code>method</code> with <code>args</code> on the delegate 104 * object for this proxy. If the method invocation throws an InvocationTargetException, 105 * throws the original application exception instead (generally more desirable 106 * for a proxy). 107 * @return the value returned by the delegate 108 * @throws any exception thrown trying to invoke the method. 109 */ 110 protected final Object invokeTarget(Method method, Object[] args) 111 throws Throwable 112 { 113 try { 114 return method.invoke(target, args); 115 } catch (InvocationTargetException ite) { 116 throw ite.getTargetException(); 117 } catch (Throwable t) { 118 throw t; 119 } 120 } 121 } 122 123 /** 124 * Delegates all method calls to a target connection, wrapping returned Statement instances 125 * with behavior tracking instrumentation. 126 */ 127 protected class ConnectionHandler extends HandlerStub { 128 129 private boolean suspended; 130 131 protected ConnectionHandler(Connection target) { 132 super(target); 133 } 134 135 public boolean isSuspended() { 136 return suspended; 137 } 138 139 public final Object invoke(Object proxy, Method method, Object[] args) throws Throwable { 140 //handle ConnectionProxy.getTargetConnection() for use by Spring. 141 String op = method.getName(); 142 Class<?> deClass = method.getDeclaringClass(); 143 if (ConnectionProxy.class == deClass && "getTargetConnection".equals(op)) 144 return target; 145 146 //handle suspend / resume of event generation, for the benefit of jdbc persister 147 //implementations that don't want to add a bunch of noise to the event log. 148 if (BehaviorTrackingConnectionProxy.class == deClass) { 149 if ("suspendTracking".equals(op)) { 150 suspended = true; 151 } else if ("resumeTracking".equals(op)) { 152 suspended = false; 153 } 154 return null; 155 } 156 157 //equals & hashCode handling. 158 Object stub = invokeIdentity(proxy, op, args); 159 if (stub != null) 160 return stub; 161 162 //all other calls are delegated to the target connection. 163 Object ret = invokeTarget(method, args); 164 165 //if the return value is a statement, wrap the statement for behavior tracking. 166 Class<?> type = method.getReturnType(); 167 if (PreparedStatement.class.isAssignableFrom(type)) { 168 //for prepared statement, the SQL is provided when the statement is created. 169 String sql = findSqlArg(args); 170 //for other statements we get the exact SQL when the statement is executed. 171 ret = Proxy.newProxyInstance(BehaviorTrackingDataSource.class.getClassLoader(), 172 new Class[]{ type }, new PreparedStatementHandler(this, ret, sql)); 173 } else if (Statement.class.isAssignableFrom(type)) { 174 //for other statements we get the exact SQL when the statement is executed. 175 ret = Proxy.newProxyInstance(BehaviorTrackingDataSource.class.getClassLoader(), 176 new Class[]{ type }, new DynamicStatementHandler(this, ret)); 177 } 178 179 return ret; 180 } 181 182 } 183 184 /** Base invocation handler for instrumenting Statement objects with behavior tracking events. */ 185 protected abstract class StatementHandler extends HandlerStub { 186 187 private ConnectionHandler parent; 188 private EventDataElement batch; 189 190 public StatementHandler(ConnectionHandler parent, Object target) { 191 super(target); 192 this.parent = parent; 193 } 194 195 /** 196 * notification that a statement has been added to the current batch. Subclasses must implement this method 197 * to add any useful parameter info to <code>batchData</code>. 198 */ 199 protected abstract void addBatch(EventDataElement batchData, Object[] args); 200 /** 201 * notification that a non-batch statement has been executed. Subclasses 202 * must implement this method to add any useful parameter data to <code>event</code>. 203 */ 204 protected abstract void addExecuteParameters(BehaviorEvent event, Object[] args); 205 206 /** 207 * Intercept an event call on the underlying statement object. 208 * If the method represents a statement execution, a behavior tracking event will 209 * be recorded, including any event data gathered from preceding calls to 210 * {@link #addBatch(EventDataElement, Object[])}, {@link #addExecuteParameters(BehaviorEvent, Object[])}, 211 * and {@link #addOperationData(String, Object[])}. 212 */ 213 public final Object invoke(Object proxy, Method method, Object[] args) throws Throwable { 214 String op = method.getName(); 215 Object stub = invokeIdentity(proxy, op, args); 216 if (stub != null) 217 return stub; 218 219 //only bother with the event if tracking is enabled on the parent connection 220 if (!parent.isSuspended()) { 221 222 if (op.startsWith("execute")) { 223 224 BehaviorEvent event = createEvent(op); 225 if (op.endsWith("Batch")) { 226 //consolidate batch call execution data into root data element. 227 if (batch != null) { 228 event.addData().addElement(batch); 229 batch = null; 230 } 231 } else { 232 addExecuteParameters(event, args); 233 } 234 235 //query or batch is being executed -- start the event timer. 236 trackingManager.start(event); 237 try { 238 return invokeTarget(method, args); 239 } catch (Throwable t) { 240 event.setError(t); 241 throw t; 242 } finally { 243 trackingManager.stop(event); 244 } 245 246 } else if (op.equals("addBatch")) { 247 //statement is being rolled up into a batch for execution, 248 //add parameter and sql info to event data. 249 if (batch == null) 250 batch = new EventDataElement("batch"); 251 addBatch(batch, args); 252 } else { 253 addOperationData(op, args); 254 } 255 } 256 257 return invokeTarget(method, args); 258 } 259 260 /** 261 * Hook for subclasses to extract any data from a Statement method call that is 262 * not an execution. E.g. prepared statements receive parameter data from 263 * various setXX() calls. Default behavior does nothing. 264 */ 265 protected void addOperationData(String op, Object[] args) { 266 } 267 268 /** 269 * Create, but do not start, a new behavior tracking event for the given execute method name. 270 * Simply calls {@link BehaviorTrackingManager#createEvent(String, String)}. 271 */ 272 protected BehaviorEvent createEvent(String name) { 273 //initialize statement event with the accumulated parameter and SQL information. 274 return trackingManager.createEvent(eventType, name); 275 } 276 } 277 278 /** 279 * Behavior tracking logic for prepared and callable statements. 280 */ 281 protected class PreparedStatementHandler extends StatementHandler { 282 283 private String sql; 284 private EventDataElement parameters = new EventDataElement("parameters"); 285 286 public PreparedStatementHandler(ConnectionHandler parent, Object target, String sql) { 287 super(parent, target); 288 this.sql = sql; 289 } 290 291 /** overridden to append the prepared statement SQL to the newly created event */ 292 @Override 293 protected BehaviorEvent createEvent(String name) { 294 BehaviorEvent ret = trackingManager.createEvent(eventType, name); 295 ret.addData().addElement("sql").setText(sql); 296 return ret; 297 } 298 299 /** overridden to add prepared statement parameter data to the batch data element */ 300 @Override 301 protected void addBatch(EventDataElement batchData, Object[] args) { 302 //prepared statement batch. add any parameters to 303 //event info and reset for next statement. 304 batchData.addElement(parameters); 305 parameters = new EventDataElement("parameters"); 306 } 307 308 /** overridden to add prepared statement parameter data to the execute event */ 309 @Override 310 protected void addExecuteParameters(BehaviorEvent event, Object[] args) { 311 if (!parameters.isEmpty()) { 312 //transfer parameters from buffer into event object. 313 event.addData().addElement(parameters); 314 //clear out the parameter buffer for the next execute event. 315 parameters = new EventDataElement("parameters"); 316 } 317 } 318 319 /** 320 * Overridden to read any prepared statement parameter info out of the given method call data, 321 * for inclusion in the next {@link #addBatch(EventDataElement, Object[])} or {@link #addExecuteParameters(BehaviorEvent, Object[])} 322 * call. 323 */ 324 @Override 325 protected void addOperationData(String op, Object[] args) { 326 //maybe parameters being set? store up parameters in 'parameters' element until we start another execute event or batch statement. 327 //we have to support multiple execute() calls on the same statement object to support prepared / callable API 328 if (op.startsWith("set")) { 329 Object key = null; 330 Object value = null; 331 if (op.equals("setNull")) { 332 key = args[0]; 333 } else if (args.length == 2) { 334 if (!(op.endsWith("Stream") || op.endsWith("lob"))) { 335 key = args[0]; 336 value = args[1]; 337 } 338 } else if (args.length == 3 && ("setObject".equals(op) || "setDate".equals(op))) { 339 key = args[0]; 340 value = args[1]; 341 } 342 343 if (key != null) { 344 EventDataElement v = parameters.addElement("param"); 345 if (value != null) 346 v.setText(value.toString()); 347 } 348 } 349 } 350 } 351 352 /** 353 * Behavior tracking logic for dynamic (not prepared or callable) sql statements. 354 */ 355 protected class DynamicStatementHandler extends StatementHandler { 356 357 public DynamicStatementHandler(ConnectionHandler parent, Object target) { 358 super(parent, target); 359 } 360 361 /** overridden to add the static sql from <code>args</code> to the event data */ 362 @Override 363 protected void addExecuteParameters(BehaviorEvent event, Object[] args) { 364 event.addData().addElement("sql").setText(findSqlArg(args)); 365 } 366 367 /** overridden to add the static sql from <code>args</code> to the event data */ 368 @Override 369 protected void addBatch(EventDataElement batchData, Object[] args) { 370 //static SQL batch. add an element to contain SQL. 371 batchData.addElement("sql").setText(findSqlArg(args)); 372 } 373 } 374 375 }