All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.brsanthu.googleanalytics.GoogleAnalytics Maven / Gradle / Ivy

Go to download

This is Java API for Google Analytics (Measurement Protocol). More information about the protocol is available at https://developers.google.com/analytics/devguides/collection/protocol/v1/.

There is a newer version: 2.0.0
Show newest version
/*
 * 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.brsanthu.googleanalytics;

import static com.brsanthu.googleanalytics.GaUtils.isEmpty;
import static com.brsanthu.googleanalytics.GaUtils.isNotEmpty;

import java.io.IOException;
import java.net.UnknownHostException;
import java.nio.charset.Charset;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.Future;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

import org.apache.http.HttpHost;
import org.apache.http.NameValuePair;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.client.HttpClient;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.impl.client.BasicCredentialsProvider;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.util.EntityUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * This is the main class of this library that accepts the requests from clients and
 * sends the events to Google Analytics (GA).
 *
 * Clients needs to instantiate this object with {@link GoogleAnalyticsConfig} and {@link DefaultRequest}.
 * Configuration contains sensible defaults so one could just initialize using one of the convenience constructors.
 *
 * This object is ThreadSafe and it is intended that clients create one instance of this for each GA Tracker Id
 * and reuse each time an event needs to be posted.
 *
 * This object contains resources which needs to be shutdown/disposed. So {@link #close()} method is called
 * to release all resources. Once close method is called, this instance cannot be reused so create new instance
 * if required.
 */
public class GoogleAnalytics {

	private static final Charset UTF8 = Charset.forName("UTF-8");
	private static final Logger logger = LoggerFactory.getLogger(GoogleAnalytics.class);

	private GoogleAnalyticsConfig config = null;
	private DefaultRequest defaultRequest = null;
	private CloseableHttpClient httpClient = null;
	private ThreadPoolExecutor executor = null;
	private GoogleAnalyticsStats stats = new GoogleAnalyticsStats();

	public GoogleAnalytics(String trackingId) {
		this(new GoogleAnalyticsConfig(), new DefaultRequest().trackingId(trackingId));
	}

	public GoogleAnalytics(GoogleAnalyticsConfig config, String trackingId) {
		this(config, new DefaultRequest().trackingId(trackingId));
	}

	public GoogleAnalytics(String trackingId, String appName, String appVersion) {
		this(new GoogleAnalyticsConfig(), trackingId, appName, appVersion);
	}

	public GoogleAnalytics(GoogleAnalyticsConfig config, String trackingId, String appName, String appVersion) {
		this(config, new DefaultRequest().trackingId(trackingId).applicationName(appName).applicationVersion(appVersion));
	}

	public GoogleAnalytics(GoogleAnalyticsConfig config, DefaultRequest defaultRequest) {
		if (config.isDiscoverRequestParameters() && config.getRequestParameterDiscoverer() != null) {
			config.getRequestParameterDiscoverer().discoverParameters(config, defaultRequest);
		}

		logger.info("Initializing Google Analytics with config=" + config + " and defaultRequest=" + defaultRequest);

		this.config = config;
		this.defaultRequest = defaultRequest;
		this.httpClient = createHttpClient(config);
	}

	public GoogleAnalyticsConfig getConfig() {
		return config;
	}

	public HttpClient getHttpClient() {
		return httpClient;
	}

	public DefaultRequest getDefaultRequest() {
		return defaultRequest;
	}

	public void setDefaultRequest(DefaultRequest request) {
		this.defaultRequest = request;
	}

	public void setHttpClient(CloseableHttpClient httpClient) {
		this.httpClient = httpClient;
	}

	@SuppressWarnings({ "rawtypes" })
	public GoogleAnalyticsResponse post(GoogleAnalyticsRequest request) {
		GoogleAnalyticsResponse response = new GoogleAnalyticsResponse();
		if (!config.isEnabled()) {
			return response;
		}

		CloseableHttpResponse httpResponse = null;
		try {
			List postParms = new ArrayList();

			logger.debug("Processing " + request);

			//Process the parameters
			processParameters(request, postParms);

			//Process custom dimensions
			processCustomDimentionParameters(request, postParms);

			//Process custom metrics
			processCustomMetricParameters(request, postParms);
			
			logger.debug("Processed all parameters and sending the request " + postParms);
			
			HttpPost httpPost = new HttpPost(config.getUrl());
			httpPost.setEntity(new UrlEncodedFormEntity(postParms, UTF8));

			httpResponse = (CloseableHttpResponse) httpClient.execute(httpPost);
			response.setStatusCode(httpResponse.getStatusLine().getStatusCode());
			response.setPostedParms(postParms);
			
			EntityUtils.consumeQuietly(httpResponse.getEntity());

			if (config.isGatherStats()) {
				gatherStats(request);
			}

		} catch (Exception e) {
			if (e instanceof UnknownHostException) {
				logger.warn("Coudln't connect to Google Analytics. Internet may not be available. " + e.toString());
			} else {
				logger.warn("Exception while sending the Google Analytics tracker request " + request, e);
			}
		} finally {
			try {
				httpResponse.close();
			} catch (Exception e2) {
				//ignore
			}
		}

		return response;
	}

	@SuppressWarnings({ "rawtypes", "unchecked" })
	private void processParameters(GoogleAnalyticsRequest request, List postParms) {
		Map requestParms = request.getParameters();
		Map defaultParms = defaultRequest.getParameters();
		for (GoogleAnalyticsParameter parm : defaultParms.keySet()) {
			String value = requestParms.get(parm);
			String defaultValue = defaultParms.get(parm);
			if (isEmpty(value) && !isEmpty(defaultValue)) {
				requestParms.put(parm, defaultValue);
			}
		}
		for (GoogleAnalyticsParameter key : requestParms.keySet()) {
			postParms.add(new BasicNameValuePair(key.getParameterName(), requestParms.get(key)));
		}
	}
	
	/**
	 * Processes the custom dimentions and adds the values to list of parameters, which would be posted to GA.
	 * 
	 * @param request
	 * @param postParms
	 */
	private void processCustomDimentionParameters(@SuppressWarnings("rawtypes") GoogleAnalyticsRequest request, List postParms) {
		Map customDimParms = new HashMap();
		for (String defaultCustomDimKey : defaultRequest.customDimentions().keySet()) {
			customDimParms.put(defaultCustomDimKey, defaultRequest.customDimentions().get(defaultCustomDimKey));
		}

		@SuppressWarnings("unchecked")
		Map requestCustomDims = request.customDimentions();
		for (String requestCustomDimKey : requestCustomDims.keySet()) {
			customDimParms.put(requestCustomDimKey, requestCustomDims.get(requestCustomDimKey));
		}
		
		for (String key : customDimParms.keySet()) {
			postParms.add(new BasicNameValuePair(key, customDimParms.get(key)));
		}
	}

	/**
	 * Processes the custom metrics and adds the values to list of parameters, which would be posted to GA.
	 * 
	 * @param request
	 * @param postParms
	 */
	private void processCustomMetricParameters(@SuppressWarnings("rawtypes") GoogleAnalyticsRequest request, List postParms) {
		Map customMetricParms = new HashMap();
		for (String defaultCustomMetricKey : defaultRequest.custommMetrics().keySet()) {
			customMetricParms.put(defaultCustomMetricKey, defaultRequest.custommMetrics().get(defaultCustomMetricKey));
		}

		@SuppressWarnings("unchecked")
		Map requestCustomMetrics = request.custommMetrics();
		for (String requestCustomDimKey : requestCustomMetrics.keySet()) {
			customMetricParms.put(requestCustomDimKey, requestCustomMetrics.get(requestCustomDimKey));
		}
		
		for (String key : customMetricParms.keySet()) {
			postParms.add(new BasicNameValuePair(key, customMetricParms.get(key)));
		}
	}
	
	
	private void gatherStats(@SuppressWarnings("rawtypes") GoogleAnalyticsRequest request) {
		String hitType = request.hitType();

		if ("pageview".equalsIgnoreCase(hitType)) {
			stats.pageViewHit();

		} else if ("appview".equalsIgnoreCase(hitType)) {
			stats.appViewHit();

		} else if ("event".equalsIgnoreCase(hitType)) {
			stats.eventHit();

		} else if ("item".equalsIgnoreCase(hitType)) {
			stats.itemHit();

		} else if ("transaction".equalsIgnoreCase(hitType)) {
			stats.transactionHit();

		} else if ("social".equalsIgnoreCase(hitType)) {
			stats.socialHit();

		} else if ("timing".equalsIgnoreCase(hitType)) {
			stats.timingHit();
		}
	}

	public Future postAsync(final RequestProvider requestProvider) {
		if (!config.isEnabled()) {
			return null;
		}

		Future future = getExecutor().submit(new Callable() {
			public GoogleAnalyticsResponse call() throws Exception {
				try {
					@SuppressWarnings("rawtypes")
					GoogleAnalyticsRequest request = requestProvider.getRequest();
					if (request != null) {
						return post(request);
					}
				} catch (Exception e) {
					logger.warn("Request Provider (" + requestProvider +") thrown exception " + e.toString() + " and hence nothing is posted to GA.");
				}

				return null;
			}
		});
		return future;
	}

	@SuppressWarnings("rawtypes")
	public Future postAsync(final GoogleAnalyticsRequest request) {
		if (!config.isEnabled()) {
			return null;
		}

		Future future = getExecutor().submit(new Callable() {
			public GoogleAnalyticsResponse call() throws Exception {
				return post(request);
			}
		});
		return future;
	}

	public void close() {
		try {
			executor.shutdown();
		} catch (Exception e) {
			//ignore
		}

		try {
			httpClient.close();
		} catch (IOException e) {
			//ignore
		}
	}

	protected CloseableHttpClient createHttpClient(GoogleAnalyticsConfig config) {
		PoolingHttpClientConnectionManager connManager = new PoolingHttpClientConnectionManager();
		connManager.setDefaultMaxPerRoute(getDefaultMaxPerRoute(config));

		HttpClientBuilder builder = HttpClients.custom().setConnectionManager(connManager);

		if (isNotEmpty(config.getUserAgent())) {
			builder.setUserAgent(config.getUserAgent());
		}

		if (isNotEmpty(config.getProxyHost())) {
			builder.setProxy(new HttpHost(config.getProxyHost(), config.getProxyPort()));

			if (isNotEmpty(config.getProxyUserName())) {
				BasicCredentialsProvider credentialsProvider = new BasicCredentialsProvider();
				credentialsProvider.setCredentials(new AuthScope(config.getProxyHost(), config.getProxyPort()),
						new UsernamePasswordCredentials(config.getProxyUserName(), config.getProxyPassword()));
				builder.setDefaultCredentialsProvider(credentialsProvider);
			}
		}

		return builder.build();
	}

	protected int getDefaultMaxPerRoute(GoogleAnalyticsConfig config) {
		return Math.max(config.getMaxThreads(), 1);
	}

	protected ThreadPoolExecutor getExecutor() {
		if (executor == null) {
			executor = createExecutor(config);
		}
		return executor;
	}

	protected synchronized ThreadPoolExecutor createExecutor(GoogleAnalyticsConfig config) {
		return new ThreadPoolExecutor(0, config.getMaxThreads(), 5, TimeUnit.MINUTES, new LinkedBlockingDeque(), createThreadFactory());
	}

	protected ThreadFactory createThreadFactory() {
		return new GoogleAnalyticsThreadFactory(config.getThreadNameFormat());
	}

	public GoogleAnalyticsStats getStats() {
		return stats;
	}

	public void resetStats() {
		stats = new GoogleAnalyticsStats();
	}
}

class GoogleAnalyticsThreadFactory implements ThreadFactory {
    private final AtomicInteger threadNumber = new AtomicInteger(1);
    private String threadNameFormat = null;

    public GoogleAnalyticsThreadFactory(String threadNameFormat) {
    	this.threadNameFormat = threadNameFormat;
	}

	public Thread newThread(Runnable r) {
        Thread thread = new Thread(Thread.currentThread().getThreadGroup(), r, MessageFormat.format(threadNameFormat, threadNumber.getAndIncrement()), 0);
        thread.setDaemon(true);
        thread.setPriority(Thread.MIN_PRIORITY);
        return thread;
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy