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.aop;
015    
016    import java.lang.reflect.Array;
017    import java.lang.reflect.Method;
018    
019    import org.aopalliance.intercept.MethodInterceptor;
020    import org.aopalliance.intercept.MethodInvocation;
021    import org.springframework.beans.factory.annotation.Required;
022    
023    import com.mtgi.analytics.BehaviorEvent;
024    import com.mtgi.analytics.BehaviorTrackingManager;
025    import com.mtgi.analytics.EventDataElement;
026    
027    /**
028     * Aspect-Oriented "around" advisor that logs method invocations
029     * to a backing instance of {@link BehaviorTrackingManager}.  Method
030     * parameters and result value are included in the event data.
031     * The eventType attribute of generated events is set to <code>method</code>
032     * unless overridden with {@link #setEventType(String)}.
033     */
034    public class BehaviorTrackingAdvice implements MethodInterceptor {
035    
036            private String eventType = "method";
037            private BehaviorTrackingManager trackingManager;
038            
039            private String application;
040            
041            public String getApplication() {
042                    return application;
043            }
044    
045            @Required
046            public void setApplication(String application) {
047                    this.application = application;
048            }
049    
050            public void setEventType(String eventType) {
051                    this.eventType = eventType;
052            }
053    
054            @Required
055            public void setTrackingManager(BehaviorTrackingManager manager) {
056                    this.trackingManager = manager;
057            }
058            
059            public BehaviorTrackingManager getTrackingManager() {
060                    return trackingManager;
061            }
062            
063    
064            public Object invoke(MethodInvocation invocation) throws Throwable {
065    
066                    Method m = invocation.getMethod();
067                    String eventName = m.getDeclaringClass().getName() + "." + m.getName();
068    
069                    BehaviorEvent event = trackingManager.createEvent(eventType, eventName);
070    
071                    //log method parameters.  would be nice if we could figure
072                    //out parameter names here.
073                    EventDataElement data = event.addData();
074                    EventDataElement parameters = data.addElement("parameters");
075                    Class<?>[] sig = m.getParameterTypes();
076                    Object[] args = invocation.getArguments();
077                    
078                    for (int i = 0; i < sig.length; ++i) {
079                            Object val = args[i];
080                            logValue(parameters, "param", sig[i], val);
081                    }
082                    
083                    trackingManager.start(event);
084                    try {
085                            Object ret = invocation.proceed();
086                            logValue(data, "result", m.getReturnType(), ret);
087                            return ret;
088                    } catch (Throwable error) {
089                            event.setError(error);
090                            throw error;
091                    } finally {
092                            trackingManager.stop(event);
093                    }
094            }
095            
096            /**
097             * marshal the given value to a String.  Only java builtin types,
098             * and arrays of those types, are converted; everything else is
099             * represented as "[qualified.type.name]" to avoid invoking
100             * costly (or buggy) toString() methods.
101             */
102            private static final void logValue(EventDataElement parent, String elementName, Class<?> expectedType, Object arg) {
103                    EventDataElement element = parent.addElement(elementName);
104                    if (arg == null)
105                            return;
106                    
107                    Class<?> type = arg.getClass();
108                    //log the concrete type of the argument if it differs from the expected type (i.e. is a subclass)
109                    //the primitive type checks avoid logging redundant type info for autoboxed values
110                    if (type != expectedType && !(expectedType.isPrimitive() || type.isPrimitive()))
111                            element.add("type", type.getName());
112                    
113                    //TODO: use annotations or some other configuration for custom
114                    //parameter logging?
115                    String value = "{object}";
116                    if (type.isArray()) {
117                            if (shouldLog(type.getComponentType()))
118                                    value = toStringArray(arg);
119                    } else {
120                            if (shouldLog(type))
121                                    value = arg.toString();
122                    }
123                    element.setText(value);
124            }
125    
126            protected static final String toStringArray(Object array) {
127                    StringBuffer ret = new StringBuffer("[");
128                    int len = Array.getLength(array);
129                    int maxLen = Math.min(len, 100);
130                    for (int i = 0; i < maxLen; ++i) {
131                            if (i > 0)
132                                    ret.append(", ");
133                            ret.append(String.valueOf(Array.get(array, i)));
134                    }
135                    if (maxLen < len)
136                            ret.append(", ... (").append(len - maxLen).append(" more)");
137                    ret.append("]");
138                    return ret.toString();
139            }
140            
141            private static final boolean shouldLog(Class<?> type) {
142                    return (type.isPrimitive()) || type.isEnum() || type.getName().startsWith("java.lang");
143            }
144            
145    }