/** * 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()); } return buildInstance(); }
/** * 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; } KeenProject useProject = (project == null ? defaultProject : project); try { String projectId = useProject.getProjectId(); Map<String, List<Object>> eventHandles = eventStore.getHandles(projectId); Map<String, List<Map<String, Object>>> events = buildEventMap(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); } }
/** * 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<String, Object> validateAndBuildEvent( KeenProject project, String eventCollection, Map<String, Object> event, Map<String, Object> keenProperties) { if (project.getWriteKey() == null) { throw new NoWriteKeyException( "You can't send events to Keen IO if you haven't set a write key."); } validateEventCollection(eventCollection); validateEvent(event); KeenLogging.log(String.format(Locale.US, "Adding event to collection: %s", eventCollection)); // build the event Map<String, Object> newEvent = new HashMap<String, Object>(); // handle keen properties Calendar currentTime = Calendar.getInstance(); String timestamp = ISO_8601_FORMAT.format(currentTime.getTime()); if (keenProperties == null) { keenProperties = new HashMap<String, Object>(); keenProperties.put("timestamp", timestamp); } else { if (!keenProperties.containsKey("timestamp")) { keenProperties.put("timestamp", timestamp); } } newEvent.put("keen", keenProperties); // handle global properties Map<String, Object> globalProperties = getGlobalProperties(); if (globalProperties != null) { newEvent.putAll(globalProperties); } GlobalPropertiesEvaluator globalPropertiesEvaluator = getGlobalPropertiesEvaluator(); if (globalPropertiesEvaluator != null) { Map<String, Object> props = globalPropertiesEvaluator.getGlobalProperties(eventCollection); if (props != null) { newEvent.putAll(props); } } // now handle user-defined properties newEvent.putAll(event); return newEvent; }
/** * Handles a failure in the Keen library. If the client is running in debug mode, this will * immediately throw a runtime exception. Otherwise, this will log an error message and, if the * callback is non-null, call the {@link KeenCallback#onFailure(Exception)} method. Any exceptions * thrown by the callback are silently ignored. * * @param callback A callback; may be null. * @param e The exception which caused the failure. */ private void handleFailure(KeenCallback callback, Exception e) { if (isDebugMode) { if (e instanceof RuntimeException) { throw (RuntimeException) e; } else { throw new RuntimeException(e); } } else { KeenLogging.log("Encountered error: " + e.getMessage()); if (callback != null) { try { callback.onFailure(e); } catch (Exception userException) { // Do nothing. } } } }
/** * Posts a request to the server in the specified project, using the given URL and request data. * The request data will be serialized into JSON using the client's {@link * io.keen.client.java.KeenJsonHandler}. * * @param project The project in which the event(s) will be published; this is used to determine * the write key to use for authentication. * @param url The URL to which the POST should be sent. * @param requestData The request data, which will be serialized into JSON and sent in the request * body. * @return The response from the server. * @throws IOException If there was an error communicating with the server. */ private synchronized String publishObject( KeenProject project, URL url, final Map<String, ?> requestData) throws IOException { if (requestData == null || requestData.size() == 0) { KeenLogging.log("No API calls were made because there were no events to upload"); return null; } // Build an output source which simply writes the serialized JSON to the output. OutputSource source = new OutputSource() { @Override public void writeTo(OutputStream out) throws IOException { OutputStreamWriter writer = new OutputStreamWriter(out, ENCODING); jsonHandler.writeJson(writer, requestData); } }; // If logging is enabled, log the request being sent. if (KeenLogging.isLoggingEnabled()) { try { StringWriter writer = new StringWriter(); jsonHandler.writeJson(writer, requestData); String request = writer.toString(); KeenLogging.log( String.format(Locale.US, "Sent request '%s' to URL '%s'", request, url.toString())); } catch (IOException e) { KeenLogging.log("Couldn't log event written to file: "); e.printStackTrace(); } } // Send the request. String writeKey = project.getWriteKey(); Request request = new Request(url, "POST", writeKey, source); Response response = httpHandler.execute(request); // If logging is enabled, log the response. if (KeenLogging.isLoggingEnabled()) { KeenLogging.log( String.format( Locale.US, "Received response: '%s' (%d)", response.body, response.statusCode)); } // If the request succeeded, return the response body. Otherwise throw an exception. if (response.isSuccess()) { return response.body; } else { throw new ServerException(response.body); } }
/** * 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")); }
/** * Handles a response from the Keen service to a batch post events operation. In particular, this * method will iterate through the responses and remove any successfully processed events (or * events which failed for known fatal reasons) from the event store so they won't be sent in * subsequent posts. * * @param handles A map from collection names to lists of handles in the event store. This is * referenced against the response from the server to determine which events to remove from * the store. * @param response The response from the server. * @throws IOException If there is an error removing events from the store. */ @SuppressWarnings("unchecked") private void handleAddEventsResponse(Map<String, List<Object>> handles, String response) throws IOException { // Parse the response into a map. StringReader reader = new StringReader(response); Map<String, Object> responseMap; responseMap = jsonHandler.readJson(reader); // It's not obvious what the best way is to try and recover from them, but just hoping it // doesn't happen is probably the wrong answer. // Loop through all the event collections. for (Map.Entry<String, Object> entry : responseMap.entrySet()) { String collectionName = entry.getKey(); // Get the list of handles in this collection. List<Object> collectionHandles = handles.get(collectionName); // Iterate through the elements in the collection List<Map<String, Object>> eventResults = (List<Map<String, Object>>) entry.getValue(); int index = 0; for (Map<String, Object> eventResult : eventResults) { // now loop through each event collection's individual results boolean removeCacheEntry = true; boolean success = (Boolean) eventResult.get(KeenConstants.SUCCESS_PARAM); if (!success) { // grab error code and description Map errorDict = (Map) eventResult.get(KeenConstants.ERROR_PARAM); String errorCode = (String) errorDict.get(KeenConstants.NAME_PARAM); if (errorCode.equals(KeenConstants.INVALID_COLLECTION_NAME_ERROR) || errorCode.equals(KeenConstants.INVALID_PROPERTY_NAME_ERROR) || errorCode.equals(KeenConstants.INVALID_PROPERTY_VALUE_ERROR)) { removeCacheEntry = true; KeenLogging.log( "An invalid event was found. Deleting it. Error: " + errorDict.get(KeenConstants.DESCRIPTION_PARAM)); } else { String description = (String) errorDict.get(KeenConstants.DESCRIPTION_PARAM); removeCacheEntry = false; KeenLogging.log( String.format( Locale.US, "The event could not be inserted for some reason. " + "Error name and description: %s %s", errorCode, description)); } } // If the cache entry should be removed, get the handle at the appropriate index // and ask the event store to remove it. if (removeCacheEntry) { Object handle = collectionHandles.get(index); // Try to remove the object from the cache. Catch and log exceptions to prevent // a single failure from derailing the rest of the cleanup. try { eventStore.remove(handle); } catch (IOException e) { KeenLogging.log("Failed to remove object '" + handle + "' from cache"); } } index++; } } }