Class |
Line # |
Actions |
|||
---|---|---|---|---|---|
Plausible | 49 | 211 | 84 |
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 | 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 | private Plausible() |
131 | { | |
132 | 0 | this.resetLists(); |
133 | } | |
134 | ||
135 | 0 | public static void setEnabled(boolean b) |
136 | { | |
137 | 0 | ENABLED = b; |
138 | } | |
139 | ||
140 | 0 | 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 | 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 | 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 | 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 | private void addJsonValues(String key, List<Object> values) |
307 | { | |
308 | 0 | jsonObject.add(objectEntry(key, values)); |
309 | } | |
310 | ||
311 | 0 | private void addJsonValue(String key, String value) |
312 | { | |
313 | 0 | jsonObject.add(objectEntry(key, value)); |
314 | } | |
315 | ||
316 | 0 | private void addJsonValue(String key, int value) |
317 | { | |
318 | 0 | jsonObject.add(objectEntry(key, Integer.valueOf(value))); |
319 | } | |
320 | ||
321 | 0 | private void addJsonValue(String key, boolean value) |
322 | { | |
323 | 0 | jsonObject.add(objectEntry(key, Boolean.valueOf(value))); |
324 | } | |
325 | ||
326 | 0 | private void addQueryStringValue(String key, String value) |
327 | { | |
328 | 0 | queryStringValues.add(stringEntry(key, value)); |
329 | } | |
330 | ||
331 | 0 | private void addCookieValue(String key, String value) |
332 | { | |
333 | 0 | cookieValues.add(stringEntry(key, value)); |
334 | } | |
335 | ||
336 | 0 | private void resetLists() |
337 | { | |
338 | 0 | jsonObject = new ArrayList<>(); |
339 | 0 | queryStringValues = new ArrayList<>(); |
340 | 0 | cookieValues = new ArrayList<>(); |
341 | } | |
342 | ||
343 | 0 | 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 | public static void reset() |
353 | { | |
354 | 0 | getInstance().resetLists(); |
355 | } | |
356 | ||
357 | 0 | 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 | private void buildCookieHeaders() |
386 | { | |
387 | // TODO not needed yet | |
388 | } | |
389 | ||
390 | 0 | 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 | 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 | 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 | 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 | 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 | private static void indent(StringBuilder sb, int indent) |
527 | { | |
528 | 0 | prettyWhitespace(sb, " ", indent); |
529 | } | |
530 | ||
531 | 0 | private static void newline(StringBuilder sb) |
532 | { | |
533 | 0 | prettyWhitespace(sb, "\n", -1); |
534 | } | |
535 | ||
536 | 0 | private static void space(StringBuilder sb) |
537 | { | |
538 | 0 | prettyWhitespace(sb, " ", -1); |
539 | } | |
540 | ||
541 | 0 | 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 | 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 | 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 | } |