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 }