io.keen.client.java.KeenClient Maven / Gradle / Ivy
Show all versions of keen-client-java-core Show documentation
package io.keen.client.java;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.StringReader;
import java.io.StringWriter;
import java.net.InetSocketAddress;
import java.net.MalformedURLException;
import java.net.Proxy;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import io.keen.client.java.exceptions.InvalidEventCollectionException;
import io.keen.client.java.exceptions.InvalidEventException;
import io.keen.client.java.exceptions.NoWriteKeyException;
import io.keen.client.java.exceptions.ServerException;
import io.keen.client.java.http.HttpHandler;
import io.keen.client.java.http.HttpMethods;
import io.keen.client.java.http.OutputSource;
import io.keen.client.java.http.Request;
import io.keen.client.java.http.Response;
import io.keen.client.java.http.UrlConnectionHttpHandler;
/**
*
* KeenClient provides all of the functionality required to:
*
*
*
* - Create events from map objects
* - Automatically insert properties into events as they are created
* - Post events to the Keen server, either one-at-a-time or in batches
* - Store events in between batch posts, if desired
* - Perform posts either synchronously or asynchronously
*
*
*
* To create a {@link KeenClient}, use a subclass of {@link io.keen.client.java.KeenClient.Builder}
* which provides the default interfaces for various operations (HTTP, JSON, queueing, async).
*
*
* @author dkador, klitwack
* @since 1.0.0
*/
public class KeenClient {
///// PUBLIC STATIC METHODS /////
/**
* Call this to retrieve the {@code KeenClient} singleton instance.
*
* @return The singleton instance of the client.
*/
public static KeenClient client() {
if (ClientSingleton.INSTANCE.client == null) {
throw new IllegalStateException("Please call KeenClient.initialize() before requesting the client.");
}
return ClientSingleton.INSTANCE.client;
}
/**
* Initializes the static Keen client. Only the first call to this method has any effect. All
* subsequent calls are ignored.
*
* @param client The {@link io.keen.client.java.KeenClient} implementation to use as the
* singleton client for the library.
*/
public static void initialize(KeenClient client) {
if (client == null) {
throw new IllegalArgumentException("Client must not be null");
}
if (ClientSingleton.INSTANCE.client != null) {
// Do nothing.
return;
}
ClientSingleton.INSTANCE.client = client;
}
/**
* Gets whether or not the singleton KeenClient has been initialized.
*
* @return {@code true} if and only if the client has been initialized.
*/
public static boolean isInitialized() {
return (ClientSingleton.INSTANCE.client != null);
}
///// PUBLIC METHODS //////
/**
* Adds an event to the default project with default Keen properties and no callbacks.
*
* @see #addEvent(KeenProject, String, java.util.Map, java.util.Map, KeenCallback)
* @param eventCollection The name of the collection in which to publish the event.
* @param event A Map that consists of key/value pairs. Keen naming conventions apply (see
* docs). Nested Maps and lists are acceptable (and encouraged!).
*/
public void addEvent(String eventCollection, Map event) {
addEvent(eventCollection, event, null);
}
/**
* Adds an event to the default project with no callbacks.
*
* @see #addEvent(KeenProject, String, java.util.Map, java.util.Map, KeenCallback)
* @param eventCollection The name of the collection in which to publish the event.
* @param event A Map that consists of key/value pairs. Keen naming conventions apply (see
* docs). Nested Maps and lists are acceptable (and encouraged!).
* @param keenProperties A Map that consists of key/value pairs to override default properties.
* ex: "timestamp" -> Calendar.getInstance()
*/
public void addEvent(String eventCollection, Map event,
Map keenProperties) {
addEvent(null, eventCollection, event, keenProperties, null);
}
/**
* Synchronously adds an event to the specified collection. This method will immediately
* publish the event to the Keen server in the current thread.
*
* @param project The project in which to publish the event. If a default project has been set
* on the client, this parameter may be null, in which case the default project
* will be used.
* @param eventCollection The name of the collection in which to publish the event.
* @param event A Map that consists of key/value pairs. Keen naming conventions apply (see
* docs). Nested Maps and lists are acceptable (and encouraged!).
* @param keenProperties A Map that consists of key/value pairs to override default properties.
* ex: "timestamp" -> Calendar.getInstance()
* @param callback An optional callback to receive notification of success or failure.
*/
public void addEvent(KeenProject project, String eventCollection, Map event,
Map keenProperties, KeenCallback callback) {
if (!isActive) {
handleLibraryInactive(callback);
return;
}
if (project == null && defaultProject == null) {
handleFailure(null,
project,
eventCollection,
event,
keenProperties,
new IllegalStateException("No project specified, but no default project found"));
return;
}
KeenProject useProject = (project == null ? defaultProject : project);
try {
// Build the event.
Map newEvent =
validateAndBuildEvent(useProject, eventCollection, event, keenProperties);
// Publish the event.
publish(useProject, eventCollection, newEvent);
handleSuccess(callback, project, eventCollection, event, keenProperties);
} catch (Exception e) {
handleFailure(callback, project, eventCollection, event, keenProperties, e);
}
}
/**
* Adds an event to the default project with default Keen properties and no callbacks.
*
* @see #addEvent(KeenProject, String, java.util.Map, java.util.Map, KeenCallback)
* @param eventCollection The name of the collection in which to publish the event.
* @param event A Map that consists of key/value pairs. Keen naming conventions apply (see
* docs). Nested Maps and lists are acceptable (and encouraged!).
*/
public void addEventAsync(String eventCollection, Map event) {
addEventAsync(eventCollection, event, null);
}
/**
* Adds an event to the default project with no callbacks.
*
* @see #addEvent(KeenProject, String, java.util.Map, java.util.Map, KeenCallback)
* @param eventCollection The name of the collection in which to publish the event.
* @param event A Map that consists of key/value pairs. Keen naming conventions apply (see
* docs). Nested Maps and lists are acceptable (and encouraged!).
* @param keenProperties A Map that consists of key/value pairs to override default properties.
* ex: "timestamp" -> Calendar.getInstance()
*/
public void addEventAsync(String eventCollection, Map event,
final Map keenProperties) {
addEventAsync(null, eventCollection, event, keenProperties, null);
}
/**
* Asynchronously adds an event to the specified collection. This method will request that
* the Keen client's {@link java.util.concurrent.Executor} executes the publish operation.
*
* @param project The project in which to publish the event. If a default project has been set
* on the client this parameter may be null, in which case the default project
* will be used.
* @param eventCollection The name of the collection in which to publish the event.
* @param event A Map that consists of key/value pairs. Keen naming conventions apply (see
* docs). Nested Maps and lists are acceptable (and encouraged!).
* @param keenProperties A Map that consists of key/value pairs to override default properties.
* ex: "timestamp" -> Calendar.getInstance()
* @param callback An optional callback to receive notification of success or failure.
*/
public void addEventAsync(final KeenProject project, final String eventCollection,
final Map event,
final Map keenProperties,
final KeenCallback callback) {
if (!isActive) {
handleLibraryInactive(callback);
return;
}
if (project == null && defaultProject == null) {
handleFailure(null,
project,
eventCollection,
event,
keenProperties,
new IllegalStateException(
"No project specified, but no default project found"));
return;
}
final KeenProject useProject = (project == null ? defaultProject : project);
// Wrap the asynchronous execute in a try/catch block in case the executor throws a
// RejectedExecutionException (or anything else).
try {
publishExecutor.execute(new Runnable() {
@Override
public void run() {
addEvent(useProject, eventCollection, event, keenProperties, callback);
}
});
} catch (Exception e) {
handleFailure(callback, project, eventCollection, event, keenProperties, e);
}
}
/**
* Queues an event in the default project with default Keen properties and no callbacks.
*
* @see #queueEvent(KeenProject, String, java.util.Map, java.util.Map, KeenCallback)
* @param eventCollection The name of the collection in which to publish the event.
* @param event A Map that consists of key/value pairs. Keen naming conventions apply (see
* docs). Nested Maps and lists are acceptable (and encouraged!).
*/
public void queueEvent(String eventCollection, Map event) {
queueEvent(eventCollection, event, null);
}
/**
* Queues an event in the default project with no callbacks.
*
* @see #queueEvent(KeenProject, String, java.util.Map, java.util.Map, KeenCallback)
* @param eventCollection The name of the collection in which to publish the event.
* @param event A Map that consists of key/value pairs. Keen naming conventions apply (see
* docs). Nested Maps and lists are acceptable (and encouraged!).
* @param keenProperties A Map that consists of key/value pairs to override default properties.
* ex: "timestamp" -> Calendar.getInstance()
*/
public void queueEvent(String eventCollection, Map event,
Map keenProperties) {
queueEvent(null, eventCollection, event, keenProperties, null);
}
/**
* Synchronously queues an event for publishing. The event will be cached in the client's
* {@link io.keen.client.java.KeenEventStore} until the next call to either
* {@link #sendQueuedEvents()} or {@link #sendQueuedEventsAsync()}.
*
* @param project The project in which to publish the event. If a default project has been set
* on the client this parameter may be null, in which case the default project
* will be used.
* @param eventCollection The name of the collection in which to publish the event.
* @param event A Map that consists of key/value pairs. Keen naming conventions apply (see
* docs). Nested Maps and lists are acceptable (and encouraged!).
* @param keenProperties A Map that consists of key/value pairs to override default properties.
* ex: "timestamp" -> Calendar.getInstance()
* @param callback An optional callback to receive notification of success or failure.
*/
public void queueEvent(KeenProject project, String eventCollection, Map event,
Map keenProperties, final KeenCallback callback) {
if (!isActive) {
handleLibraryInactive(callback);
return;
}
if (project == null && defaultProject == null) {
handleFailure(null,
project,
eventCollection,
event,
keenProperties,
new IllegalStateException(
"No project specified, but no default project found"));
return;
}
KeenProject useProject = (project == null ? defaultProject : project);
try {
// Build the event
Map newEvent =
validateAndBuildEvent(useProject, eventCollection, event, keenProperties);
// Serialize the event into JSON.
StringWriter writer = new StringWriter();
jsonHandler.writeJson(writer, newEvent);
String jsonEvent = writer.toString();
KeenUtils.closeQuietly(writer);
try {
// Save the JSON event out to the event store.
Object handle = eventStore.store(useProject.getProjectId(), eventCollection, jsonEvent);
if (eventStore instanceof KeenAttemptCountingEventStore) {
synchronized (attemptsLock) {
Map attempts = getAttemptsMap(useProject.getProjectId(), eventCollection);
attempts.put("" + handle.hashCode(), maxAttempts);
setAttemptsMap(useProject.getProjectId(), eventCollection, attempts);
}
}
} catch(IOException ex) {
KeenLogging.log("Failed to set the event POST attempt count. The event was still " +
"queued and will we POSTed.");
}
handleSuccess(callback, project, eventCollection, event, keenProperties);
} catch (Exception e) {
handleFailure(callback, project, eventCollection, event, keenProperties, e);
}
}
/**
* Sends all queued events for the default project with no callbacks.
*
* @see #sendQueuedEvents(KeenProject, KeenCallback)
*/
public void sendQueuedEvents() {
sendQueuedEvents(null);
}
/**
* Sends all queued events for the specified project with no callbacks.
*
* @see #sendQueuedEvents(KeenProject, KeenCallback)
* @param project The project for which to send queued events. If a default project has been set
* on the client this parameter may be null, in which case the default project
* will be used.
*/
public void sendQueuedEvents(KeenProject project) {
sendQueuedEvents(project, null);
}
/**
* Synchronously sends all queued events for the given project. This method will immediately
* publish the events to the Keen server in the current thread.
*
* @param project The project for which to send queued events. If a default project has been set
* on the client this parameter may be null, in which case the default project
* will be used.
* @param callback An optional callback to receive notification of success or failure.
*/
public synchronized void sendQueuedEvents(KeenProject project, KeenCallback callback) {
if (!isActive) {
handleLibraryInactive(callback);
return;
}
if (project == null && defaultProject == null) {
handleFailure(null, new IllegalStateException("No project specified, but no default project found"));
return;
}
if (!isNetworkConnected()) {
KeenLogging.log("Not sending events because there is no network connection. " +
"Events will be retried next time `sendQueuedEvents` is called.");
handleFailure(callback, new Exception("Network not connected."));
return;
}
KeenProject useProject = (project == null ? defaultProject : project);
try {
String projectId = useProject.getProjectId();
Map> eventHandles = eventStore.getHandles(projectId);
Map>> events = buildEventMap(projectId, eventHandles);
String response = publishAll(useProject, events);
if (response != null) {
try {
handleAddEventsResponse(eventHandles, response);
} catch (Exception e) {
// Errors handling the response are non-fatal; just log them.
KeenLogging.log("Error handling response to batch publish: " + e.getMessage());
}
}
handleSuccess(callback);
} catch (Exception e) {
handleFailure(callback, e);
}
}
/**
* Sends all queued events for the default project with no callbacks.
*
* @see #sendQueuedEventsAsync(KeenProject, KeenCallback)
*/
public void sendQueuedEventsAsync() {
sendQueuedEventsAsync(null);
}
/**
* Sends all queued events for the specified project with no callbacks.
*
* @see #sendQueuedEventsAsync(KeenProject, KeenCallback)
* @param project The project for which to send queued events. If a default project has been set
* on the client this parameter may be null, in which case the default project
* will be used.
*/
public void sendQueuedEventsAsync(final KeenProject project) {
sendQueuedEventsAsync(project, null);
}
/**
* Asynchronously sends all queued events for the given project. This method will request that
* the Keen client's {@link java.util.concurrent.Executor} executes the publish operation.
*
* @param project The project for which to send queued events. If a default project has been set
* on the client this parameter may be null, in which case the default project
* will be used.
* @param callback An optional callback to receive notification of success or failure.
*/
public void sendQueuedEventsAsync(final KeenProject project, final KeenCallback callback) {
if (!isActive) {
handleLibraryInactive(callback);
return;
}
if (project == null && defaultProject == null) {
handleFailure(null, new IllegalStateException("No project specified, but no default project found"));
return;
}
final KeenProject useProject = (project == null ? defaultProject : project);
// Wrap the asynchronous execute in a try/catch block in case the executor throws a
// RejectedExecutionException (or anything else).
try {
publishExecutor.execute(new Runnable() {
@Override
public void run() {
sendQueuedEvents(useProject, callback);
}
});
} catch (Exception e) {
handleFailure(callback, e);
}
}
/**
* Gets the JSON handler for this client.
*
* @return The {@link io.keen.client.java.KeenJsonHandler}.
*/
public KeenJsonHandler getJsonHandler() {
return jsonHandler;
}
/**
* Gets the event store for this client.
*
* @return The {@link io.keen.client.java.KeenEventStore}.
*/
public KeenEventStore getEventStore() {
return eventStore;
}
/**
* Gets the executor for asynchronous publishing for this client.
*
* @return The {@link java.util.concurrent.Executor}.
*/
public Executor getPublishExecutor() {
return publishExecutor;
}
/**
* Gets the default project that this {@link KeenClient} will use if no project is specified.
*
* @return The default project.
*/
public KeenProject getDefaultProject() {
return defaultProject;
}
/**
* Sets the default project that this {@link KeenClient} should use if no project is specified.
*
* @param defaultProject The new default project.
*/
public void setDefaultProject(KeenProject defaultProject) {
this.defaultProject = defaultProject;
}
/**
* Gets the base API URL associated with this instance of the {@link KeenClient}.
*
* @return The base API URL
*/
public String getBaseUrl() {
return baseUrl;
}
/**
* Sets the base API URL associated with this instance of the {@link KeenClient}.
*
* Use this if you want to disable SSL.
*
* @param baseUrl The new base URL (i.e. 'http://api.keen.io'), or null to reset the base URL to
* the default ('https://api.keen.io').
*/
public void setBaseUrl(String baseUrl) {
if (baseUrl == null) {
this.baseUrl = KeenConstants.SERVER_ADDRESS;
} else {
this.baseUrl = baseUrl;
}
}
/**
* Sets the maximum number of HTTPS POST retry attempts for all events added in the future.
*
* @param maxAttempts the maximum number attempts
*/
public void setMaxAttempts(int maxAttempts) {
this.maxAttempts = maxAttempts;
}
/**
* Sets the maximum number of HTTPS POST retry attempts for all events added in the future.
*
* @return the maximum number attempts
*/
public int getMaxAttempts() {
return maxAttempts;
}
/**
* Gets the {@link GlobalPropertiesEvaluator} associated with this instance of the {@link KeenClient}.
*
* @return The {@link GlobalPropertiesEvaluator}
*/
public GlobalPropertiesEvaluator getGlobalPropertiesEvaluator() {
return globalPropertiesEvaluator;
}
/**
* Call this to set the {@link GlobalPropertiesEvaluator} for this instance of the {@link KeenClient}.
* The evaluator is invoked every time an event is added to an event collection.
*
* Global properties are properties which are sent with EVERY event. For example, you may wish to always
* capture device information like OS version, handset type, orientation, etc.
*
* The evaluator takes as a parameter a single String, which is the name of the event collection the
* event's being added to. You're responsible for returning a Map which represents the global properties
* for this particular event collection.
*
* Note that because we use a class defined by you, you can create DYNAMIC global properties. For example,
* if you want to capture device orientation, then your evaluator can ask the device for its current orientation
* and then construct the Map. If your global properties aren't dynamic, then just return the same Map
* every time.
*
* Example usage:
*
* {@code KeenClient client = KeenClient.client();
* GlobalPropertiesEvaluator evaluator = new GlobalPropertiesEvaluator() {
* public Map getGlobalProperties(String eventCollection) {
* Map map = new HashMap();
* map.put("some dynamic property name", "some dynamic property value");
* return map;
* }
* };
* client.setGlobalPropertiesEvaluator(evaluator);
* }
*
*
* @param globalPropertiesEvaluator The evaluator which is invoked any time an event is added to an event
* collection.
*/
public void setGlobalPropertiesEvaluator(GlobalPropertiesEvaluator globalPropertiesEvaluator) {
this.globalPropertiesEvaluator = globalPropertiesEvaluator;
}
/**
* Gets the Keen Global Properties map. See docs for {@link #setGlobalProperties(java.util.Map)}.
*
* @return The Global Properties map.
*/
public Map getGlobalProperties() {
return globalProperties;
}
/**
* Call this to set the Keen Global Properties Map for this instance of the {@link KeenClient}. The Map
* is used every time an event is added to an event collection.
*
* Keen Global Properties are properties which are sent with EVERY event. For example, you may wish to always
* capture static information like user ID, app version, etc.
*
* Every time an event is added to an event collection, the SDK will check to see if this property is defined.
* If it is, the SDK will copy all the properties from the global properties into the newly added event.
*
* Note that because this is just a Map, it's much more difficult to create DYNAMIC global properties.
* It also doesn't support per-collection properties. If either of these use cases are important to you, please use
* the {@link GlobalPropertiesEvaluator}.
*
* Also note that the Keen properties defined in {@link #getGlobalPropertiesEvaluator()} take precedence over
* the properties defined in getGlobalProperties, and that the Keen Properties defined in each
* individual event take precedence over either of the Global Properties.
*
* Example usage:
*
*
* {@code
* KeenClient client = KeenClient.client();
* Map map = new HashMap();
* map.put("some standard key", "some standard value");
* client.setGlobalProperties(map);
* }
*
*
* @param globalProperties The new map you wish to use as the Keen Global Properties.
*/
public void setGlobalProperties(Map globalProperties) {
this.globalProperties = globalProperties;
}
/**
* Gets whether or not the Keen client is running in debug mode.
*
* @return {@code true} if debug mode is enabled, otherwise {@code false}.
*/
public boolean isDebugMode() {
return isDebugMode;
}
/**
* Sets whether or not the Keen client should run in debug mode. When debug mode is enabled,
* all exceptions will be thrown immediately; otherwise they will be logged and reported to
* any callbacks, but never thrown.
*
* @param isDebugMode {@code true} to enable debug mode, or {@code false} to disable it.
*/
public void setDebugMode(boolean isDebugMode) {
this.isDebugMode = isDebugMode;
}
/**
* Gets whether or not the client is in active mode.
*
* @return {@code true} if the client is active,; {@code false} if it is inactive.
*/
public boolean isActive() {
return isActive;
}
/**
* Sets an HTTP proxy server configuration for this client.
*
* @param proxyHost The proxy hostname or IP address.
* @param proxyPort The proxy port number.
*/
public void setProxy(String proxyHost, int proxyPort) {
this.proxy = new Proxy(Proxy.Type.HTTP, new InetSocketAddress(proxyHost, proxyPort));
}
/**
* Sets an HTTP proxy server configuration for this client.
*
* @param proxy The Proxy object to set.
*/
public void setProxy(Proxy proxy) {
this.proxy = proxy;
}
/**
* Gets the client Proxy.
*
* @return the proxy.
*/
public Proxy getProxy() {
return proxy;
}
///// PROTECTED ABSTRACT BUILDER IMPLEMENTATION /////
/**
* Builder class for instantiating Keen clients. Subclasses should override this and
* implement the getDefault* methods to provide new default behavior.
*
* This builder doesn't include any default implementation for handling JSON serialization and
* de-serialization. Subclasses must provide one.
*
* This builder defaults to using HttpURLConnection to handle HTTP requests.
*
* To cache events in between batch uploads, this builder defaults to a RAM-based event store.
*
* This builder defaults to a fixed thread pool (constructed with
* {@link java.util.concurrent.Executors#newFixedThreadPool(int)}) to run asynchronous requests.
*/
public static abstract class Builder {
private HttpHandler httpHandler;
private KeenJsonHandler jsonHandler;
private KeenEventStore eventStore;
private Executor publishExecutor;
private KeenNetworkStatusHandler networkStatusHandler;
private int connectTimeout = KeenClient.DEFAULT_CONNECT_TIMEOUT;
private int readTimeout = KeenClient.DEFAULT_READ_TIMEOUT;
/**
* Gets the default {@link HttpHandler} to use if none is explicitly set for this builder.
*
* This implementation returns a handler that will use {@link java.net.HttpURLConnection}
* to make HTTP requests.
*
* Subclasses should override this to provide an alternative default {@link HttpHandler}.
*
* @return The default {@link HttpHandler}.
* @throws Exception If there is an error creating the {@link HttpHandler}.
*/
protected HttpHandler getDefaultHttpHandler() throws Exception {
return new UrlConnectionHttpHandler();
}
/**
* Gets the {@link HttpHandler} that this builder is currently configured to use for making
* HTTP requests. If null, a default will be used instead.
*
* @return The {@link HttpHandler} to use.
*/
public HttpHandler getHttpHandler() {
return httpHandler;
}
/**
* Sets the {@link HttpHandler} to use for making HTTP requests.
*
* @param httpHandler The {@link HttpHandler} to use.
*/
public void setHttpHandler(HttpHandler httpHandler) {
this.httpHandler = httpHandler;
}
/**
* Sets the {@link HttpHandler} to use for making HTTP requests.
*
* @param httpHandler The {@link HttpHandler} to use.
* @return This instance (for method chaining).
*/
public Builder withHttpHandler(HttpHandler httpHandler) {
setHttpHandler(httpHandler);
return this;
}
/**
* Gets the default {@link KeenJsonHandler} to use if none is explicitly set for this builder.
*
* Subclasses must override this to provide a default {@link KeenJsonHandler}.
*
* @return The default {@link KeenJsonHandler}.
* @throws Exception If there is an error creating the {@link KeenJsonHandler}.
*/
protected abstract KeenJsonHandler getDefaultJsonHandler() throws Exception;
/**
* Gets the {@link KeenJsonHandler} that this builder is currently configured to use for
* handling JSON operations. If null, a default will be used instead.
*
* @return The {@link KeenJsonHandler} to use.
*/
public KeenJsonHandler getJsonHandler() {
return jsonHandler;
}
/**
* Sets the {@link KeenJsonHandler} to use for handling JSON operations.
*
* @param jsonHandler The {@link KeenJsonHandler} to use.
*/
public void setJsonHandler(KeenJsonHandler jsonHandler) {
this.jsonHandler = jsonHandler;
}
/**
* Sets the {@link KeenJsonHandler} to use for handling JSON operations.
*
* @param jsonHandler The {@link KeenJsonHandler} to use.
* @return This instance (for method chaining).
*/
public Builder withJsonHandler(KeenJsonHandler jsonHandler) {
setJsonHandler(jsonHandler);
return this;
}
/**
* Gets the default {@link KeenEventStore} to use if none is explicitly set for this builder.
*
* This implementation returns a RAM-based store.
*
* Subclasses should override this to provide an alternative default {@link KeenEventStore}.
*
* @return The default {@link KeenEventStore}.
* @throws Exception If there is an error creating the {@link KeenEventStore}.
*/
protected KeenEventStore getDefaultEventStore() throws Exception {
return new RamEventStore();
}
/**
* Gets the {@link KeenEventStore} that this builder is currently configured to use for
* storing events between batch publish operations. If null, a default will be used instead.
*
* @return The {@link KeenEventStore} to use.
*/
public KeenEventStore getEventStore() {
return eventStore;
}
/**
* Sets the {@link KeenEventStore} to use for storing events in between batch publish
* operations.
*
* @param eventStore The {@link KeenEventStore} to use.
*/
public void setEventStore(KeenEventStore eventStore) {
this.eventStore = eventStore;
}
/**
* Sets the {@link KeenEventStore} to use for storing events in between batch publish
* operations.
*
* @param eventStore The {@link KeenEventStore} to use.
* @return This instance (for method chaining).
*/
public Builder withEventStore(KeenEventStore eventStore) {
setEventStore(eventStore);
return this;
}
/**
* Gets the default {@link Executor} to use if none is explicitly set for this builder.
*
* This implementation returns a simple fixed thread pool with the number of threads equal
* to the number of available processors.
*
* Subclasses should override this to provide an alternative default {@link Executor}.
*
* @return The default {@link Executor}.
* @throws Exception If there is an error creating the {@link Executor}.
*/
protected Executor getDefaultPublishExecutor() throws Exception {
int procCount = Runtime.getRuntime().availableProcessors();
return Executors.newFixedThreadPool(procCount);
}
/**
* Gets the {@link Executor} that this builder is currently configured to use for
* asynchronous publishing operations. If null, a default will be used instead.
*
* @return The {@link Executor} to use.
*/
public Executor getPublishExecutor() {
return publishExecutor;
}
/**
* Sets the {@link Executor} to use for asynchronous publishing operations.
*
* @param publishExecutor The {@link Executor} to use.
*/
public void setPublishExecutor(Executor publishExecutor) {
this.publishExecutor = publishExecutor;
}
/**
* Sets the {@link Executor} to use for asynchronous publishing operations.
*
* @param publishExecutor The {@link Executor} to use.
* @return This instance (for method chaining).
*/
public Builder withPublishExecutor(Executor publishExecutor) {
setPublishExecutor(publishExecutor);
return this;
}
/**
* Gets the default {@link KeenNetworkStatusHandler} to use if none is explicitly set for this builder.
*
* This implementation always returns true.
*
* Subclasses should override this to provide an alternative default {@link KeenNetworkStatusHandler}.
*
* @return The default {@link KeenNetworkStatusHandler}.
*/
protected KeenNetworkStatusHandler getDefaultNetworkStatusHandler() {
return new AlwaysConnectedNetworkStatusHandler();
}
/**
* Gets the {@link KeenNetworkStatusHandler} that this builder is currently configured to use.
* If null, a default will be used instead.
*
* @return The {@link KeenNetworkStatusHandler} to use.
*/
public KeenNetworkStatusHandler getNetworkStatusHandler () {
return networkStatusHandler;
}
/**
* Sets the {@link KeenNetworkStatusHandler} to use.
*
* @param networkStatusHandler The {@link KeenNetworkStatusHandler} to use.
*/
public void setNetworkStatusHandler(KeenNetworkStatusHandler networkStatusHandler) {
this.networkStatusHandler = networkStatusHandler;
}
/**
* Sets the {@link KeenNetworkStatusHandler} to use.
*
* @param networkStatusHandler The {@link KeenNetworkStatusHandler} to use.
* @return This instance (for method chaining).
*/
public Builder withNetworkStatusHandler(KeenNetworkStatusHandler networkStatusHandler) {
setNetworkStatusHandler(networkStatusHandler);
return this;
}
/**
* Sets the connection timeout to use.
*
* @param connectTimeout in milliseconds
* @return This instance (for method chaining).
*/
public Builder withConnectTimeout(int connectTimeout) {
this.connectTimeout = connectTimeout;
return this;
}
/**
* Sets the read timeout to use.
*
* @param readTimeout in milliseconds
* @return This instance (for method chaining).
*/
public Builder withReadTimeout(int readTimeout) {
this.readTimeout = readTimeout;
return this;
}
/**
* Builds a new Keen client using the interfaces which have been specified explicitly on
* this builder instance via the set* or with* methods, or the default interfaces if none
* have been specified.
*
* @return A newly constructed Keen client.
*/
public KeenClient build() {
try {
if (httpHandler == null) {
httpHandler = getDefaultHttpHandler();
}
} catch (Exception e) {
KeenLogging.log("Exception building HTTP handler: " + e.getMessage());
}
try {
if (jsonHandler == null) {
jsonHandler = getDefaultJsonHandler();
}
} catch (Exception e) {
KeenLogging.log("Exception building JSON handler: " + e.getMessage());
}
try {
if (eventStore == null) {
eventStore = getDefaultEventStore();
}
} catch (Exception e) {
KeenLogging.log("Exception building event store: " + e.getMessage());
}
try {
if (publishExecutor == null) {
publishExecutor = getDefaultPublishExecutor();
}
} catch (Exception e) {
KeenLogging.log("Exception building publish executor: " + e.getMessage());
}
try {
if (networkStatusHandler == null) {
networkStatusHandler = getDefaultNetworkStatusHandler();
}
} catch (Exception e) {
KeenLogging.log("Exception building network status handler: " + e.getMessage());
}
return buildInstance();
}
/**
* Builds an instance based on this builder. This method is exposed only as a test hook to
* allow test classes to modify how the {@link KeenClient} is constructed (i.e. by
* providing a mock {@link Environment}.
*
* @return The new {@link KeenClient}.
*/
protected KeenClient buildInstance() {
return new KeenClient(this);
}
}
///// PROTECTED CONSTRUCTORS /////
/**
* Constructs a Keen client using system environment variables.
*
* @param builder The builder from which to retrieve this client's interfaces and settings.
*/
protected KeenClient(Builder builder) {
this(builder, new Environment());
}
/**
* Constructs a Keen client using the provided environment.
*
* NOTE: This constructor is only intended for use by test code, and should not be used
* directly. Subclasses should call the default {@link #KeenClient(Builder)} constructor.
*
* @param builder The builder from which to retrieve this client's interfaces and settings.
* @param env The environment to use to attempt to build the default project.
*/
KeenClient(Builder builder, Environment env) {
// Initialize final properties using the builder.
this.httpHandler = builder.httpHandler;
this.jsonHandler = builder.jsonHandler;
this.eventStore = builder.eventStore;
this.publishExecutor = builder.publishExecutor;
this.networkStatusHandler = builder.networkStatusHandler;
this.connectTimeout = builder.connectTimeout;
this.readTimeout = builder.readTimeout;
// If any of the interfaces are null, mark this client as inactive.
if (httpHandler == null || jsonHandler == null ||
eventStore == null || publishExecutor == null) {
setActive(false);
}
// Initialize other properties.
this.baseUrl = KeenConstants.SERVER_ADDRESS;
this.globalPropertiesEvaluator = null;
this.globalProperties = null;
// If a default project has been specified in environment variables, use it.
if (env.getKeenProjectId() != null) {
defaultProject = new KeenProject(env);
}
}
///// PROTECTED METHODS /////
/**
* Sets whether or not the client is in active mode. When the client is inactive, all requests
* will be ignored.
*
* @param isActive {@code true} to make the client active, or {@code false} to make it
* inactive.
*/
protected void setActive(boolean isActive) {
this.isActive = isActive;
KeenLogging.log("Keen Client set to " + (isActive? "active" : "inactive"));
}
/**
* Validates an event and inserts global properties, producing a new event object which is
* ready to be published to the Keen service.
*
* @param project The project in which the event will be published.
* @param eventCollection The name of the collection in which the event will be published.
* @param event A Map that consists of key/value pairs.
* @param keenProperties A Map that consists of key/value pairs to override default properties.
* @return A new event Map containing Keen properties and global properties.
*/
protected Map validateAndBuildEvent(KeenProject project,
String eventCollection, Map event, Map keenProperties) {
if (project.getWriteKey() == null) {
throw new NoWriteKeyException("You can't send events to Keen if you haven't set a write key.");
}
validateEventCollection(eventCollection);
validateEvent(event);
KeenLogging.log(String.format(Locale.US, "Adding event to collection: %s", eventCollection));
// Create maps to aggregate keen & non-keen properties
Map newEvent = new HashMap();
Map mergedKeenProperties = new HashMap();
// separate keen & non-keen properties from static globals and merge them into separate maps
if (null != globalProperties) {
mergeGlobalProperties(getGlobalProperties(), mergedKeenProperties, newEvent);
}
// separate keen & non-keen properties from dynamic globals and merge them into separate maps
GlobalPropertiesEvaluator globalPropertiesEvaluator = getGlobalPropertiesEvaluator();
if (globalPropertiesEvaluator != null) {
mergeGlobalProperties(globalPropertiesEvaluator.getGlobalProperties(eventCollection), mergedKeenProperties,
newEvent);
}
// merge any per-event keen properties
if (keenProperties != null) {
mergedKeenProperties.putAll(keenProperties);
}
// if no keen.timestamp was provided by globals or event, add one now
if (!mergedKeenProperties.containsKey("timestamp")) {
Calendar currentTime = Calendar.getInstance();
String timestamp = ISO_8601_FORMAT.format(currentTime.getTime());
mergedKeenProperties.put("timestamp", timestamp);
}
// add merged keen properties to event
newEvent.put("keen", mergedKeenProperties);
// merge any per-event non-keen properties
newEvent.putAll(event);
return newEvent;
}
/**
* Removes the "keen" key from the globalProperties map and, if a map was removed, then all of its pairs are added to the keenProperties map.
* Anything left in the globalProperties map is then added to the newEvent map.
*
* @param globalProperties
* @param keenProperties
* @param newEvent
*/
private void mergeGlobalProperties(Map globalProperties, Map keenProperties,
Map newEvent) {
if (globalProperties != null) {
// Clone globals so we don't modify the original
globalProperties = new HashMap(globalProperties);
Object keen = globalProperties.remove("keen");
if (keen instanceof Map) {
keenProperties.putAll((Map)keen);
}
newEvent.putAll(globalProperties);
}
}
///// PRIVATE TYPES /////
/**
* The {@link io.keen.client.java.KeenClient} class's singleton enum.
*/
private enum ClientSingleton {
INSTANCE;
KeenClient client;
}
///// PRIVATE CONSTANTS /////
private static final DateFormat ISO_8601_FORMAT =
new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ", Locale.US);
///// PRIVATE FIELDS /////
private final HttpHandler httpHandler;
private final KeenJsonHandler jsonHandler;
private final KeenEventStore eventStore;
private final Executor publishExecutor;
private final KeenNetworkStatusHandler networkStatusHandler;
private final Object attemptsLock = new Object();
private final int connectTimeout;
private final int readTimeout;
private boolean isActive = true;
private boolean isDebugMode;
private int maxAttempts = KeenConstants.DEFAULT_MAX_ATTEMPTS;
private KeenProject defaultProject;
private String baseUrl;
private GlobalPropertiesEvaluator globalPropertiesEvaluator;
private Map globalProperties;
private Proxy proxy;
///// PRIVATE METHODS /////
/**
* Validates the name of an event collection.
*
* @param eventCollection An event collection name to be validated.
* @throws io.keen.client.java.exceptions.InvalidEventCollectionException If the event collection name is invalid. See Keen documentation for details.
*/
private void validateEventCollection(String eventCollection) {
if (eventCollection == null || eventCollection.length() == 0) {
throw new InvalidEventCollectionException("You must specify a non-null, " +
"non-empty event collection: " + eventCollection);
}
if (eventCollection.length() > 256) {
throw new InvalidEventCollectionException("An event collection name cannot be longer than 256 characters.");
}
}
/**
* @see #validateEvent(java.util.Map, int)
* @param event The event to validate.
*/
private void validateEvent(Map event) {
validateEvent(event, 0);
}
/**
* Validates an event.
*
* @param event The event to validate.
* @param depth The number of layers of the map structure that have already been traversed; this
* should be 0 for the initial call and will increment on each recursive call.
*/
@SuppressWarnings("unchecked") // cast to generic Map will always be okay in this case
private void validateEvent(Map event, int depth) {
if (depth == 0) {
if (event == null || event.size() == 0) {
throw new InvalidEventException("You must specify a non-null, non-empty event.");
}
if (event.containsKey("keen")) {
throw new InvalidEventException("An event cannot contain a root-level property named 'keen'.");
}
} else if (depth > KeenConstants.MAX_EVENT_DEPTH) {
throw new InvalidEventException("An event's depth (i.e. layers of nesting) cannot exceed " +
KeenConstants.MAX_EVENT_DEPTH);
}
for (Map.Entry entry : event.entrySet()) {
String key = entry.getKey();
if (key.contains(".")) {
throw new InvalidEventException("An event cannot contain a property with the period (.) character in " +
"it.");
}
if (key.length() > 256) {
throw new InvalidEventException("An event cannot contain a property name longer than 256 characters.");
}
validateEventValue(entry.getValue(), depth);
}
}
/**
* Validates a value within an event structure. This method will handle validating each element
* in a list, as well as recursively validating nested maps.
*
* @param value The value to validate.
* @param depth The current depth of validation.
*/
@SuppressWarnings("unchecked") // cast to generic Map will always be okay in this case
private void validateEventValue(Object value, int depth) {
if (value instanceof String) {
String strValue = (String) value;
if (strValue.length() >= 10000) {
throw new InvalidEventException("An event cannot contain a string property value longer than 10," +
"000 characters.");
}
} else if (value instanceof Map) {
validateEvent((Map) value, depth + 1);
} else if (value instanceof Iterable) {
for (Object listElement : (Iterable) value) {
validateEventValue(listElement, depth);
}
}
}
/**
* Builds a map from collection name to a list of event maps, given a map from collection name
* to a list of event handles. This method just uses the event store to retrieve each event by
* its handle.
*
* @param eventHandles A map from collection name to a list of event handles in the event store.
* @return A map from collection name to a list of event maps.
* @throws IOException If there is an error retrieving events from the store.
*/
private Map>> buildEventMap(String projectId,
Map> eventHandles) throws IOException {
Map>> result =
new HashMap>>();
for (Map.Entry> entry : eventHandles.entrySet()) {
String eventCollection = entry.getKey();
List