Java tutorial
/* * This file is a component of 3wks Analytics, a software library from 3wks. * Copyright (C) 2013 3wks, <support@3wks.com.au> * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.threewks.analytics.filter; import java.io.IOException; import java.util.List; import java.util.Map; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import java.util.logging.Logger; import javax.servlet.Filter; import javax.servlet.FilterChain; import javax.servlet.FilterConfig; import javax.servlet.ServletException; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.apache.commons.lang3.StringUtils; import com.threewks.analytics.client.Analytics; import com.threewks.analytics.client.AnalyticsClient; import com.threewks.analytics.model.HttpCookie; import com.threewks.analytics.model.HttpHeader; import com.threewks.analytics.model.HttpRequest; import com.threewks.analytics.model.HttpResponse; /** * <p> * Servlet filter for capturing HTTP request/response data and posting it to 3wks-analytics. * <p> * * <p> * There are a two options for configuring this filter. * <p> * * <p> * <strong>Option 1</strong> - a single API key. * </p> * * <pre> * <filter> * <filter-name>HttpRequestResponseTrackingFilter</filter-name> * <filter-class>com.threewks.analytics.filter.HttpRequestResponseTrackingFilter</filter-class> * <init-param> * <param-name>serviceUrl</param-name> * <param-value>http://analytics.3wks.com</param-value> * </init-param> * <init-param> * <param-name>apiKey</param-name> * <param-value>your-api-key</param-value> * </init-param> * </filter> * </pre> * * <p> * <strong>Option 2</strong> - environment specific API keys. This option allows you to set a system property that defines the current environment and use that to prefix the API key property. * </p> * * <pre> * <filter> * <filter-name>HttpRequestResponseTrackingFilter</filter-name> * <filter-class>com.threewks.analytics.filter.HttpRequestResponseTrackingFilter</filter-class> * <init-param> * <param-name>serviceUrl</param-name> * <param-value>http://analytics.3wks.com</param-value> * </init-param> * <init-param> * <param-name>environmentProperty</param-name> * <param-value>ENV</param-value> * </init-param> * <init-param> * <param-name>dev_apiKey</param-name> * <param-value>your-development-api-key</param-value> * </init-param> * <init-param> * <param-name>prd_apiKey</param-name> * <param-value>your-production-api-key</param-value> * </init-param> * </filter> * </pre> * * <p> * Additional initialisation parameters that can be set are: * <ul> * <li>threadPoolSize - (default: 10) defines how many threads to use for asynchronously sending requests to the server.</li> * </ul> * </p> * * <p> * To configure the filter to capture all requests, add the following filter mapping: * </p> * * <pre> * <filter-mapping> * <filter-name>HttpRequestResponseTrackingFilter</filter-name> * <url-pattern>/*</url-pattern> * </filter-mapping> * </pre> */ public class HttpRequestResponseTrackingFilter implements Filter { private static final Logger logger = Logger.getLogger("3wks-analytics"); private static final String SERVICE_URL_PARAM = "serviceUrl"; private static final String API_KEY_PARAM = "apiKey"; private static final String THREAD_POOL_SIZE_PARAM = "threadPoolSize"; private static final String ENVIRONMENT_PROPERTY = "environmentProperty"; private static final String DEFAULT_ENVIRONMENT = "dev"; private static final int DEFAULT_THREAD_POOL_SIZE = 10; private Analytics analytics; private ExecutorService threadPool; @Override public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { final HttpRequest httpRequest = new HttpRequest((HttpServletRequest) req); trackRequestAsync(httpRequest); // wrap the response so we can capture some additional information from it (headers, cookies etc) AnalyticsHttpServletResponseWrapper responseWrapper = new AnalyticsHttpServletResponseWrapper( (HttpServletResponse) res); chain.doFilter(req, responseWrapper); final HttpResponse httpResponse = createHttpResponse(httpRequest, responseWrapper); trackResponseAsync(httpResponse); } @Override public void init(FilterConfig config) throws ServletException { String serviceUrl = getMandatoryInitParam(config, SERVICE_URL_PARAM); String threadPoolSize = config.getInitParameter(THREAD_POOL_SIZE_PARAM); String apiKey = getApiKey(config); analytics = new AnalyticsClient(apiKey, serviceUrl); createThreadPool(threadPoolSize != null ? Integer.valueOf(threadPoolSize) : DEFAULT_THREAD_POOL_SIZE); } /** * <p> * Get the API key to use to connect to the analytics service. The follow strategies are used (in order): * <ol> * <li>Look for a single 'apiKey' init param.</li> * <li>Look for an init param called 'environmentProperty'. Using the value of this property look for a system property with the same name. Finally, look for an init param called * '<environment>_apiKey'. Note that value of this property is case sensitive. If no system property can be found the environment is assumed to be 'dev'.</li> * </ol> * </p> * * <p> * To implement different logic to obtain the apiKey (eg: from a database) subclass this filter and override this method. * </p> * * @param config the {@link FilterConfig} to get the parameters from. * @return the API key to use to connect to the analytics service. * @throws ServletException if no API key could be obtained. */ public String getApiKey(FilterConfig config) throws ServletException { /* * Option 1 - single 'apiKey' init param */ String apiKey = config.getInitParameter(API_KEY_PARAM); if (apiKey != null) { return apiKey; } /* * Option 2 - environment specific API key combined with an environment system property. */ String environmentProperty = getMandatoryInitParam(config, ENVIRONMENT_PROPERTY); String environment = System.getProperty(environmentProperty, DEFAULT_ENVIRONMENT); return getMandatoryInitParam(config, String.format("%s_%s", environment, API_KEY_PARAM)); } @Override public void destroy() { threadPool.shutdown(); try { if (!threadPool.awaitTermination(60, TimeUnit.SECONDS)) { logger.severe("Thread pool did not shut down after 60 seconds, forcefully shutting down now."); threadPool.shutdownNow(); } } catch (InterruptedException e) { threadPool.shutdownNow(); } } void trackRequestAsync(final HttpRequest httpRequest) { threadPool.execute(new Runnable() { @Override public void run() { analytics.trackRequest(httpRequest); } }); } void trackResponseAsync(final HttpResponse httpResponse) { threadPool.execute(new Runnable() { @Override public void run() { analytics.trackResponse(httpResponse); } }); } /** * Create a fixed thread pool of the given size. * * @param size the size of the thread pool. */ void createThreadPool(int size) { threadPool = Executors.newFixedThreadPool(size); } /** * Get a mandatory initialization parameter from the filter configuration. * * @param config the filter configuration to get the parameter from. * @param paramName the name of the parameter. * @return the parameter value. * @throws ServletException if the value of the parameter is blank. */ private String getMandatoryInitParam(FilterConfig config, String paramName) throws ServletException { String value = config.getInitParameter(paramName); if (StringUtils.isBlank(value)) { throw new ServletException(String.format("Missing mandatory init param '%s' for %s", paramName, this.getClass().getCanonicalName())); } else { return value; } } /** * Create an {@link HttpResponse} from the given {@link HttpRequest} and {@link AnalyticsHttpServletResponseWrapper}. * * @param httpRequest the request object. * @param responseWrapper the response wrapper. * @return the response. */ private static HttpResponse createHttpResponse(HttpRequest httpRequest, AnalyticsHttpServletResponseWrapper responseWrapper) { HttpResponse httpResponse = new HttpResponse(); httpResponse.setCharacterEncoding(responseWrapper.getCharacterEncoding()); httpResponse.setContentType(responseWrapper.getContentType()); httpResponse.setRedirectUrl(responseWrapper.getRedirectUrl()); httpResponse.setStatusCode(responseWrapper.getStatusCode()); List<HttpCookie> cookies = httpResponse.getCookies(); for (Cookie cookie : responseWrapper.getCookies()) { cookies.add(new HttpCookie(cookie)); } List<HttpHeader> headers = httpResponse.getHeaders(); Map<String, List<String>> headers2 = responseWrapper.getHeaders(); for (Map.Entry<String, List<String>> header : headers2.entrySet()) { String name = header.getKey(); List<String> values = header.getValue(); headers.add(new HttpHeader(name, values.size() > 1 ? values.toString() : values.get(0))); } // link the id back to the request httpResponse.setId(httpRequest.getId()); return httpResponse; } }