com.amazonaws.mobileconnectors.pinpoint.analytics.AnalyticsEvent.java Source code

Java tutorial

Introduction

Here is the source code for com.amazonaws.mobileconnectors.pinpoint.analytics.AnalyticsEvent.java

Source

/**
 * Copyright 2016-2017 Amazon.com, Inc. or its affiliates. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License").
 * You may not use this file except in compliance with the License.
 * A copy of the License is located at
 *
 * http://aws.amazon.com/apache2.0
 *
 * or in the "license" file accompanying this file. This file 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.amazonaws.mobileconnectors.pinpoint.analytics;

import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.json.JSONException;
import org.json.JSONObject;
import com.amazonaws.mobileconnectors.pinpoint.internal.core.PinpointContext;
import com.amazonaws.mobileconnectors.pinpoint.internal.core.system.AndroidAppDetails;
import com.amazonaws.mobileconnectors.pinpoint.internal.core.system.AndroidDeviceDetails;
import com.amazonaws.mobileconnectors.pinpoint.internal.core.util.JSONBuilder;
import com.amazonaws.mobileconnectors.pinpoint.internal.core.util.JSONSerializable;
import com.amazonaws.mobileconnectors.pinpoint.internal.core.util.SDKInfo;
import com.amazonaws.mobileconnectors.pinpoint.internal.core.util.StringUtil;
import com.amazonaws.mobileconnectors.pinpoint.internal.event.ClientContext;

/**
 * Represents an AnalyticsEvent
 */
public class AnalyticsEvent implements JSONSerializable {

    static final int MAX_EVENT_ATTRIBUTE_METRIC_KEY_LENGTH = 50;
    static final int MAX_EVENT_ATTRIBUTE_VALUE_LENGTH = 1000;
    static final int MAX_NUM_OF_METRICS_AND_ATTRIBUTES = 50;
    private static final Log log = LogFactory.getLog(AnalyticsEvent.class);
    private static final int INDENTATION = 4;
    private final String eventId;
    private final String eventType;
    private final String sdkName;
    private final String sdkVersion;
    private final PinpointSession session;
    private final Map<String, String> attributes = new ConcurrentHashMap<String, String>();
    private final Map<String, Double> metrics = new ConcurrentHashMap<String, Double>();
    private final Long timestamp;
    private final String uniqueId;
    private final AndroidAppDetails appDetails;
    private final AndroidDeviceDetails deviceDetails;
    private final AtomicInteger currentNumOfAttributesAndMetrics = new AtomicInteger(0);

    AnalyticsEvent(final String eventType, final Map<String, String> attributes, final Map<String, Double> metrics,
            final SDKInfo sdkInfo, final String sessionId, final long sessionStart, final Long sessionEnd,
            final Long sessionDuration, final long timestamp, final String uniqueId,
            final AndroidAppDetails appDetails, final AndroidDeviceDetails deviceDetails) {
        this(UUID.randomUUID().toString(), eventType, attributes, metrics, sdkInfo, sessionId, sessionStart,
                sessionEnd, sessionDuration, timestamp, uniqueId, appDetails, deviceDetails);
    }

    private AnalyticsEvent(final String eventId, final String eventType, final Map<String, String> attributes,
            final Map<String, Double> metrics, final SDKInfo sdkInfo, final String sessionId,
            final long sessionStart, final Long sessionEnd, final Long sessionDuration, final long timestamp,
            final String uniqueId, final AndroidAppDetails appDetails, final AndroidDeviceDetails deviceDetails) {
        this.eventId = eventId;
        this.sdkName = sdkInfo.getName();
        this.sdkVersion = sdkInfo.getVersion();
        this.session = new PinpointSession(sessionId, sessionStart, sessionEnd, sessionDuration);
        this.timestamp = timestamp;
        this.uniqueId = uniqueId;
        this.eventType = eventType;
        this.appDetails = appDetails;
        this.deviceDetails = deviceDetails;
        if (null != attributes) {
            for (final Entry<String, String> kvp : attributes.entrySet()) {
                this.addAttribute(kvp.getKey(), kvp.getValue());
            }
        }
        if (null != metrics) {
            for (final Entry<String, Double> kvp : metrics.entrySet()) {
                this.addMetric(kvp.getKey(), kvp.getValue());
            }
        }
    }

    /**
     * Creates a copy of an event with specified parameters
     *
     * @param context   The Pinpoint context
     * @param sessionId The SessionId of the new event
     * @param timestamp The timestamp of the new event
     * @param copyEvent The event to be copied
     * @return An instance of an AnalyticsEvent object
     */
    public static AnalyticsEvent createFromEvent(final PinpointContext context, final String sessionId,
            final long timestamp, final AnalyticsEvent copyEvent) {
        return new AnalyticsEvent(copyEvent.getEventId(), copyEvent.getEventType(), copyEvent.getAllAttributes(),
                copyEvent.getAllMetrics(), context.getSDKInfo(), sessionId,
                copyEvent.getSession().getSessionStart(), copyEvent.getSession().getSessionStop(),
                copyEvent.getSession().getSessionDuration(), timestamp, context.getUniqueId(),
                context.getSystem().getAppDetails(), context.getSystem().getDeviceDetails());
    }

    /**
     * Creates a new instance of an AnalyticsEvent
     *
     * @param context      The Pinpoint context
     * @param sessionId    The SessionId of the new event
     * @param sessionStart The sessionStart of the new event
     * @param sessionEnd   The sessionEnd of the new event
     * @param duration     The session duration of the new event
     * @param timestamp    The timestamp of the new event
     * @param eventType    The eventType of the new event
     * @return An instance of an AnalyticsEvent object
     */
    public static AnalyticsEvent newInstance(final PinpointContext context, final String sessionId,
            final Long sessionStart, final Long sessionEnd, Long duration, long timestamp, final String eventType) {
        return new AnalyticsEvent(eventType, null, null, context.getSDKInfo(), sessionId, sessionStart, sessionEnd,
                duration, timestamp, context.getUniqueId(), context.getSystem().getAppDetails(),
                context.getSystem().getDeviceDetails());
    }

    /**
     * Creates a new instance of an AnalyticsEvent
     *
     * @param eventId         The eventId of the new event
     * @param eventType       The eventType of the new event
     * @param attributes      A list of attributes of the new event
     * @param metrics         A list of metrics of the new event
     * @param sdkInfo         The {@link SDKInfo} of the new event
     * @param sessionId       The SessionId of the new event
     * @param sessionStart    The sessionStart of the new event
     * @param sessionStop     The sessionStop of the new event
     * @param sessionDuration The sessionDuration of the new event
     * @param timestamp       The timestamp of the new event
     * @param uniqueId        The uniqueId of the new event
     * @param appDetails      The {@link AndroidAppDetails} of the new event
     * @param deviceDetails   The {@link AndroidDeviceDetails} of the new event
     * @return An instance of an AnalyticsEvent object
     */
    public static AnalyticsEvent newInstance(final String eventId, final String eventType,
            final Map<String, String> attributes, final Map<String, Double> metrics, final SDKInfo sdkInfo,
            final String sessionId, final Long sessionStart, final Long sessionStop, final Long sessionDuration,
            final long timestamp, final String uniqueId, final AndroidAppDetails appDetails,
            final AndroidDeviceDetails deviceDetails) {
        return new AnalyticsEvent(eventId, eventType, attributes, metrics, sdkInfo, sessionId, sessionStart,
                sessionStop, sessionDuration, timestamp, uniqueId, appDetails, deviceDetails);
    }

    private static String processAttributeMetricKey(final String key) {
        final String trimmedKey = StringUtil.clipString(key, MAX_EVENT_ATTRIBUTE_METRIC_KEY_LENGTH, false);
        if (trimmedKey.length() < key.length()) {
            log.warn("The attribute key has been trimmed to a length of " + MAX_EVENT_ATTRIBUTE_METRIC_KEY_LENGTH
                    + " characters.");
        }
        return trimmedKey;
    }

    private static String processAttributeValue(final String value) {
        final String trimmedValue = StringUtil.clipString(value, MAX_EVENT_ATTRIBUTE_VALUE_LENGTH, false);
        if (trimmedValue.length() < value.length()) {
            log.warn("The attribute value has been trimmed to a length of " + MAX_EVENT_ATTRIBUTE_VALUE_LENGTH
                    + " characters.");
        }
        return trimmedValue;
    }

    /**
     * Translates an event to a JSONObject
     *
     * @param source The event to transform
     * @return A {@link JSONObject}
     */
    public static JSONObject translateFromEvent(final AnalyticsEvent source) {
        if (null == source) {
            log.warn("The Event provided was null");
            return new JSONObject();
        }

        final JSONObject json = source.toJSONObject();
        if (json.has("class")) {
            json.remove("class");
        }
        if (json.has("hashCode")) {
            json.remove("hashCode");
        }
        return json;
    }

    /**
     * Transforms a JSONObject into an event
     *
     * @param source The event as a JSONObject
     * @return An AnalyticsEvent
     * @throws JSONException
     */
    public static AnalyticsEvent translateToEvent(final JSONObject source) throws JSONException {
        final Map<String, String> attributes = new HashMap<String, String>();
        final Map<String, Double> metrics = new HashMap<String, Double>();

        final AndroidAppDetails appDetails = new AndroidAppDetails(source.optString("app_package_name"),
                source.optString("app_version_code"), source.optString("app_version_name"),
                source.optString("app_title"), source.optString(ClientContext.APP_ID_KEY));
        final SDKInfo sdkInfo = new SDKInfo(source.optString("sdk_version"), source.optString("sdk_name"));
        final AndroidDeviceDetails deviceDetails = new AndroidDeviceDetails(source.optString("carrier"));
        final String eventId = source.getString("event_id");
        final String eventType = source.getString("event_type");
        final Long timestamp = source.getLong("timestamp");
        final String uniqueId = source.getString("unique_id");

        String sessionId = "";
        Long sessionStart = null;
        Long sessionStop = null;
        Long sessionDuration = null;

        final JSONObject sessionJSON = source.getJSONObject("session");
        if (sessionJSON != null) {
            sessionId = sessionJSON.getString("id");
            sessionStart = sessionJSON.getLong("startTimestamp");
            sessionStop = sessionJSON.optLong("stopTimestamp");
            sessionDuration = sessionJSON.optLong("duration");
        }

        final JSONObject attributesJSON = source.optJSONObject("attributes");
        if (attributesJSON != null) {
            final Iterator<String> keysIterator = attributesJSON.keys();
            String key;
            while (keysIterator.hasNext()) {
                key = keysIterator.next();
                attributes.put(key, attributesJSON.optString(key));
            }
        }

        final JSONObject metricsJSON = source.optJSONObject("metrics");
        if (metricsJSON != null) {
            final Iterator<String> keysIterator = metricsJSON.keys();
            String key;
            while (keysIterator.hasNext()) {
                key = keysIterator.next();
                try {
                    metrics.put(key, metricsJSON.getDouble(key));
                } catch (final JSONException e) {
                    // Do not log e due to potentially sensitive information
                    log.error("Failed to convert metric back to double from JSON value.");
                }
            }
        }

        return AnalyticsEvent.newInstance(eventId, eventType, attributes, metrics, sdkInfo, sessionId, sessionStart,
                sessionStop, sessionDuration, timestamp, uniqueId, appDetails, deviceDetails);
    }

    /**
     * Returns the eventId
     *
     * @return the eventId
     */
    public String getEventId() {
        return this.eventId;
    }

    /**
     * Adds an attribute to this {@link AnalyticsEvent} with the specified key.
     * Only 40 attributes/metrics are allowed to be added to an Event. If 40
     * attribute/metrics already exist on this Event, the call may be ignored.
     *
     * @param name  The name of the attribute. The name will be truncated if it
     *              exceeds 50 characters.
     * @param value The value of the attribute. The value will be truncated if
     *              it exceeds 200 characters.
     */
    public void addAttribute(final String name, final String value) {
        if (null == name) {
            return;
        }

        if (null != value) {
            if (currentNumOfAttributesAndMetrics.get() < MAX_NUM_OF_METRICS_AND_ATTRIBUTES) {
                attributes.put(this.processAttributeMetricKey(name), processAttributeValue(value));
                currentNumOfAttributesAndMetrics.incrementAndGet();
            } else {
                log.warn("Max number of attributes/metrics reached(" + MAX_NUM_OF_METRICS_AND_ATTRIBUTES + ").");
            }
        } else {
            attributes.remove(name);
        }
    }

    /**
     * Determines if this {@link AnalyticsEvent} contains a specific attribute
     *
     * @param attributeName The name of the attribute
     * @return true if this {@link AnalyticsEvent} has an attribute with the
     * specified name, false otherwise
     */
    public boolean hasAttribute(final String attributeName) {
        if (attributeName == null) {
            return false;
        }
        return attributes.containsKey(attributeName);
    }

    /**
     * Adds a metric to this {@link AnalyticsEvent} with the specified key. Only
     * 40 attributes/metrics are allowed to be added to an Event. If 50
     * attribute/metrics already exist on this Event, the call may be ignored.
     *
     * @param name  The name of the metric. The name will be truncated if it
     *              exceeds 50 characters.
     * @param value The value of the metric.
     */
    public void addMetric(final String name, final Double value) {
        if (null == name) {
            return;
        }

        if (null != value) {
            if (currentNumOfAttributesAndMetrics.get() < MAX_NUM_OF_METRICS_AND_ATTRIBUTES) {
                metrics.put(this.processAttributeMetricKey(name), value);
                currentNumOfAttributesAndMetrics.incrementAndGet();
            } else {
                log.warn("Max number of attributes/metrics reached(" + MAX_NUM_OF_METRICS_AND_ATTRIBUTES + ").");
            }
        } else {
            metrics.remove(name);
        }
    }

    /**
     * Determines if this {@link AnalyticsEvent} contains a specific metric.
     *
     * @param metricName The name of the metric
     * @return true if this {@link AnalyticsEvent} has a metric with the
     * specified name, false otherwise
     */
    public boolean hasMetric(final String metricName) {
        if (metricName == null) {
            return false;
        }
        return metrics.containsKey(metricName);
    }

    /**
     * Returns the name/type of this {@link AnalyticsEvent}
     *
     * @return the name/type of this {@link AnalyticsEvent}
     */
    public String getEventType() {
        return this.eventType;
    }

    /**
     * Returns the value of the attribute with the specified name.
     *
     * @param name The name of the attribute to return
     * @return The attribute with the specified name, or null if attribute does
     * not exist
     */
    public String getAttribute(final String name) {
        if (name == null) {
            return null;
        }
        return attributes.get(name);
    }

    /**
     * Returns the value of the metric with the specified name.
     *
     * @param name The name of the metric to return
     * @return The metric with the specified name, or null if metric does not
     * exist
     */
    public Double getMetric(final String name) {
        if (name == null) {
            return null;
        }
        return metrics.get(name);
    }

    public PinpointSession getSession() {
        return session;
    }

    public Long getEventTimestamp() {
        return timestamp;
    }

    public String getUniqueId() {
        return uniqueId;
    }

    public String getSdkName() {
        return sdkName;
    }

    public String getSdkVersion() {
        return sdkVersion;
    }

    /**
     * Adds an attribute to this {@link AnalyticsEvent} with the specified key.
     * Only 40 attributes/metrics are allowed to be added to an
     * {@link AnalyticsEvent}. If 40 attribute/metrics already exist on this
     * {@link AnalyticsEvent}, the call may be ignored.
     *
     * @param name  The name of the attribute. The name will be truncated if it
     *              exceeds 50 characters.
     * @param value The value of the attribute. The value will be truncated if
     *              it exceeds 200 characters.
     * @return The same {@link AnalyticsEvent} instance is returned to allow for
     * method chaining.
     */
    public AnalyticsEvent withAttribute(String name, String value) {
        addAttribute(name, value);
        return this;
    }

    /**
     * Adds a metric to this {@link AnalyticsEvent} with the specified key. Only
     * 40 attributes/metrics are allowed to be added to an
     * {@link AnalyticsEvent}. If 40 attribute/metrics already exist on this
     * {@link AnalyticsEvent}, the call may be ignored.
     *
     * @param name  The name of the metric. The name will be truncated if it
     *              exceeds 50 characters.
     * @param value The value of the metric.
     * @return The same {@link AnalyticsEvent} instance is returned to allow for
     * method chaining.
     */
    public AnalyticsEvent withMetric(String name, Double value) {
        addMetric(name, value);
        return this;
    }

    /**
     * Returns a map of all attributes contained within this
     * {@link AnalyticsEvent}
     *
     * @return a map of all attributes, where the attribute names are the keys
     * and the attribute values are the values
     */
    public Map<String, String> getAllAttributes() {
        return Collections.unmodifiableMap(attributes);
    }

    /**
     * Returns a map of all metrics contained within this {@link AnalyticsEvent}
     *
     * @return a map of all metrics, where the metric names are the keys and the
     * metric values are the values
     */
    public Map<String, Double> getAllMetrics() {
        return Collections.unmodifiableMap(metrics);
    }

    @Override
    public String toString() {
        final JSONObject json = toJSONObject();
        try {
            return json.toString(INDENTATION);
        } catch (final JSONException e) {
            return json.toString();
        }
    }

    @Override
    public JSONObject toJSONObject() {
        final Locale locale = this.deviceDetails.locale();
        final String localeString = locale != null ? locale.toString() : "UNKNOWN";

        final JSONBuilder builder = new JSONBuilder(this);

        // ****************************************************
        // ==================System Attributes=================
        // ****************************************************
        builder.withAttribute("event_id", getEventId());
        builder.withAttribute("event_type", getEventType());
        builder.withAttribute("unique_id", getUniqueId());
        builder.withAttribute("timestamp", getEventTimestamp());

        // ****************************************************
        // ==============Device Details Attributes=============
        // ****************************************************
        builder.withAttribute("platform", this.deviceDetails.platform());
        builder.withAttribute("platform_version", this.deviceDetails.platformVersion());
        builder.withAttribute("make", this.deviceDetails.manufacturer());
        builder.withAttribute("model", this.deviceDetails.model());
        builder.withAttribute("locale", localeString);
        builder.withAttribute("carrier", this.deviceDetails.carrier());

        // ****************************************************
        // ==============Session Attributes=============
        // ****************************************************
        final JSONObject sessionObject = new JSONObject();
        try {
            sessionObject.put("id", session.getSessionId());
            if (session.getSessionStart() != null) {
                sessionObject.put("startTimestamp", session.getSessionStart());
            }
            if (session.getSessionStop() != null) {
                sessionObject.put("stopTimestamp", session.getSessionStop());
            }
            if (session.getSessionDuration() != null) {
                sessionObject.put("duration", session.getSessionDuration().longValue());
            }
        } catch (final JSONException e) {
            log.error("Error serializing session information", e);
        }
        builder.withAttribute("session", sessionObject);

        // ****************************************************
        // ====SDK Details Attributes -- Prefix with 'sdk_'====
        // ****************************************************
        builder.withAttribute("sdk_version", this.sdkVersion);
        builder.withAttribute("sdk_name", this.sdkName);

        // ****************************************************
        // Application Details Attributes -- Prefix with 'app_'
        // ****************************************************
        builder.withAttribute("app_version_name", this.appDetails.versionName());
        builder.withAttribute("app_version_code", this.appDetails.versionCode());
        builder.withAttribute("app_package_name", this.appDetails.packageName());
        builder.withAttribute("app_title", this.appDetails.getAppTitle());
        builder.withAttribute(ClientContext.APP_ID_KEY, this.appDetails.getAppId());

        final JSONObject attributesJson = new JSONObject();
        for (final Entry<String, String> entry : getAllAttributes().entrySet()) {
            try {
                attributesJson.put(entry.getKey(), entry.getValue());
            } catch (final JSONException e) {
                // Do not log e due to potentially sensitive information
                log.error("Error serializing attribute for eventType: " + eventType);
            }
        }

        final JSONObject metricsJson = new JSONObject();
        for (final Entry<String, Double> entry : getAllMetrics().entrySet()) {
            try {
                metricsJson.put(entry.getKey(), entry.getValue());
            } catch (final JSONException e) {
                // Do not log e due to potentially sensitive information
                log.error("Error serializing metric for eventType: " + eventType);
            }
        }

        // If there are any attributes put then add the attributes to the
        // structure
        if (attributesJson.length() > 0) {
            builder.withAttribute("attributes", attributesJson);
        }

        // If there are any metrics put then add the attributes to the structure
        if (metricsJson.length() > 0) {
            builder.withAttribute("metrics", metricsJson);
        }
        return builder.toJSONObject();
    }

    /**
     * Creates a ClientContext object
     *
     * @param networkType The network type from the Pinpoint Context
     * @return Returns a {@link ClientContext} object
     */
    public ClientContext createClientContext(final String networkType) {
        final ClientContext.ClientContextBuilder builder = new ClientContext.ClientContextBuilder();
        builder.withAppPackageName(appDetails.packageName()).withAppVersionCode(appDetails.versionCode())
                .withAppVersionName(appDetails.versionName()).withLocale(deviceDetails.locale().toString())
                .withMake(deviceDetails.manufacturer()).withModel(deviceDetails.model())
                .withPlatformVersion(deviceDetails.platformVersion()).withUniqueId(uniqueId)
                .withAppTitle(appDetails.getAppTitle()).withNetworkType(networkType)
                .withCarrier(deviceDetails.carrier()).withAppId(appDetails.getAppId());
        return builder.build();
    }
}