package jalview.analytics; import java.io.IOException; import java.io.OutputStream; import java.net.HttpURLConnection; import java.net.MalformedURLException; import java.net.URL; import java.net.URLConnection; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; import java.util.AbstractMap; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Random; import java.util.UUID; import jalview.bin.Console; public class GoogleAnalytics4 { private static final String JALVIEW_ID = "Jalview Desktop"; private static final String SESSION_ID = new Random().toString(); private static final String MEASUREMENT_ID = "G-6TMPHMXEQ0"; private static final String API_SECRET = "Qb9NSbqkRDqizG6j2BBJ2g"; // This will generate a different CLIENT_ID each time the application is // launched. Do we want to store it in .jalview_properties? private static final String CLIENT_ID = UUID.randomUUID().toString(); private static final String BASE_URL = "https://www.google-analytics.com/mp/collect"; private List> queryStringValues; private List> jsonObject; private List events; private List> cookieValues; private static boolean ENABLED = false; public GoogleAnalytics4() { this.reset(); } private static void setEnabled(boolean b) { ENABLED = b; } public void sendAnalytics() { sendAnalytics(null); } public void sendAnalytics(String path) { if (!ENABLED) { Console.debug("GoogleAnalytics4 not enabled."); return; } if (path != null) { addEvent("page_event", path); } addQueryStringValue("measurement_id", MEASUREMENT_ID); addQueryStringValue("api_secret", API_SECRET); addJsonValue("client_id", CLIENT_ID); addJsonValues("events", Event.toObjectList(events)); // addJsonValue("events", ) StringBuilder sb = new StringBuilder(); sb.append(BASE_URL); sb.append('?'); sb.append(buildQueryString()); try { URL url = new URL(sb.toString()); URLConnection urlConnection = url.openConnection(); HttpURLConnection httpURLConnection = (HttpURLConnection) urlConnection; httpURLConnection.setRequestMethod("POST"); httpURLConnection.setDoOutput(true); byte[] jsonBytes = buildJson().getBytes(StandardCharsets.UTF_8); int jsonLength = jsonBytes.length; httpURLConnection.setFixedLengthStreamingMode(jsonLength); httpURLConnection.setRequestProperty("Content-Type", "application/json; charset=UTF-8"); httpURLConnection.connect(); try (OutputStream os = httpURLConnection.getOutputStream()) { os.write(jsonBytes); } int responseCode = httpURLConnection.getResponseCode(); String responseMessage = httpURLConnection.getResponseMessage(); if (responseCode < 200 || responseCode > 299) { Console.warn("GoogleAnalytics4 connection failed: '" + responseCode + " " + responseMessage + "'"); } else { Console.debug("GoogleAnalytics4 connection succeeded: '" + responseCode + " " + responseMessage + "'"); } } catch (MalformedURLException e) { Console.debug( "Somehow the GoogleAnalytics4 BASE_URL and queryString is malformed.", e); return; } catch (IOException e) { Console.debug("Connection to GoogleAnalytics4 BASE_URL '" + BASE_URL + "' failed.", e); } catch (ClassCastException e) { Console.debug( "Couldn't cast URLConnection to HttpURLConnection in GoogleAnalytics4.", e); } } public void addEvent(String name, String... paramsStrings) { if (paramsStrings.length % 2 != 0) { Console.error( "Cannot addEvent with odd number of paramsStrings. Ignoring."); return; } Event event = new Event(name); for (int i = 0; i < paramsStrings.length - 1; i += 2) { String key = paramsStrings[i]; String value = paramsStrings[i + 1]; event.addParam(key, value); } events.add(event); } private void addJsonObject(String key, List> object) { jsonObject.add(objectEntry(key, object)); } private void addJsonValues(String key, List values) { jsonObject.add(objectEntry(key, values)); } private void addJsonValue(String key, String value) { jsonObject.add(objectEntry(key, value)); } private void addJsonValue(String key, int value) { jsonObject.add(objectEntry(key, Integer.valueOf(value))); } private void addJsonValue(String key, boolean value) { jsonObject.add(objectEntry(key, Boolean.valueOf(value))); } private void addQueryStringValue(String key, String value) { queryStringValues.add(stringEntry(key, value)); } private void addCookieValue(String key, String value) { cookieValues.add(stringEntry(key, value)); } public void reset() { jsonObject = new ArrayList<>(); events = new ArrayList(); queryStringValues = new ArrayList<>(); cookieValues = new ArrayList<>(); } private String buildQueryString() { StringBuilder sb = new StringBuilder(); for (Map.Entry entry : queryStringValues) { if (sb.length() > 0) { sb.append('&'); } sb.append(URLEncoder.encode(entry.getKey(), StandardCharsets.UTF_8)); sb.append('='); sb.append( URLEncoder.encode(entry.getValue(), StandardCharsets.UTF_8)); } return sb.toString(); } private void buildCookieHeaders() { // TODO not needed yet } private String buildJson() { StringBuilder sb = new StringBuilder(); addJsonObject(sb, 0, jsonObject); return sb.toString(); } private void addJsonObject(StringBuilder sb, int indent, List> entries) { indent(sb, indent); sb.append("{\n"); for (Map.Entry entry : entries) { String key = entry.getKey(); // TODO sensibly escape " characters in key Object value = entry.getValue(); indent(sb, indent); sb.append('"').append(key).append('"'); sb.append(": "); if (List.class.equals(value.getClass())) { sb.append('\n'); } addJsonValue(sb, indent + 1, value); sb.append(",\n"); } sb.append("}\n"); } private void addJsonValue(StringBuilder sb, int indent, Object value) { if (value == null) { return; } try { Class c = value.getClass(); if (Map.Entry.class.equals(c)) { Map.Entry entry = (Map.Entry) value; List> object = new ArrayList<>(); object.add(entry); addJsonObject(sb, indent + 1, object); } else if (List.class.equals(c)) { // list of Map.Entries or list of values? List valueList = (List) value; if (valueList.size() > 0 && Map.Entry.class.equals(valueList.get(0).getClass())) { // entries indent(sb, indent); List> entryList = (List>) value; addJsonObject(sb, indent + 1, entryList); } else { // values indent(sb, indent); sb.append("[\n"); for (Object v : valueList) { indent(sb, indent + 1); addJsonValue(sb, indent + 1, v); sb.append(",\n"); } indent(sb, indent); sb.append("]"); } } else if (String.class.equals(c)) { sb.append('"'); sb.append((String) value); sb.append('"'); } else if (Integer.class.equals(c)) { sb.append(((Integer) value).toString()); } else if (Boolean.class.equals(c)) { sb.append(((Boolean) value).toString()); } } catch (ClassCastException e) { Console.debug( "Could not deal with type of jsonObject " + value.toString(), e); } } private void indent(StringBuilder sb, int indent) { sb.append(" ".repeat(indent)); } protected static Map.Entry objectEntry(String s, Object o) { return new AbstractMap.SimpleEntry(s, o); } protected static Map.Entry stringEntry(String s, String v) { return new AbstractMap.SimpleEntry(s, v); } } class Event { private String name; private List> params; @SafeVarargs public Event(String name, Map.Entry... paramEntries) { this.name = name; this.params = new ArrayList>(); for (Map.Entry paramEntry : paramEntries) { if (paramEntry == null) { continue; } params.add(paramEntry); } } public void addParam(String param, String value) { params.add(GoogleAnalytics4.stringEntry(param, value)); } protected List> toObject() { List> object = new ArrayList<>(); object.add(GoogleAnalytics4.objectEntry("name", (Object) name)); if (params.size() > 0) { object.add(GoogleAnalytics4.objectEntry("params", (Object) params)); } return object; } protected static List toObjectList(List events) { List eventObjectList = new ArrayList<>(); for (Event event : events) { eventObjectList.add((Object) event.toObject()); } return eventObjectList; } }