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    }