Clover icon

Coverage Report

  1. Project Clover database Wed Nov 13 2024 16:21:17 GMT
  2. Package jalview.analytics

File Plausible.java

 

Coverage histogram

../../img/srcFileCovDistChart0.png
59% of files have more coverage

Code metrics

70
211
29
1
602
489
84
0.4
7.28
29
2.9

Classes

Class Line # Actions
Plausible 49 211 84
0.00%
 

Contributing tests

No tests hitting this source file were found.

Source view

1    /*
2    * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
3    * Copyright (C) $$Year-Rel$$ The Jalview Authors
4    *
5    * This file is part of Jalview.
6    *
7    * Jalview is free software: you can redistribute it and/or
8    * modify it under the terms of the GNU General Public License
9    * as published by the Free Software Foundation, either version 3
10    * of the License, or (at your option) any later version.
11    *
12    * Jalview is distributed in the hope that it will be useful, but
13    * WITHOUT ANY WARRANTY; without even the implied warranty
14    * of MERCHANTABILITY or FITNESS FOR A PARTICULAR
15    * PURPOSE. See the GNU General Public License for more details.
16    *
17    * You should have received a copy of the GNU General Public License
18    * along with Jalview. If not, see <http://www.gnu.org/licenses/>.
19    * The Jalview Authors are detailed in the 'AUTHORS' file.
20    */
21    package jalview.analytics;
22   
23    import java.io.BufferedReader;
24    import java.io.IOException;
25    import java.io.InputStreamReader;
26    import java.io.OutputStream;
27    import java.io.UnsupportedEncodingException;
28    import java.lang.invoke.MethodHandles;
29    import java.net.HttpURLConnection;
30    import java.net.MalformedURLException;
31    import java.net.URL;
32    import java.net.URLEncoder;
33    import java.nio.charset.StandardCharsets;
34    import java.util.AbstractMap;
35    import java.util.ArrayList;
36    import java.util.Collections;
37    import java.util.HashMap;
38    import java.util.Iterator;
39    import java.util.List;
40    import java.util.Map;
41    import java.util.Random;
42   
43    import jalview.bin.Cache;
44    import jalview.bin.Console;
45    import jalview.util.ChannelProperties;
46    import jalview.util.HttpUtils;
47    import jalview.util.UserAgent;
48   
 
49    public class Plausible
50    {
51    private static final String USER_AGENT;
52   
53    private static final String JALVIEW_ID = "Jalview Desktop";
54   
55    private static final String DOMAIN = "jalview.org";
56   
57    private static final String CONFIG_API_BASE_URL = "https://www.jalview.org/services/config/analytics/url";
58   
59    private static final String DEFAULT_API_BASE_URL = "https://analytics.jalview.org/api/event";
60   
61    private static final String API_BASE_URL;
62   
63    private static final String clientId;
64   
65    public static final String APPLICATION_BASE_URL = "desktop://localhost";
66   
67    private List<Map.Entry<String, String>> queryStringValues;
68   
69    private List<Map.Entry<String, Object>> jsonObject;
70   
71    private List<Map.Entry<String, String>> cookieValues;
72   
73    private static boolean ENABLED = false;
74   
75    private static boolean DEBUG = true;
76   
77    private static Plausible instance = null;
78   
79    private static final Map<String, String> defaultProps;
80   
 
81  0 toggle static
82    {
83  0 defaultProps = new HashMap<>();
84  0 defaultProps.put("app_name",
85    ChannelProperties.getProperty("app_name") + " Desktop");
86  0 defaultProps.put("version", Cache.getProperty("VERSION"));
87  0 defaultProps.put("build_date",
88    Cache.getDefault("BUILD_DATE", "unknown"));
89  0 defaultProps.put("java_version", System.getProperty("java.version"));
90  0 String val = System.getProperty("sys.install4jVersion");
91  0 if (val != null)
92    {
93  0 defaultProps.put("install4j_version", val);
94    }
95  0 val = System.getProperty("installer.template_version");
96  0 if (val != null)
97    {
98  0 defaultProps.put("install4j_template_version", val);
99    }
100  0 val = System.getProperty("launcher.version");
101  0 if (val != null)
102    {
103  0 defaultProps.put("launcher.version", val);
104    }
105  0 defaultProps.put("java_arch",
106    System.getProperty("os.arch") + " "
107    + System.getProperty("os.name") + " "
108    + System.getProperty("os.version"));
109  0 defaultProps.put("os", System.getProperty("os.name"));
110  0 defaultProps.put("os_version", System.getProperty("os.version"));
111  0 defaultProps.put("os_arch", System.getProperty("os.arch"));
112  0 String installation = Cache.applicationProperties
113    .getProperty("INSTALLATION");
114  0 if (installation != null)
115    {
116  0 defaultProps.put("installation", installation);
117    }
118   
119    // ascertain the API_BASE_URL
120  0 API_BASE_URL = getAPIBaseURL();
121   
122    // random clientId to make User-Agent unique (to register analytic)
123  0 clientId = String.format("%08x", new Random().nextInt());
124   
125  0 USER_AGENT = UserAgent.getUserAgent(
126    MethodHandles.lookup().lookupClass().getCanonicalName() + " "
127    + clientId);
128    }
129   
 
130  0 toggle private Plausible()
131    {
132  0 this.resetLists();
133    }
134   
 
135  0 toggle public static void setEnabled(boolean b)
136    {
137  0 ENABLED = b;
138    }
139   
 
140  0 toggle public void sendEvent(String eventName, String urlString,
141    String... propsStrings)
142    {
143  0 sendEvent(eventName, urlString, false, propsStrings);
144    }
145   
146    /**
147    * The simplest way to send an analytic event.
148    *
149    * @param eventName
150    * The event name. To emulate a webpage view use "pageview" and set a
151    * "url" key/value. See https://plausible.io/docs/events-api
152    * @param sendDefaultProps
153    * Flag whether to add the default props about the application.
154    * @param propsStrings
155    * Optional multiple Strings in key, value pairs (there should be an
156    * even number of propsStrings) to be set as property of the event.
157    * To emulate a webpage view set "url" as the URL in a "pageview"
158    * event.
159    */
 
160  0 toggle public void sendEvent(String eventName, String urlString,
161    boolean sendDefaultProps, String... propsStrings)
162    {
163    // clear out old lists
164  0 this.resetLists();
165   
166  0 if (!ENABLED)
167    {
168  0 Console.debug("Plausible not enabled.");
169  0 return;
170    }
171  0 Map<String, String> props = new HashMap<>();
172   
173    // add these to all events from this application instance
174  0 if (sendDefaultProps)
175    {
176  0 props.putAll(defaultProps);
177    }
178   
179    // add (and overwrite with) the passed in props
180  0 if (propsStrings != null && propsStrings.length > 0)
181    {
182  0 if (propsStrings.length % 2 != 0)
183    {
184  0 Console.warn(
185    "Cannot addEvent with odd number of propsStrings. Ignoring the last one.");
186    }
187  0 for (int i = 0; i < propsStrings.length - 1; i += 2)
188    {
189  0 String key = propsStrings[i];
190  0 String value = propsStrings[i + 1];
191  0 props.put(key, value);
192    }
193    }
194   
195  0 addJsonValue("domain", DOMAIN);
196  0 addJsonValue("name", eventName);
197  0 StringBuilder eventUrlSb = new StringBuilder(APPLICATION_BASE_URL);
198  0 if (!APPLICATION_BASE_URL.endsWith("/") && !urlString.startsWith("/"))
199    {
200  0 eventUrlSb.append("/");
201    }
202  0 eventUrlSb.append(urlString);
203  0 addJsonValue("url", eventUrlSb.toString());
204  0 addJsonObject("props", props);
205  0 StringBuilder urlSb = new StringBuilder();
206  0 urlSb.append(API_BASE_URL);
207  0 String qs = buildQueryString();
208  0 if (qs != null && qs.length() > 0)
209    {
210  0 urlSb.append('?');
211  0 urlSb.append(qs);
212    }
213  0 try
214    {
215  0 URL url = new URL(urlSb.toString());
216  0 HttpURLConnection httpURLConnection = (HttpURLConnection) HttpUtils
217    .openConnection(url);
218  0 httpURLConnection.setRequestMethod("POST");
219  0 httpURLConnection.setDoOutput(true);
220   
221  0 String jsonString = buildJson();
222   
223  0 Console.debug(
224    "Plausible: HTTP Request is: '" + urlSb.toString() + "'");
225  0 if (DEBUG)
226    {
227  0 Console.debug("Plausible: User-Agent is: '" + USER_AGENT + "'");
228    }
229  0 Console.debug("Plausible: POSTed JSON is:\n" + jsonString);
230   
231  0 byte[] jsonBytes = jsonString.getBytes(StandardCharsets.UTF_8);
232  0 int jsonLength = jsonBytes.length;
233   
234  0 httpURLConnection.setFixedLengthStreamingMode(jsonLength);
235  0 httpURLConnection.setRequestProperty("Content-Type",
236    "application/json");
237  0 httpURLConnection.setRequestProperty("User-Agent", USER_AGENT);
238  0 httpURLConnection.connect();
239  0 try (OutputStream os = httpURLConnection.getOutputStream())
240    {
241  0 os.write(jsonBytes);
242    }
243  0 int responseCode = httpURLConnection.getResponseCode();
244  0 String responseMessage = httpURLConnection.getResponseMessage();
245   
246  0 if (responseCode < 200 || responseCode > 299)
247    {
248  0 Console.warn("Plausible connection failed: '" + responseCode + " "
249    + responseMessage + "'");
250    }
251    else
252    {
253  0 Console.debug("Plausible connection succeeded: '" + responseCode
254    + " " + responseMessage + "'");
255    }
256   
257  0 if (DEBUG)
258    {
259  0 BufferedReader br = new BufferedReader(new InputStreamReader(
260    (httpURLConnection.getInputStream())));
261  0 StringBuilder sb = new StringBuilder();
262  0 String response;
263  0 while ((response = br.readLine()) != null)
264    {
265  0 sb.append(response);
266    }
267  0 String body = sb.toString();
268  0 Console.debug("Plausible response content:\n" + body);
269    }
270    } catch (MalformedURLException e)
271    {
272  0 Console.debug(
273    "Somehow the Plausible BASE_URL and queryString is malformed: '"
274    + urlSb.toString() + "'",
275    e);
276  0 return;
277    } catch (IOException e)
278    {
279  0 Console.debug("Connection to Plausible BASE_URL '" + API_BASE_URL
280    + "' failed.", e);
281    } catch (ClassCastException e)
282    {
283  0 Console.debug(
284    "Couldn't cast URLConnection to HttpURLConnection in Plausible.",
285    e);
286    }
287    }
288   
 
289  0 toggle private void addJsonObject(String key, Map<String, String> map)
290    {
291  0 List<Map.Entry<String, ? extends Object>> list = new ArrayList<>();
292  0 for (String k : map.keySet())
293    {
294  0 list.add(stringEntry(k, map.get(k)));
295    }
296  0 addJsonObject(key, list);
297   
298    }
299   
 
300  0 toggle private void addJsonObject(String key,
301    List<Map.Entry<String, ? extends Object>> object)
302    {
303  0 jsonObject.add(objectEntry(key, object));
304    }
305   
 
306  0 toggle private void addJsonValues(String key, List<Object> values)
307    {
308  0 jsonObject.add(objectEntry(key, values));
309    }
310   
 
311  0 toggle private void addJsonValue(String key, String value)
312    {
313  0 jsonObject.add(objectEntry(key, value));
314    }
315   
 
316  0 toggle private void addJsonValue(String key, int value)
317    {
318  0 jsonObject.add(objectEntry(key, Integer.valueOf(value)));
319    }
320   
 
321  0 toggle private void addJsonValue(String key, boolean value)
322    {
323  0 jsonObject.add(objectEntry(key, Boolean.valueOf(value)));
324    }
325   
 
326  0 toggle private void addQueryStringValue(String key, String value)
327    {
328  0 queryStringValues.add(stringEntry(key, value));
329    }
330   
 
331  0 toggle private void addCookieValue(String key, String value)
332    {
333  0 cookieValues.add(stringEntry(key, value));
334    }
335   
 
336  0 toggle private void resetLists()
337    {
338  0 jsonObject = new ArrayList<>();
339  0 queryStringValues = new ArrayList<>();
340  0 cookieValues = new ArrayList<>();
341    }
342   
 
343  0 toggle public static Plausible getInstance()
344    {
345  0 if (instance == null)
346    {
347  0 instance = new Plausible();
348    }
349  0 return instance;
350    }
351   
 
352  0 toggle public static void reset()
353    {
354  0 getInstance().resetLists();
355    }
356   
 
357  0 toggle private String buildQueryString()
358    {
359  0 StringBuilder sb = new StringBuilder();
360  0 for (Map.Entry<String, String> entry : queryStringValues)
361    {
362  0 if (sb.length() > 0)
363    {
364  0 sb.append('&');
365    }
366  0 try
367    {
368  0 sb.append(URLEncoder.encode(entry.getKey(), "UTF-8"));
369    } catch (UnsupportedEncodingException e)
370    {
371  0 sb.append(entry.getKey());
372    }
373  0 sb.append('=');
374  0 try
375    {
376  0 sb.append(URLEncoder.encode(entry.getValue(), "UTF-8"));
377    } catch (UnsupportedEncodingException e)
378    {
379  0 sb.append(entry.getValue());
380    }
381    }
382  0 return sb.toString();
383    }
384   
 
385  0 toggle private void buildCookieHeaders()
386    {
387    // TODO not needed yet
388    }
389   
 
390  0 toggle private String buildJson()
391    {
392  0 StringBuilder sb = new StringBuilder();
393  0 addJsonObject(sb, 0, jsonObject);
394  0 return sb.toString();
395    }
396   
 
397  0 toggle private void addJsonObject(StringBuilder sb, int indent,
398    List<Map.Entry<String, Object>> entries)
399    {
400  0 indent(sb, indent);
401  0 sb.append('{');
402  0 newline(sb);
403  0 Iterator<Map.Entry<String, Object>> entriesI = entries.iterator();
404  0 while (entriesI.hasNext())
405    {
406  0 Map.Entry<String, Object> entry = entriesI.next();
407  0 String key = entry.getKey();
408    // TODO sensibly escape " characters in key
409  0 Object value = entry.getValue();
410  0 indent(sb, indent + 1);
411  0 sb.append('"').append(quoteEscape(key)).append('"').append(':');
412  0 space(sb);
413  0 if (value != null && value instanceof List)
414    {
415  0 newline(sb);
416    }
417  0 addJsonValue(sb, indent + 2, value);
418  0 if (entriesI.hasNext())
419    {
420  0 sb.append(',');
421    }
422  0 newline(sb);
423    }
424  0 indent(sb, indent);
425  0 sb.append('}');
426    }
427   
 
428  0 toggle private void addJsonValue(StringBuilder sb, int indent, Object value)
429    {
430  0 if (value == null)
431    {
432  0 return;
433    }
434  0 try
435    {
436  0 if (value instanceof Map.Entry)
437    {
438  0 Map.Entry<String, Object> entry = (Map.Entry<String, Object>) value;
439  0 List<Map.Entry<String, Object>> object = new ArrayList<>();
440  0 object.add(entry);
441  0 addJsonObject(sb, indent, object);
442    }
443  0 else if (value instanceof List)
444    {
445    // list of Map.Entries or list of values?
446  0 List<Object> valueList = (List<Object>) value;
447  0 if (valueList.size() > 0 && valueList.get(0) instanceof Map.Entry)
448    {
449    // entries
450    // indent(sb, indent);
451  0 List<Map.Entry<String, Object>> entryList = (List<Map.Entry<String, Object>>) value;
452  0 addJsonObject(sb, indent, entryList);
453    }
454    else
455    {
456    // values
457  0 indent(sb, indent);
458  0 sb.append('[');
459  0 newline(sb);
460  0 Iterator<Object> valueListI = valueList.iterator();
461  0 while (valueListI.hasNext())
462    {
463  0 Object v = valueListI.next();
464  0 addJsonValue(sb, indent + 1, v);
465  0 if (valueListI.hasNext())
466    {
467  0 sb.append(',');
468    }
469  0 newline(sb);
470    }
471  0 indent(sb, indent);
472  0 sb.append("]");
473    }
474    }
475  0 else if (value instanceof String)
476    {
477  0 sb.append('"').append(quoteEscape((String) value)).append('"');
478    }
479  0 else if (value instanceof Integer)
480    {
481  0 sb.append(((Integer) value).toString());
482    }
483  0 else if (value instanceof Boolean)
484    {
485  0 sb.append('"').append(((Boolean) value).toString()).append('"');
486    }
487    } catch (ClassCastException e)
488    {
489  0 Console.debug(
490    "Could not deal with type of json Object " + value.toString(),
491    e);
492    }
493    }
494   
 
495  0 toggle private static String quoteEscape(String s)
496    {
497  0 if (s == null)
498    {
499  0 return null;
500    }
501    // this escapes quotation marks (") that aren't already escaped (in the
502    // string) ready to go into a quoted JSON string value
503  0 return s.replaceAll("((?<!\\\\)(?:\\\\{2})*)\"", "$1\\\\\"");
504    }
505   
 
506  0 toggle private static void prettyWhitespace(StringBuilder sb, String whitespace,
507    int repeat)
508    {
509    // only add whitespace if we're in DEBUG mode
510  0 if (!Console.getLogger().isDebugEnabled())
511    {
512  0 return;
513    }
514  0 if (repeat >= 0 && whitespace != null)
515    {
516    // sb.append(whitespace.repeat(repeat));
517  0 sb.append(String.join("", Collections.nCopies(repeat, whitespace)));
518   
519    }
520    else
521    {
522  0 sb.append(whitespace);
523    }
524    }
525   
 
526  0 toggle private static void indent(StringBuilder sb, int indent)
527    {
528  0 prettyWhitespace(sb, " ", indent);
529    }
530   
 
531  0 toggle private static void newline(StringBuilder sb)
532    {
533  0 prettyWhitespace(sb, "\n", -1);
534    }
535   
 
536  0 toggle private static void space(StringBuilder sb)
537    {
538  0 prettyWhitespace(sb, " ", -1);
539    }
540   
 
541  0 toggle protected static Map.Entry<String, Object> objectEntry(String s, Object o)
542    {
543  0 return new AbstractMap.SimpleEntry<String, Object>(s, o);
544    }
545   
 
546  0 toggle protected static Map.Entry<String, String> stringEntry(String s, String v)
547    {
548  0 return new AbstractMap.SimpleEntry<String, String>(s, v);
549    }
550   
 
551  0 toggle private static String getAPIBaseURL()
552    {
553  0 try
554    {
555  0 URL url = new URL(CONFIG_API_BASE_URL);
556  0 HttpURLConnection httpURLConnection = (HttpURLConnection) HttpUtils
557    .openConnection(url);
558  0 httpURLConnection.setRequestMethod("GET");
559  0 httpURLConnection.setRequestProperty("User-Agent", USER_AGENT);
560  0 httpURLConnection.setConnectTimeout(5000);
561  0 httpURLConnection.setReadTimeout(3000);
562  0 httpURLConnection.connect();
563  0 int responseCode = httpURLConnection.getResponseCode();
564  0 String responseMessage = httpURLConnection.getResponseMessage();
565   
566  0 if (responseCode < 200 || responseCode > 299)
567    {
568  0 Console.warn("Config URL connection to '" + CONFIG_API_BASE_URL
569    + "' failed: '" + responseCode + " " + responseMessage
570    + "'");
571    }
572   
573  0 BufferedReader br = new BufferedReader(
574    new InputStreamReader((httpURLConnection.getInputStream())));
575  0 StringBuilder sb = new StringBuilder();
576  0 String response;
577  0 while ((response = br.readLine()) != null)
578    {
579  0 sb.append(response);
580    }
581  0 if (sb.length() > 7 && sb.substring(0, 5).equals("https"))
582    {
583  0 return sb.toString();
584    }
585   
586    } catch (MalformedURLException e)
587    {
588  0 Console.debug("Somehow the config URL is malformed: '"
589    + CONFIG_API_BASE_URL + "'", e);
590    } catch (IOException e)
591    {
592  0 Console.debug("Connection to Plausible BASE_URL '" + API_BASE_URL
593    + "' failed.", e);
594    } catch (ClassCastException e)
595    {
596  0 Console.debug(
597    "Couldn't cast URLConnection to HttpURLConnection in Plausible.",
598    e);
599    }
600  0 return DEFAULT_API_BASE_URL;
601    }
602    }