com.threewks.analytics.filter.HttpRequestResponseTrackingFilter.java Source code

Java tutorial

Introduction

Here is the source code for com.threewks.analytics.filter.HttpRequestResponseTrackingFilter.java

Source

/*
 * 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>
 * &lt;filter&gt;
 *     &lt;filter-name&gt;HttpRequestResponseTrackingFilter&lt;/filter-name&gt;
 *     &lt;filter-class&gt;com.threewks.analytics.filter.HttpRequestResponseTrackingFilter&lt;/filter-class&gt;
 *     &lt;init-param&gt;
 *         &lt;param-name&gt;serviceUrl&lt;/param-name&gt;
 *         &lt;param-value&gt;http://analytics.3wks.com&lt;/param-value&gt;
 *     &lt;/init-param&gt;
 *     &lt;init-param&gt;
 *         &lt;param-name&gt;apiKey&lt;/param-name&gt;
 *         &lt;param-value&gt;your-api-key&lt;/param-value&gt;
 *     &lt;/init-param&gt;
 * &lt;/filter&gt;
 * </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>
 * &lt;filter&gt;
 *     &lt;filter-name&gt;HttpRequestResponseTrackingFilter&lt;/filter-name&gt;
 *     &lt;filter-class&gt;com.threewks.analytics.filter.HttpRequestResponseTrackingFilter&lt;/filter-class&gt;
 *     &lt;init-param&gt;
 *         &lt;param-name&gt;serviceUrl&lt;/param-name&gt;
 *         &lt;param-value&gt;http://analytics.3wks.com&lt;/param-value&gt;
 *     &lt;/init-param&gt;
 *     &lt;init-param&gt;
 *         &lt;param-name&gt;environmentProperty&lt;/param-name&gt;
 *         &lt;param-value&gt;ENV&lt;/param-value&gt;
 *     &lt;/init-param&gt;
 *     &lt;init-param&gt;
 *         &lt;param-name&gt;dev_apiKey&lt;/param-name&gt;
 *         &lt;param-value&gt;your-development-api-key&lt;/param-value&gt;
 *     &lt;/init-param&gt;
 *     &lt;init-param&gt;
 *         &lt;param-name&gt;prd_apiKey&lt;/param-name&gt;
 *         &lt;param-value&gt;your-production-api-key&lt;/param-value&gt;
 *     &lt;/init-param&gt;
 * &lt;/filter&gt;
 * </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>
 * &lt;filter-mapping&gt;
 *     &lt;filter-name&gt;HttpRequestResponseTrackingFilter&lt;/filter-name&gt;
 *     &lt;url-pattern&gt;/*&lt;/url-pattern&gt;
 * &lt;/filter-mapping&gt;
 * </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
     * '&lt;environment&gt;_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;
    }

}