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.servlet;
015    
016    import static org.springframework.web.context.support.WebApplicationContextUtils.getRequiredWebApplicationContext;
017    
018    import java.io.IOException;
019    import java.util.Map;
020    
021    import javax.servlet.Filter;
022    import javax.servlet.FilterChain;
023    import javax.servlet.FilterConfig;
024    import javax.servlet.ServletContext;
025    import javax.servlet.ServletException;
026    import javax.servlet.ServletRequest;
027    import javax.servlet.ServletResponse;
028    import javax.servlet.http.HttpServletResponse;
029    import javax.servlet.http.HttpServletResponseWrapper;
030    
031    import org.springframework.web.context.WebApplicationContext;
032    
033    import com.mtgi.analytics.BehaviorEvent;
034    import com.mtgi.analytics.BehaviorTrackingManager;
035    import com.mtgi.analytics.EventDataElement;
036    
037    /**
038     * <p>A servlet filter which logs all activity to an instance of {@link BehaviorTrackingManager}
039     * in the application's Spring context.  All request parameters and any specific
040     * response status code is included in the event data.</p>
041     * 
042     * <p>If there is only one BehaviorTrackingManager in the Spring context, that
043     * instance is used automatically.  If there is more than one, which manager the
044     * filter should use is configured using the init parameter <code>com.mtgi.analytics.manager</code>.</p>
045     * 
046     * <p>By default all events generated by this filter will have a type of <code>http-request</code>.
047     * An alternate type value can be specified using the filter parameter
048     * <code>com.mtgi.analytics.servlet.event</code>.</p>
049     */
050    public class BehaviorTrackingFilter implements Filter {
051    
052            /** filter parameter specifying the bean name of the BehaviorTrackingManager instance to use in the application spring context. */
053            public static final String PARAM_MANAGER_NAME = "com.mtgi.analytics.manager";
054            /** filter parameter specifying the eventType value to use when logging behavior tracking events. */
055            public static final String PARAM_EVENT_TYPE = "com.mtgi.analytics.servlet.event";
056            /** filter parameter specifying a list of parameters to include in logging; defaults to all if unspecified */
057            public static final String PARAM_PARAMETERS_INCLUDE = "com.mtgi.analytics.parameters.include";
058    
059            public static final String ATT_FILTER_REGISTERED = BehaviorTrackingFilter.class.getName() + ".count";
060            
061            public static boolean isFiltered(ServletContext context) {
062                    Integer count = (Integer)context.getAttribute(ATT_FILTER_REGISTERED);
063                    return count != null && count > 0;
064            }
065            
066            private ServletContext servletContext;
067            private ServletRequestBehaviorTrackingAdapter delegate;
068            
069            public void destroy() {
070                    delegate = null;
071                    Integer count = (Integer)servletContext.getAttribute(ATT_FILTER_REGISTERED);
072                    if (count == null || count == 1)
073                            servletContext.removeAttribute(ATT_FILTER_REGISTERED);
074                    else
075                            servletContext.setAttribute(ATT_FILTER_REGISTERED, count - 1);
076            }
077    
078            public void init(FilterConfig config) throws ServletException {
079                    servletContext = config.getServletContext();
080                    WebApplicationContext context = getRequiredWebApplicationContext(servletContext);
081                    String managerName = config.getInitParameter(PARAM_MANAGER_NAME);
082                    
083                    BehaviorTrackingManager manager;
084                    if (managerName == null) {
085                            //if there is no bean name configured, we assume there
086                            //must be exactly one such bean in the application context.
087                            Map<?,?> managers = context.getBeansOfType(BehaviorTrackingManager.class);
088                            if (managers.isEmpty())
089                                    throw new ServletException("Unable to find a bean of class " + BehaviorTrackingManager.class.getName() + " in the Spring application context; perhaps it has not been configured?");
090                            if (managers.size() > 1)
091                                    throw new ServletException("More than one instance of " + BehaviorTrackingManager.class.getName() + " in Spring application context; you must specify which to use with the filter parameter " + PARAM_MANAGER_NAME);
092                            
093                            manager = (BehaviorTrackingManager)managers.values().iterator().next();
094                    } else {
095                            //lookup the specified bean name.
096                            manager = (BehaviorTrackingManager)context.getBean(managerName, BehaviorTrackingManager.class);
097                    }
098    
099                    //see if there is an event type name configured.
100                    String eventType = config.getInitParameter(PARAM_EVENT_TYPE);
101                    String params = config.getInitParameter(PARAM_PARAMETERS_INCLUDE);
102                    String[] parameters = params == null ? null : params.split("[\\r\\n\\s,;]+");
103    
104                    delegate = new ServletRequestBehaviorTrackingAdapter(eventType, manager, parameters, null);
105    
106                    //increment count of tracking filters registered in the servlet context.  the filter
107                    //and alternative request listener check this attribute to make sure both are not registered at once.
108                    Integer count = (Integer)servletContext.getAttribute(ATT_FILTER_REGISTERED);
109                    servletContext.setAttribute(ATT_FILTER_REGISTERED, count == null ? 1 : count + 1);
110            }
111    
112            public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
113                    //wrap the response so that we can intercept response status if the application
114                    //sets it.
115                    BehaviorTrackingResponse btr = new BehaviorTrackingResponse((HttpServletResponse)response);
116                    BehaviorEvent event = delegate.start(request);
117                    try {
118                            chain.doFilter(request, btr);
119                            
120                            //log response codes.
121                            EventDataElement data = event.getData();
122                            data.add("response-status", btr.status);
123                            data.add("response-message", btr.message);
124                            
125                            //if an error code is being sent back, populate the 'error' field of the event with relevant info.
126                            if (btr.status != null && btr.status >= 400)
127                                    event.setError(btr.status + ": " + btr.message);
128                            
129                    } catch (Throwable error) {
130                            //log exception messages to event data.
131                            handleServerError(event, error);
132                    } finally {
133                            delegate.stop(event);
134                    }
135            }
136            
137            private static final void handleServerError(BehaviorEvent event, Throwable e) throws ServletException, IOException {
138    
139                    event.addData().add("response-status", 500);
140                    
141                    if (e instanceof ServletException) {
142                            ServletException se = (ServletException)e;
143                            Throwable cause = se.getRootCause();
144                            if (cause != null)
145                                    event.setError(cause);
146                            else
147                                    event.setError(se);
148                            throw se;
149                    } else {
150                            event.setError(e);
151                    }
152                    
153                    //propagate exception
154                    if (e instanceof IOException)
155                            throw (IOException)e;
156                    if (e instanceof RuntimeException)
157                            throw (RuntimeException)e;
158                    //should not get this far in normal execution, but cover this case anyway..
159                    throw new ServletException(e);
160            }
161            
162            private static class BehaviorTrackingResponse extends HttpServletResponseWrapper {
163    
164                    Integer status;
165                    String message;
166                    
167                    protected BehaviorTrackingResponse(HttpServletResponse response) {
168                            super(response);
169                    }
170    
171                    @Override
172                    public void sendError(int status, String message) throws IOException {
173                            this.status = status;
174                            this.message = message;
175                            super.sendError(status, message);
176                    }
177    
178                    @Override
179                    public void sendError(int status) throws IOException {
180                            this.status = status;
181                            super.sendError(status);
182                    }
183    
184                    @Override
185                    public void setStatus(int status, String message) {
186                            this.status = status;
187                            this.message = message;
188                            super.setStatus(status, message);
189                    }
190    
191                    @Override
192                    public void setStatus(int status) {
193                            this.status = status;
194                            super.setStatus(status);
195                    }
196                    
197            }
198    }