Merge branch 'improvement/JAL-4212_remove_macos_test_java_icons' into develop improvement/JAL-4212_remove_macos_test_java_icons
authorJames Procter <j.procter@dundee.ac.uk>
Mon, 10 Jul 2023 09:37:59 +0000 (10:37 +0100)
committerJames Procter <j.procter@dundee.ac.uk>
Mon, 10 Jul 2023 09:37:59 +0000 (10:37 +0100)
29 files changed:
help/help/html/privacy.html
help/markdown/releases/release-2_11_2_7.md [new file with mode: 0644]
help/markdown/releases/release-2_11_3_0.md
help/markdown/whatsnew/whatsnew-2_11_2_7.md [new file with mode: 0644]
help/markdown/whatsnew/whatsnew-2_11_3_0.md
j11lib/JGoogleAnalytics_0.3.jar [deleted file]
j8lib/JGoogleAnalytics_0.3.jar [deleted file]
resources/lang/Messages.properties
resources/lang/Messages_es.properties
src/jalview/analysis/AlignSeq.java
src/jalview/analysis/AlignmentUtils.java
src/jalview/analytics/Plausible.java [moved from src/jalview/analytics/GoogleAnalytics4.java with 55% similarity]
src/jalview/api/RotatableCanvasI.java
src/jalview/bin/Cache.java
src/jalview/bin/Commands.java
src/jalview/bin/Jalview.java
src/jalview/datamodel/DBRefSource.java
src/jalview/datamodel/Sequence.java
src/jalview/ext/jmol/JmolParser.java
src/jalview/gui/AppJmol.java
src/jalview/gui/Desktop.java
src/jalview/gui/ImageExporter.java
src/jalview/gui/PopupMenu.java
src/jalview/util/HttpUtils.java
src/jalview/ws/sifts/SiftsClient.java
test/jalview/analysis/TestAlignSeq.java
test/jalview/bin/CommandsTest.java
test/jalview/datamodel/SequenceTest.java
test/jalview/ws/sifts/SiftsClientTest.java

index 9ad61b5..8704ce8 100644 (file)
   <p>Usage data is collected from the logs of various web services
     that the Jalview Desktop contacts through its normal operation.
     These are described below:</p>
-  <ul>
-    <li><em>HTTP logs on the Jalview website</em><br> We
-      record IP addresses of machines which access the web site, either
-      via the browser when downloading the application, or when the
-      Jalview Desktop user interface is launched.<br> <br>
-      <ul>
-        <li><i>The Jalview Getdown Launcher</i> (Since 2.11.0) examines release
-          channels every time Jalview launches to determine if a new
-          release is available.</li>
-        <li><i>The questionnaire web service at
-            www.jalview.org/cgi-bin/questionnaire.pl is checked and a
-            unique cookie for the current questionnaire is stored in the
-            Jalview properties file.</i></li>
-        <li><i>The Jalview web services stack is contacted to
-            retrieve the currently available web services. All
-            interactions with the public Jalview web services are
-            logged, but we delete all job data (input data and results)
-            after about two weeks.</i></li>
-      </ul> <br></li>
-    <li><em>Google Analytics</em><br> Since Jalview 2.4.0b2,
-      the Jalview Desktop records usage data with Google Analytics via
-      the <a href="http://code.google.com/p/jgoogleanalytics/">JGoogleAnalytics</a>
-      class.<br> The Google Analytics logs for Jalview version 2.4
-      only record the fact that the application was started, but in the
-      future, we will use this mechanism to improve the Desktop user
-      interface, by tracking which parts of the user interface are being
-      used most often.</li>
-  </ul>
-  </p>
+       <ul>
+               <li><em>HTTP logs on the Jalview website</em><br> We record
+                       IP addresses of machines which access the web site, either via the
+                       browser when downloading the application, or when the Jalview Desktop
+                       user interface is launched.<br> <br>
+                       <ul>
+                               <li><i>The Jalview Getdown Launcher</i> (Since 2.11.0) examines
+                                       release channels every time Jalview launches to determine if a new
+                                       release is available.</li>
+                               <li><i>The questionnaire web service at
+                                               www.jalview.org/cgi-bin/questionnaire.pl is checked and a unique
+                                               cookie for the current questionnaire is stored in the Jalview
+                                               properties file.</i></li>
+                               <li><i>The Jalview web services stack is contacted to
+                                               retrieve the currently available web services. All interactions
+                                               with the public Jalview web services are logged, but we delete all
+                                               job data (input data and results) after about two weeks.</i></li>
+                       </ul> <br></li>
+               <li><em>Usage Analytics</em><br> Since Jalview 2.11.2.7, the
+                       Jalview Desktop records usage data with a self-hosted instance of the
+                       analytics stack <a href="https://plausible.io">Plausible.io</a> via a
+                       custom GPLv3 client developed by Ben Soares. Prior to this, Jalview
+                       versions as far back as 2.4 recorded application launches via <a
+                       href="http://code.google.com/p/jgoogleanalytics/">JGoogleAnalytics</a>
+                       .<br> Usage logs for Jalview record the fact that the
+                       application was started, and details about the OS, installed Jalview
+                       launcher (if any) and java version used. In the future, we will use
+                       this mechanism to improve the Desktop user interface, by tracking
+                       which parts of the user interface are being used most often.</li>
+       </ul>
   <p>
     <strong>Stopping Jalview from calling home</strong><br> If you
     run Jalview in 'headless mode' via the command line, then the
diff --git a/help/markdown/releases/release-2_11_2_7.md b/help/markdown/releases/release-2_11_2_7.md
new file mode 100644 (file)
index 0000000..361d39a
--- /dev/null
@@ -0,0 +1,12 @@
+---
+version: 2.11.2.7
+date: 2023-06-30
+channel: "release"
+---
+
+## New Features
+- <!-- JAL-4001 --> Jalview now reports usage statistics via Plausible.io
+
+## Issues Resolved
+- <!-- JAL-4116 --> PDB structures slow to view when Jalview Java console is open
+- <!-- JAL-4216 --> chains in PDB or mmCIF files with negative RESNUMs not correctly parsed
index 1309483..fbd7b9f 100644 (file)
@@ -1,6 +1,6 @@
 ---
 version: 2.11.3.0
-date: 2023-06-07
+date: 2023-07-19
 channel: "release"
 ---
 
@@ -20,6 +20,9 @@ channel: "release"
 - <!-- JAL-4089 --> Use selected columns for superposition
 - <!-- JAL-4086 --> Highlight aligned positions on all associated structures when mousing over a column
 
+- <!-- JAL-4221 --> sequence descriptions are updated from database reference sources if not already defined
+
+
 ### Improved support for working with computationally determined models
 
 - <!-- JAL-3895 --> Alphafold red/orange/yellow/green colourscheme for structures
@@ -35,6 +38,8 @@ channel: "release"
 - <!-- JAL-3858 --> Import and display alphafold alignment uncertainty matrices from JSON
 - <!-- JAL-4134,JAL-4158 --> Column-wise alignment groups and selections and interactive tree viewer for PAE matrices
 - <!-- JAL-4124 --> Store/Restore PAE data and visualisation settings from Jalview Project
+- <!-- JAL-4083 --> Multiple residue sidechain highlighting in structure viewers from PAE mouseovers
+
 
 ### Jalview on the command line
 
@@ -44,6 +49,21 @@ channel: "release"
 ### Other improvements
 
 - <!-- JAL-3119 --> Name of alignment and view included in overview window's title
+- <!-- JAL-4213 --> "add reference annotation" add all positions in reference annotation tracks, not just positions in the currently highlighted columns/selection range
+- <!-- JAL-4119 --> EMBL-EBI SIFTS file downloads now use split directories
+
+- <!-- JAL-4195,JAL-4194,JAL-4193 --> sensible responses from the CLI when things go wrong during image export
+Add a command line option to set Jalview properties for this session only
+Add a command line option to suppress opening the startup file for this session
+
+
+JAL-4187        Powershell launcher script fails when given no arguments with the old ArgsParser
+
+known issue ? <!-- JAL-4127    --> 'Reload' for a jalview project results in all windows being duplicated
+
+
+- <!-- JAL-3830 --> Command-line wrapper script for macOS bundle, linux and Windows installations (bash, powershell and .bat wrappers)
+- <!-- JAL-3820 --> In Linux desktops' task-managers, the grouped Jalview windows get a generic name
 
 ## Still in progress (delete on release)
 
diff --git a/help/markdown/whatsnew/whatsnew-2_11_2_7.md b/help/markdown/whatsnew/whatsnew-2_11_2_7.md
new file mode 100644 (file)
index 0000000..f884134
--- /dev/null
@@ -0,0 +1,5 @@
+Jalview 2.11.2.7 is a minor patch release - it includes patches affecting efficiency when importing structures and a small revision to the import processing of structures with negative residue numbering. 
+
+With this release, Jalview usage statistics are now collected by a jalview.org hosted instance of the open source privacy-preserving analytics stack, Plausible.io. 
+
+
index 59495d5..ec82f83 100644 (file)
@@ -1,5 +1,7 @@
 The 2.11.3 series includes support for in-depth exploration of predicted alignment error matrices from AlphaFold in the context of multiple alignments, along with support for standard colourschemes for shading models according to their pLDDT.
 
+We're launching this release at ISMB 2023 - come find us !
+
 It also introduces new support for native ARM-based OSX architectures, and a few other goodies!
 
 
diff --git a/j11lib/JGoogleAnalytics_0.3.jar b/j11lib/JGoogleAnalytics_0.3.jar
deleted file mode 100644 (file)
index 0dbc98c..0000000
Binary files a/j11lib/JGoogleAnalytics_0.3.jar and /dev/null differ
diff --git a/j8lib/JGoogleAnalytics_0.3.jar b/j8lib/JGoogleAnalytics_0.3.jar
deleted file mode 100644 (file)
index 0dbc98c..0000000
Binary files a/j8lib/JGoogleAnalytics_0.3.jar and /dev/null differ
index 7f5746e..924b9cb 100644 (file)
@@ -1452,8 +1452,8 @@ label.choose_tempfac_type = Choose Temperature Factor type
 label.interpret_tempfac_as = Interpret Temperature Factor as
 label.add_pae_matrix_file = Add PAE matrix file
 label.nothing_selected = Nothing selected
-prompt.google_analytics_title = Jalview Usage Statistics
-prompt.google_analytics = Do you want to help make Jalview better by enabling the collection of usage statistics with Google Analytics?\nYou can enable or disable usage tracking in the preferences.
+prompt.analytics_title = Jalview Usage Statistics
+prompt.analytics = Do you want to help make Jalview better by enabling the collection of usage statistics with Plausible analytics?\nYou can enable or disable usage tracking in the preferences.
 label.working_ellipsis = Working ... 
 action.show_groups_on_matrix = Show groups on matrix
 action.show_groups_on_matrix_tooltip = When enabled, clusters defined on the matrix's associated tree or below the assigned threshold are shown as different colours on the matrix annotation row
index a4594dc..150a407 100644 (file)
@@ -1434,5 +1434,5 @@ label.tftype_default = Default
 label.tftype_plddt = pLDDT
 label.add_pae_matrix_file = Añadir un fichero de matriz PAE
 label.nothing_selected = Nada seleccionado
-prompt.google_analytics_title = Jalview Estadísticas de Uso
-prompt.google_analytics = Â¿Quiere ayudar a mejorar Jalview habilitando la recopilación de estadísticas de uso con Google Analytics?\nPuede habilitar o deshabilitar el seguimiento de uso en las preferencias.
+prompt.analytics_title = Jalview Estadísticas de Uso
+prompt.analytics = Â¿Quiere ayudar a mejorar Jalview habilitando la recopilación de estadísticas de uso con análisis Plausible?\nPuede habilitar o deshabilitar el seguimiento de uso en las preferencias.
index 65fd110..02b3f41 100755 (executable)
@@ -890,7 +890,8 @@ public class AlignSeq
         pdbpos++;
       }
 
-      if (allowmismatch || c1 == c2)
+      // ignore case differences
+      if (allowmismatch || (c1 == c2) || (Math.abs(c2-c1)==('a'-'A')))
       {
         // extend mapping interval
         if (lp1 + 1 != alignpos || lp2 + 1 != pdbpos)
index 1158c53..6ab49b2 100644 (file)
@@ -1512,7 +1512,7 @@ public class AlignmentUtils
    * @param alignment
    *          the alignment to add them to
    * @param selectionGroup
-   *          current selection group (or null if none)
+   *          current selection group - may be null, if provided then any added annotation will be trimmed to just those columns in the selection group
    */
   public static void addReferenceAnnotations(
           Map<SequenceI, List<AlignmentAnnotation>> annotations,
@@ -1536,7 +1536,7 @@ public class AlignmentUtils
    * @param seq
    * @param ann
    * @param selectionGroup
-   *          - may be null
+   *          current selection group - may be null, if provided then any added annotation will be trimmed to just those columns in the selection group
    * @return annotation added to {@code seq and {@code alignment}
    */
   public static AlignmentAnnotation addReferenceAnnotationTo(
similarity index 55%
rename from src/jalview/analytics/GoogleAnalytics4.java
rename to src/jalview/analytics/Plausible.java
index ba8a920..ab2de77 100644 (file)
@@ -1,8 +1,31 @@
+/*
+ * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
+ * Copyright (C) $$Year-Rel$$ The Jalview Authors
+ * 
+ * This file is part of Jalview.
+ * 
+ * Jalview is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License 
+ * as published by the Free Software Foundation, either version 3
+ * of the License, or (at your option) any later version.
+ *  
+ * Jalview is distributed in the hope that it will be useful, but 
+ * WITHOUT ANY WARRANTY; without even the implied warranty 
+ * of MERCHANTABILITY or FITNESS FOR A PARTICULAR 
+ * PURPOSE.  See the GNU General Public License for more details.
+ * 
+ * You should have received a copy of the GNU General Public License
+ * along with Jalview.  If not, see <http://www.gnu.org/licenses/>.
+ * The Jalview Authors are detailed in the 'AUTHORS' file.
+ */
 package jalview.analytics;
 
+import java.io.BufferedReader;
 import java.io.IOException;
+import java.io.InputStreamReader;
 import java.io.OutputStream;
 import java.io.UnsupportedEncodingException;
+import java.lang.invoke.MethodHandles;
 import java.net.HttpURLConnection;
 import java.net.MalformedURLException;
 import java.net.URL;
@@ -17,75 +40,94 @@ import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
 import java.util.Random;
-import java.util.UUID;
 
 import jalview.bin.Cache;
 import jalview.bin.Console;
 import jalview.util.ChannelProperties;
+import jalview.util.HttpUtils;
 
-public class GoogleAnalytics4
+public class Plausible
 {
+  private static final String USER_AGENT;
+
   private static final String JALVIEW_ID = "Jalview Desktop";
 
-  private static final String SESSION_ID = new Random().toString();
+  private static final String DOMAIN = "jalview.org";
 
-  private static final String MEASUREMENT_ID = "G-6TMPHMXEQ0";
+  private static final String CONFIG_API_BASE_URL = "https://www.jalview.org/services/config/analytics/url";
 
-  private static final String API_SECRET = "Qb9NSbqkRDqizG6j2BBJ2g";
+  private static final String DEFAULT_API_BASE_URL = "https://analytics.jalview.org/api/event";
 
-  // 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 API_BASE_URL;
 
-  private static final String BASE_URL = "https://www.google-analytics.com/mp/collect";
+  private static final String clientId;
 
-  private static final String DESKTOP_EVENT = "desktop_event";
+  public static final String APPLICATION_BASE_URL = "desktop://localhost";
 
   private List<Map.Entry<String, String>> queryStringValues;
 
   private List<Map.Entry<String, Object>> jsonObject;
 
-  private List<Event> events;
-
   private List<Map.Entry<String, String>> cookieValues;
 
   private static boolean ENABLED = false;
 
-  private static GoogleAnalytics4 instance = null;
+  private static boolean DEBUG = true;
 
-  private static final Map<String, String> defaultParams;
+  private static Plausible instance = null;
+
+  private static final Map<String, String> defaultProps;
 
   static
   {
-    defaultParams = new HashMap<>();
-    defaultParams.put("app_name",
+    defaultProps = new HashMap<>();
+    defaultProps.put("app_name",
             ChannelProperties.getProperty("app_name") + " Desktop");
-    defaultParams.put("version", Cache.getProperty("VERSION"));
-    defaultParams.put("build_date",
+    defaultProps.put("version", Cache.getProperty("VERSION"));
+    defaultProps.put("build_date",
             Cache.getDefault("BUILD_DATE", "unknown"));
-    defaultParams.put("java_version", System.getProperty("java.version"));
+    defaultProps.put("java_version", System.getProperty("java.version"));
     String val = System.getProperty("sys.install4jVersion");
     if (val != null)
     {
-      defaultParams.put("install4j_version", val);
+      defaultProps.put("install4j_version", val);
     }
     val = System.getProperty("installer_template_version");
     if (val != null)
     {
-      defaultParams.put("install4j_template_version", val);
+      defaultProps.put("install4j_template_version", val);
     }
     val = System.getProperty("launcher_version");
     if (val != null)
     {
-      defaultParams.put("launcher_version", val);
+      defaultProps.put("launcher_version", val);
     }
-    defaultParams.put("java_arch",
+    defaultProps.put("java_arch",
             System.getProperty("os.arch") + " "
                     + System.getProperty("os.name") + " "
                     + System.getProperty("os.version"));
+    defaultProps.put("os", System.getProperty("os.name"));
+    defaultProps.put("os_version", System.getProperty("os.version"));
+    defaultProps.put("os_arch", System.getProperty("os.arch"));
+    String installation = Cache.applicationProperties
+            .getProperty("INSTALLATION");
+    if (installation != null)
+    {
+      defaultProps.put("installation", installation);
+    }
+
+    // ascertain the API_BASE_URL
+    API_BASE_URL = getAPIBaseURL();
+
+    // random clientId to make User-Agent unique (to register analytic)
+    clientId = String.format("%08x", new Random().nextInt());
+
+    USER_AGENT = HttpUtils.getUserAgent(
+            MethodHandles.lookup().lookupClass().getCanonicalName() + " "
+                    + clientId);
   }
 
-  private GoogleAnalytics4()
+  private Plausible()
   {
     this.resetLists();
   }
@@ -95,72 +137,79 @@ public class GoogleAnalytics4
     ENABLED = b;
   }
 
-  public void sendAnalytics(String eventName, String... paramsStrings)
+  public void sendEvent(String eventName, String urlString,
+          String... propsStrings)
   {
-    sendAnalytics(eventName, false, paramsStrings);
+    sendEvent(eventName, urlString, false, propsStrings);
   }
 
   /**
    * The simplest way to send an analytic event.
    * 
    * @param eventName
-   *          The event name. To emulate a webpage view use "page_view" and set
-   *          a "page_location" parameter. See
-   *          https://developers.google.com/analytics/devguides/collection/ga4/events?client_type=gtag
-   * @param sendDefaultParams
-   *          Flag whether to add the default params about the application.
-   * @param paramsStrings
+   *          The event name. To emulate a webpage view use "pageview" and set a
+   *          "url" key/value. See https://plausible.io/docs/events-api
+   * @param sendDefaultProps
+   *          Flag whether to add the default props about the application.
+   * @param propsStrings
    *          Optional multiple Strings in key, value pairs (there should be an
-   *          even number of paramsStrings) to be set as parameters of the
-   *          event. To emulate a webpage view use "page_location" as the URL in
-   *          a "page_view" event.
+   *          even number of propsStrings) to be set as property of the event.
+   *          To emulate a webpage view set "url" as the URL in a "pageview"
+   *          event.
    */
-  public void sendAnalytics(String eventName, boolean sendDefaultParams,
-          String... paramsStrings)
+  public void sendEvent(String eventName, String urlString,
+          boolean sendDefaultProps, String... propsStrings)
   {
     // clear out old lists
     this.resetLists();
 
     if (!ENABLED)
     {
-      Console.debug("GoogleAnalytics4 not enabled.");
+      Console.debug("Plausible not enabled.");
       return;
     }
-    Map<String, String> params = new HashMap<>();
-    params.put("event_category", DESKTOP_EVENT);
-    params.put("event_label", eventName);
+    Map<String, String> props = new HashMap<>();
 
     // add these to all events from this application instance
-    if (sendDefaultParams)
+    if (sendDefaultProps)
     {
-      params.putAll(defaultParams);
+      props.putAll(defaultProps);
     }
 
-    // add (and overwrite with) the passed in params
-    if (paramsStrings != null && paramsStrings.length > 0)
+    // add (and overwrite with) the passed in props
+    if (propsStrings != null && propsStrings.length > 0)
     {
-      if (paramsStrings.length % 2 != 0)
+      if (propsStrings.length % 2 != 0)
       {
         Console.warn(
-                "Cannot addEvent with odd number of paramsStrings.  Ignoring the last one.");
+                "Cannot addEvent with odd number of propsStrings.  Ignoring the last one.");
       }
-      for (int i = 0; i < paramsStrings.length - 1; i += 2)
+      for (int i = 0; i < propsStrings.length - 1; i += 2)
       {
-        String key = paramsStrings[i];
-        String value = paramsStrings[i + 1];
-        params.put(key, value);
+        String key = propsStrings[i];
+        String value = propsStrings[i + 1];
+        props.put(key, value);
       }
     }
 
-    addEvent(eventName, params);
-    addQueryStringValue("measurement_id", MEASUREMENT_ID);
-    addQueryStringValue("api_secret", API_SECRET);
-    addJsonValue("client_id", CLIENT_ID);
-    addJsonValues("events", Event.toObjectList(events));
+    addJsonValue("domain", DOMAIN);
+    addJsonValue("name", eventName);
+    StringBuilder eventUrlSb = new StringBuilder(APPLICATION_BASE_URL);
+    if (!APPLICATION_BASE_URL.endsWith("/") && !urlString.startsWith("/"))
+    {
+      eventUrlSb.append("/");
+    }
+    eventUrlSb.append(urlString);
+    addJsonValue("url", eventUrlSb.toString());
+    addJsonObject("props", props);
     StringBuilder urlSb = new StringBuilder();
-    urlSb.append(BASE_URL);
-    urlSb.append('?');
-    urlSb.append(buildQueryString());
+    urlSb.append(API_BASE_URL);
+    String qs = buildQueryString();
+    if (qs != null && qs.length() > 0)
+    {
+      urlSb.append('?');
+      urlSb.append(qs);
+    }
     try
     {
       URL url = new URL(urlSb.toString());
@@ -171,15 +220,21 @@ public class GoogleAnalytics4
 
       String jsonString = buildJson();
 
-      Console.debug("GA4: HTTP Request is: '" + urlSb.toString() + "'");
-      Console.debug("GA4: POSTed JSON is:\n" + jsonString);
+      Console.debug(
+              "Plausible: HTTP Request is: '" + urlSb.toString() + "'");
+      if (DEBUG)
+      {
+        Console.debug("Plausible: User-Agent is: '" + USER_AGENT + "'");
+      }
+      Console.debug("Plausible: POSTed JSON is:\n" + jsonString);
 
       byte[] jsonBytes = jsonString.getBytes(StandardCharsets.UTF_8);
       int jsonLength = jsonBytes.length;
 
       httpURLConnection.setFixedLengthStreamingMode(jsonLength);
       httpURLConnection.setRequestProperty("Content-Type",
-              "application/json; charset=UTF-8");
+              "application/json");
+      httpURLConnection.setRequestProperty("User-Agent", USER_AGENT);
       httpURLConnection.connect();
       try (OutputStream os = httpURLConnection.getOutputStream())
       {
@@ -187,50 +242,63 @@ public class GoogleAnalytics4
       }
       int responseCode = httpURLConnection.getResponseCode();
       String responseMessage = httpURLConnection.getResponseMessage();
+
       if (responseCode < 200 || responseCode > 299)
       {
-        Console.warn("GoogleAnalytics4 connection failed: '" + responseCode
-                + " " + responseMessage + "'");
+        Console.warn("Plausible connection failed: '" + responseCode + " "
+                + responseMessage + "'");
       }
       else
       {
-        Console.debug("GoogleAnalytics4 connection succeeded: '"
-                + responseCode + " " + responseMessage + "'");
+        Console.debug("Plausible connection succeeded: '" + responseCode
+                + " " + responseMessage + "'");
+      }
+
+      if (DEBUG)
+      {
+        BufferedReader br = new BufferedReader(new InputStreamReader(
+                (httpURLConnection.getInputStream())));
+        StringBuilder sb = new StringBuilder();
+        String response;
+        while ((response = br.readLine()) != null)
+        {
+          sb.append(response);
+        }
+        String body = sb.toString();
+        Console.debug("Plausible response content:\n" + body);
       }
     } catch (MalformedURLException e)
     {
       Console.debug(
-              "Somehow the GoogleAnalytics4 BASE_URL and queryString is malformed.",
+              "Somehow the Plausible BASE_URL and queryString is malformed: '"
+                      + urlSb.toString() + "'",
               e);
       return;
     } catch (IOException e)
     {
-      Console.debug("Connection to GoogleAnalytics4 BASE_URL '" + BASE_URL
+      Console.debug("Connection to Plausible BASE_URL '" + API_BASE_URL
               + "' failed.", e);
     } catch (ClassCastException e)
     {
       Console.debug(
-              "Couldn't cast URLConnection to HttpURLConnection in GoogleAnalytics4.",
+              "Couldn't cast URLConnection to HttpURLConnection in Plausible.",
               e);
     }
   }
 
-  public void addEvent(String name, Map<String, String> params)
+  private void addJsonObject(String key, Map<String, String> map)
   {
-    Event event = new Event(name);
-    if (params != null && params.size() > 0)
+    List<Map.Entry<String, ? extends Object>> list = new ArrayList<>();
+    for (String k : map.keySet())
     {
-      for (String key : params.keySet())
-      {
-        String value = params.get(key);
-        event.addParam(key, value);
-      }
+      list.add(stringEntry(k, map.get(k)));
     }
-    events.add(event);
+    addJsonObject(key, list);
+
   }
 
   private void addJsonObject(String key,
-          List<Map.Entry<String, Object>> object)
+          List<Map.Entry<String, ? extends Object>> object)
   {
     jsonObject.add(objectEntry(key, object));
   }
@@ -268,16 +336,15 @@ public class GoogleAnalytics4
   private void resetLists()
   {
     jsonObject = new ArrayList<>();
-    events = new ArrayList<Event>();
     queryStringValues = new ArrayList<>();
     cookieValues = new ArrayList<>();
   }
 
-  public static GoogleAnalytics4 getInstance()
+  public static Plausible getInstance()
   {
     if (instance == null)
     {
-      instance = new GoogleAnalytics4();
+      instance = new Plausible();
     }
     return instance;
   }
@@ -480,52 +547,56 @@ public class GoogleAnalytics4
   {
     return new AbstractMap.SimpleEntry<String, String>(s, v);
   }
-}
-
-class Event
-{
-  private String name;
 
-  private List<Map.Entry<String, String>> params;
-
-  @SafeVarargs
-  public Event(String name, Map.Entry<String, String>... paramEntries)
+  private static String getAPIBaseURL()
   {
-    this.name = name;
-    this.params = new ArrayList<Map.Entry<String, String>>();
-    for (Map.Entry<String, String> paramEntry : paramEntries)
+    try
     {
-      if (paramEntry == null)
+      URL url = new URL(CONFIG_API_BASE_URL);
+      URLConnection urlConnection = url.openConnection();
+      HttpURLConnection httpURLConnection = (HttpURLConnection) urlConnection;
+      httpURLConnection.setRequestMethod("GET");
+      httpURLConnection.setRequestProperty("User-Agent", USER_AGENT);
+      httpURLConnection.setConnectTimeout(5000);
+      httpURLConnection.setReadTimeout(3000);
+      httpURLConnection.connect();
+      int responseCode = httpURLConnection.getResponseCode();
+      String responseMessage = httpURLConnection.getResponseMessage();
+
+      if (responseCode < 200 || responseCode > 299)
       {
-        continue;
+        Console.warn("Config URL connection to '" + CONFIG_API_BASE_URL
+                + "' failed: '" + responseCode + " " + responseMessage
+                + "'");
       }
-      params.add(paramEntry);
-    }
-  }
 
-  public void addParam(String param, String value)
-  {
-    params.add(GoogleAnalytics4.stringEntry(param, value));
-  }
+      BufferedReader br = new BufferedReader(
+              new InputStreamReader((httpURLConnection.getInputStream())));
+      StringBuilder sb = new StringBuilder();
+      String response;
+      while ((response = br.readLine()) != null)
+      {
+        sb.append(response);
+      }
+      if (sb.length() > 7 && sb.substring(0, 5).equals("https"))
+      {
+        return sb.toString();
+      }
 
-  protected List<Map.Entry<String, Object>> toObject()
-  {
-    List<Map.Entry<String, Object>> object = new ArrayList<>();
-    object.add(GoogleAnalytics4.objectEntry("name", (Object) name));
-    if (params.size() > 0)
+    } catch (MalformedURLException e)
     {
-      object.add(GoogleAnalytics4.objectEntry("params", (Object) params));
-    }
-    return object;
-  }
-
-  protected static List<Object> toObjectList(List<Event> events)
-  {
-    List<Object> eventObjectList = new ArrayList<>();
-    for (Event event : events)
+      Console.debug("Somehow the config URL is malformed: '"
+              + CONFIG_API_BASE_URL + "'", e);
+    } catch (IOException e)
     {
-      eventObjectList.add((Object) event.toObject());
+      Console.debug("Connection to Plausible BASE_URL '" + API_BASE_URL
+              + "' failed.", e);
+    } catch (ClassCastException e)
+    {
+      Console.debug(
+              "Couldn't cast URLConnection to HttpURLConnection in Plausible.",
+              e);
     }
-    return eventObjectList;
+    return DEFAULT_API_BASE_URL;
   }
 }
index c6eb6de..1646d89 100644 (file)
@@ -1,6 +1,6 @@
 /*
- * Jalview - A Sequence Alignment Editor and Viewer (Version 2.8.2b1)
- * Copyright (C) 2014 The Jalview Authors
+ * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
+ * Copyright (C) $$Year-Rel$$ The Jalview Authors
  * 
  * This file is part of Jalview.
  * 
index fc9ddda..5741908 100755 (executable)
@@ -43,6 +43,7 @@ import java.util.Collections;
 import java.util.Date;
 import java.util.Enumeration;
 import java.util.HashMap;
+import java.util.List;
 import java.util.Locale;
 import java.util.Map;
 import java.util.Properties;
@@ -52,7 +53,7 @@ import java.util.TreeSet;
 import javax.swing.LookAndFeel;
 import javax.swing.UIManager;
 
-import jalview.analytics.GoogleAnalytics4;
+import jalview.analytics.Plausible;
 import jalview.datamodel.PDBEntry;
 import jalview.gui.Preferences;
 import jalview.gui.UserDefinedColours;
@@ -128,7 +129,7 @@ import jalview.ws.sifts.SiftsSettings;
  * service</li>
  * <li>QUESTIONNAIRE last questionnaire:responder id string from questionnaire
  * service</li>
- * <li>USAGESTATS (false - user prompted) Enable google analytics tracker for
+ * <li>USAGESTATS (false - user prompted) Enable analytics tracker for
  * collecting usage statistics</li>
  * <li>SHOW_OVERVIEW boolean for overview window display</li>
  * <li>ANTI_ALIAS boolean for smooth fonts</li>
@@ -954,146 +955,41 @@ public class Cache
   }
 
   /**
-   * GA tracker object - actually JGoogleAnalyticsTracker null if tracking not
-   * enabled.
+   * Initialise the tracker if it is not done already.
    */
-  protected static Object tracker = null;
-
-  protected static Class trackerfocus = null;
-
-  protected static Class jgoogleanalyticstracker = null;
-
-  private static boolean useGA4 = true;
-
-  /**
-   * Initialise the google tracker if it is not done already.
-   */
-  public static void initGoogleTracker()
+  public static void initAnalytics()
   {
-    if (useGA4)
-    {
-      GoogleAnalytics4.setEnabled(true);
+    Plausible.setEnabled(true);
 
-      String appName = ChannelProperties.getProperty("app_name")
-              + " Desktop";
-      String version = Cache.getProperty("VERSION") + "_"
-              + Cache.getDefault("BUILD_DATE", "unknown");
-      String path;
-      /* we don't want to encode ':' as "%3A" for backward compatibility with the UA setup
-      try
-      {
-        path = "/" + String.join("/", URLEncoder.encode(appName, "UTF-8"),
-                URLEncoder.encode(version, "UTF-8"),
-                URLEncoder.encode(APPLICATION_STARTED, "UTF-8"));
-      } catch (UnsupportedEncodingException e)
-      {
-      */
-      path = ("/" + String.join("/", appName, version, APPLICATION_STARTED))
-              .replace(' ', '+');
-      /*
-      }
-      */
-      GoogleAnalytics4 ga4 = GoogleAnalytics4.getInstance();
-
-      // This will add a page_view similar to the old UA analytics.
-      // We probably want to get rid of this once the application_launch event
-      // is being processed properly.
-      ga4.sendAnalytics("page_view", "page_location", path, "page_title",
-              APPLICATION_STARTED);
-
-      // This will send a new "application_launch" event with parameters
-      // including the old-style "path", the channel name and version
-      ga4.sendAnalytics("application_launch", true, "page_location", path);
-    }
-    else
+    String appName = ChannelProperties.getProperty("app_name") + " Desktop";
+    String version = Cache.getProperty("VERSION") + "_"
+            + Cache.getDefault("BUILD_DATE", "unknown");
+    String path;
+    /* we don't want to encode ':' as "%3A" for backward compatibility with the UA setup
+    try
     {
-      if (tracker == null)
-      {
-        if (jgoogleanalyticstracker == null)
-        {
-          // try to get the tracker class
-          try
-          {
-            jgoogleanalyticstracker = Cache.class.getClassLoader()
-                    .loadClass(
-                            "com.boxysystems.jgoogleanalytics.JGoogleAnalyticsTracker");
-            trackerfocus = Cache.class.getClassLoader().loadClass(
-                    "com.boxysystems.jgoogleanalytics.FocusPoint");
-          } catch (Exception e)
-          {
-            Console.debug(
-                    "com.boxysystems.jgoogleanalytics package is not present - tracking not enabled.");
-            tracker = null;
-            jgoogleanalyticstracker = null;
-            trackerfocus = null;
-            return;
-          }
-        }
-        // now initialise tracker
-        Exception re = null, ex = null;
-        Error err = null;
-        String vrs = "No Version Accessible";
-        try
-        {
-          // Google analytics tracking code for Library Finder
-          tracker = jgoogleanalyticstracker
-                  .getConstructor(new Class[]
-                  { String.class, String.class, String.class })
-                  .newInstance(new Object[]
-                  { ChannelProperties.getProperty("app_name") + " Desktop",
-                      (vrs = Cache.getProperty("VERSION") + "_"
-                              + Cache.getDefault("BUILD_DATE", "unknown")),
-                      "UA-9060947-1" });
-          jgoogleanalyticstracker
-                  .getMethod("trackAsynchronously", new Class[]
-                  { trackerfocus })
-                  .invoke(tracker, new Object[]
-                  { trackerfocus
-                          .getConstructor(new Class[]
-                          { String.class })
-                          .newInstance(new Object[]
-                          { APPLICATION_STARTED }) });
-        } catch (RuntimeException e)
-        {
-          re = e;
-        } catch (Exception e)
-        {
-          ex = e;
-        } catch (Error e)
-        {
-          err = e;
-        }
-        if (re != null || ex != null || err != null)
-        {
-          if (re != null)
-          {
-            Console.debug("Caught runtime exception in googletracker init:",
-                    re);
-          }
-          if (ex != null)
-          {
-            Console.warn(
-                    "Failed to initialise GoogleTracker for Jalview Desktop with version "
-                            + vrs,
-                    ex);
-          }
-          if (err != null)
-          {
-            Console.error(
-                    "Whilst initing GoogleTracker for Jalview Desktop version "
-                            + vrs,
-                    err);
-          }
-        }
-        else
-        {
-          Console.debug("Successfully initialised tracker.");
-        }
-      }
+      path = "/" + String.join("/", URLEncoder.encode(appName, "UTF-8"),
+              URLEncoder.encode(version, "UTF-8"),
+              URLEncoder.encode(APPLICATION_STARTED, "UTF-8"));
+    } catch (UnsupportedEncodingException e)
+    {
+    */
+    List<String> pathParts = new ArrayList<>();
+    pathParts.add(appName);
+    pathParts.add(version);
+    pathParts.add(APPLICATION_STARTED);
+    path = ("/" + String.join("/", pathParts)).replace(' ', '+');
+    /*
     }
+    */
+    Plausible plausible = Plausible.getInstance();
+
+    // This will send a new "application_launch" event with parameters
+    // including the old-style "path", the channel name and version
+    plausible.sendEvent("application_launch", path, true);
   }
 
-  private static final String APPLICATION_STARTED = "Application Started.";
+  private static final String APPLICATION_STARTED = "Application Started";
 
   /**
    * get the user's default colour if available
index 7f493e0..d7d1ea3 100644 (file)
@@ -574,17 +574,28 @@ public class Commands
             Console.error("Failed to import and open structure view.");
             continue;
           }
-          while (sv.isBusy())
+          try
           {
-            try {
+            long tries=1000;
+            while (sv.isBusy() && tries>0)
+            {
               Thread.sleep(25);
+              if (sv.isBusy())
+              {
+                tries--;
+                Console.debug(
+                        "Waiting for viewer for " + structureFilepath);
+              }
             }
-            catch (Exception x)
+            if (tries==0 && sv.isBusy())
             {
-              
+              Console.warn("Gave up waiting for structure viewer to load. Something may have gone wrong.");
             }
+          } catch (Exception x)
+          {
+            Console.warn("Exception whilst waiting for structure viewer "+structureFilepath,x);
           }
-
+          Console.debug("Successfully opened viewer for "+structureFilepath);
           String structureImageFilename = ArgParser.getValueFromSubValOrArg(
                   avm, av, Arg.STRUCTUREIMAGE, subVals);
           if (sv != null && structureImageFilename != null)
@@ -624,12 +635,13 @@ public class Commands
             }
             BitmapImageSizing userBis = ImageMaker
                     .parseScaleWidthHeightStrings(scale, width, height);
+            // TODO MAKE THIS VIEWER INDEPENDENT!!
             switch (StructureViewer.getViewerType())
             {
             case JMOL:
               try
               {
-                Thread.sleep(1000);
+                Thread.sleep(1000); // WHY ???
               } catch (InterruptedException e)
               {
                 // TODO Auto-generated catch block
@@ -641,8 +653,11 @@ public class Commands
               {
                 AppJmol jmol = (AppJmol) sview;
                 try { 
+                  Console.debug("Rendering image to "+structureImageFile);
                   jmol.makePDBImage(structureImageFile, imageType, renderer,
                         userBis);
+                  Console.debug("Finished Rendering image to "+structureImageFile);
+
                 }
                 catch (ImageOutputException ioexc)
                 {
index 3a733f3..57f2575 100755 (executable)
@@ -633,8 +633,7 @@ public class Jalview
     {
       headless = true;
     }
-    System.setProperty("http.agent",
-            "Jalview Desktop/" + Cache.getDefault("VERSION", "Unknown"));
+    System.setProperty("http.agent", HttpUtils.getUserAgent());
 
     try
     {
@@ -810,6 +809,7 @@ public class Jalview
     // Run Commands from cli
     cmds = new Commands(argparser, headlessArg);
     boolean commandsSuccess = cmds.argsWereParsed();
+
     if (commandsSuccess)
     {
       if (headlessArg)
@@ -1573,7 +1573,7 @@ public class Jalview
                     + "-questionnaire URL\tQueries the given URL for information about any Jalview user questionnaires.\n"
                     + "-noquestionnaire\tTurn off questionnaire check.\n"
                     + "-nonews\tTurn off check for Jalview news.\n"
-                    + "-nousagestats\tTurn off google analytics tracking for this session.\n"
+                    + "-nousagestats\tTurn off analytics tracking for this session.\n"
                     + "-sortbytree OR -nosortbytree\tEnable or disable sorting of the given alignment by the given tree\n"
                     // +
                     // "-setprop PROPERTY=VALUE\tSet the given Jalview property,
@@ -1596,16 +1596,15 @@ public class Jalview
      */
     PromptUserConfig prompter = new PromptUserConfig(Desktop.desktop,
             "USAGESTATS",
-            MessageManager.getString("prompt.google_analytics_title"),
-            MessageManager.getString("prompt.google_analytics"),
+            MessageManager.getString("prompt.plausible_analytics_title"),
+            MessageManager.getString("prompt.plausible_analytics"),
             new Runnable()
             {
               @Override
               public void run()
               {
-                Console.debug(
-                        "Initialising googletracker for usage stats.");
-                Cache.initGoogleTracker();
+                Console.debug("Initialising analytics for usage stats.");
+                Cache.initAnalytics();
                 Console.debug("Tracking enabled.");
               }
             }, new Runnable()
@@ -1613,7 +1612,7 @@ public class Jalview
               @Override
               public void run()
               {
-                Console.debug("Not enabling Google Tracking.");
+                Console.debug("Not enabling analytics.");
               }
             }, null, true);
     desktop.addDialogThread(prompter);
index f384b1e..3b1757b 100755 (executable)
@@ -154,6 +154,11 @@ public class DBRefSource
 
   public static boolean isPrimaryCandidate(String ucversion)
   {
+    if (ucversion==null)
+    {
+      // Null/empty version is not a real reference ?
+      return false;
+    }
     // tricky - this test really needs to search the sequence's set of dbrefs to
     // see if there is a primary reference that derived this reference.
     for (int i = allSources.length; --i >= 0;)
index 32d295a..5bb55e5 100755 (executable)
@@ -1754,6 +1754,13 @@ public class Sequence extends ASequence implements SequenceI
       transferAnnotation(entry.getDatasetSequence(), mp);
       return;
     }
+    // transfer from entry to sequence
+    // if entry has a description and sequence doesn't, then transfer
+    if (entry.getDescription()!=null && (description==null || description.trim().length()==0))
+    {
+      description = entry.getDescription();
+    }
+    
     // transfer any new features from entry onto sequence
     if (entry.getSequenceFeatures() != null)
     {
index c64dac1..57b406e 100644 (file)
@@ -488,7 +488,7 @@ public class JmolParser extends StructureFile implements JmolStatusListener
   {
     int length = sq.getLength();
     boolean ssFound = false;
-    Annotation asecstr[] = new Annotation[length + firstResNum - 1];
+    Annotation asecstr[] = new Annotation[length + (firstResNum-sq.getStart())];
     for (int p = 0; p < length; p++)
     {
       if (secstr[p] >= 'A' && secstr[p] <= 'z')
index 1758b5b..49eae98 100644 (file)
@@ -28,9 +28,16 @@ import java.awt.Graphics;
 import java.awt.Graphics2D;
 import java.awt.RenderingHints;
 import java.io.File;
+import java.lang.reflect.InvocationTargetException;
 import java.util.List;
 import java.util.Locale;
 import java.util.Map;
+import java.util.concurrent.Callable;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.FutureTask;
 
 import javax.swing.JPanel;
 import javax.swing.JSplitPane;
@@ -472,11 +479,48 @@ public class AppJmol extends StructureViewerBase
     };
     String view = MessageManager.getString("action.view")
             .toLowerCase(Locale.ROOT);
-    ImageExporter exporter = new ImageExporter(writer,
+    final ImageExporter exporter = new ImageExporter(writer,
             getProgressIndicator(), type, getTitle());
     
-    exporter.doExport(file, this, width, height, view, renderer, userBis);
-    
+    final Throwable[] exceptions = new Throwable[1];
+    exceptions[0] = null;
+    final AppJmol us = this;
+    try
+    {
+      Thread runner = Executors.defaultThreadFactory().newThread(new Runnable()
+      {
+        @Override
+        public void run()
+        {
+          try
+          {
+            exporter.doExport(file, us, width, height, view, renderer,
+                    userBis);
+          } catch (Throwable t)
+          {
+            exceptions[0] = t;
+          }
+        }
+      });
+      runner.start();
+      do { Thread.sleep(25); } while (runner.isAlive());
+    } catch (Throwable e)
+    {
+      throw new ImageOutputException(
+              "Unexpected error when generating image", e);
+    }
+    if (exceptions[0] != null)
+    {
+      if (exceptions[0] instanceof ImageOutputException)
+      {
+        throw ((ImageOutputException) exceptions[0]);
+      }
+      else
+      {
+        throw new ImageOutputException(
+                "Unexpected error when generating image", exceptions[0]);
+      }
+    }
   }
 
   @Override
index b901ae4..12ff20b 100644 (file)
@@ -1078,7 +1078,36 @@ public class Desktop extends jalview.jbgui.GDesktop
 
     setKeyBindings(frame);
 
-    desktop.add(frame);
+    // Since the latest FlatLaf patch, we occasionally have problems showing structureViewer frames...
+    int tries=3;
+    boolean shown=false;
+    Exception last=null;
+    do
+    {
+      try
+      {
+        desktop.add(frame);
+        shown=true;
+      } catch (IllegalArgumentException iaex)
+      {
+        last=iaex;
+        tries--;
+        jalview.bin.Console.info(
+                "Squashed IllegalArgument Exception (" + tries + " left) for "+frame.getTitle(),
+                iaex);
+        try
+        {
+          Thread.sleep(5);
+        } catch (InterruptedException iex)
+        {
+        }
+        ;
+      }
+    } while (!shown && tries > 0);
+    if (!shown)
+    {
+      jalview.bin.Console.error("Serious Problem whilst showing window "+frame.getTitle(),last);
+    }
 
     windowMenu.add(menuItem);
 
@@ -3623,8 +3652,22 @@ public class Desktop extends jalview.jbgui.GDesktop
     {
       Desktop.instance.closeAll_actionPerformed(null);
       Desktop.instance.setVisible(false);
-      Desktop.instance.dispose();
+      Desktop us = Desktop.instance;
       Desktop.instance = null;
+      // call dispose in a separate thread - try to avoid indirect deadlocks
+      new Thread(new Runnable() {
+        @Override
+        public void run()
+        {
+          ExecutorService dex = us.dialogExecutor;
+          if (dex!=null) {
+            dex.shutdownNow();
+            us.dialogExecutor=null;
+            us.block.drainPermits();
+          }
+          us.dispose();
+        }
+      }).start();
     }
   }
 
index f337b39..4ea30d9 100644 (file)
@@ -124,6 +124,11 @@ public class ImageExporter
      */
     if (file == null && !Jalview.isHeadlessMode())
     {
+      if (Desktop.instance.isInBatchMode())
+      {
+        // defensive error report - we could wait for user input.. I  guess ?
+        throw(new ImageOutputException("Need an output file to render to when exporting images in batch mode!"));
+      }
       JalviewFileChooser chooser = imageType.getFileChooser();
       chooser.setFileView(new JalviewFileView());
       MessageManager.formatMessage("label.create_image_of",
index be9293f..88c1292 100644 (file)
@@ -1734,10 +1734,9 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener
   protected void addReferenceAnnotations_actionPerformed(
           Map<SequenceI, List<AlignmentAnnotation>> candidates)
   {
-    final SequenceGroup selectionGroup = this.ap.av.getSelectionGroup();
     final AlignmentI alignment = this.ap.getAlignment();
     AlignmentUtils.addReferenceAnnotations(candidates, alignment,
-            selectionGroup);
+            null);
     refresh();
   }
 
index 0454cab..8379777 100644 (file)
@@ -25,10 +25,11 @@ import java.io.InputStream;
 import java.net.HttpURLConnection;
 import java.net.ProtocolException;
 import java.net.URL;
-import java.util.List;
 
 import javax.ws.rs.HttpMethod;
 
+import jalview.bin.Cache;
+
 public class HttpUtils
 {
 
@@ -101,4 +102,46 @@ public class HttpUtils
     return connection.getResponseCode() == 200;
   }
 
+  public static String getUserAgent()
+  {
+    return getUserAgent(null);
+  }
+
+  public static String getUserAgent(String className)
+  {
+    StringBuilder sb = new StringBuilder();
+    sb.append("Jalview");
+    sb.append('/');
+    sb.append(Cache.getDefault("VERSION", "Unknown"));
+    sb.append(" (");
+    sb.append(System.getProperty("os.name"));
+    sb.append("; ");
+    sb.append(System.getProperty("os.arch"));
+    sb.append(' ');
+    sb.append(System.getProperty("os.name"));
+    sb.append(' ');
+    sb.append(System.getProperty("os.version"));
+    sb.append("; ");
+    sb.append("java/");
+    sb.append(System.getProperty("java.version"));
+    sb.append("; ");
+    sb.append("jalview/");
+    sb.append(ChannelProperties.getProperty("channel"));
+    if (className != null)
+    {
+      sb.append("; ");
+      sb.append(className);
+    }
+    String installation = Cache.applicationProperties
+            .getProperty("INSTALLATION");
+    if (installation != null)
+    {
+      sb.append("; ");
+      sb.append(installation);
+    }
+    sb.append(')');
+    sb.append(" help@jalview.org");
+    return sb.toString();
+  }
+
 }
index 0c707e5..037854b 100644 (file)
@@ -116,7 +116,7 @@ public class SiftsClient implements SiftsClientI
 
   private static final String NOT_OBSERVED = "Not_Observed";
 
-  private static final String SIFTS_FTP_BASE_URL = "http://ftp.ebi.ac.uk/pub/databases/msd/sifts/xml/";
+  private static final String SIFTS_SPLIT_FTP_BASE_URL = "https://ftp.ebi.ac.uk/pub/databases/msd/sifts/split_xml/";
 
   private final static String NEWLINE = System.lineSeparator();
 
@@ -305,7 +305,7 @@ public class SiftsClient implements SiftsClientI
       pdbId = pdbId.replace(".cif", "");
     }
     String siftFile = pdbId + ".xml.gz";
-    String siftsFileFTPURL = SIFTS_FTP_BASE_URL + siftFile;
+    String siftsFileFTPURL = getDownloadUrlFor(siftFile);
 
     /*
      * Download the file from URL to either
@@ -348,6 +348,11 @@ public class SiftsClient implements SiftsClientI
     return downloadTo;
   }
 
+  public static String getDownloadUrlFor(String siftFile)
+  {
+    return SIFTS_SPLIT_FTP_BASE_URL +siftFile.substring(1, 3)+"/"+siftFile;
+  }
+
   /**
    * Delete the SIFTs file for the given PDB Id in the local SIFTs download
    * directory
index 5134511..44bea81 100644 (file)
@@ -29,11 +29,15 @@ import jalview.datamodel.SequenceI;
 import jalview.gui.JvOptionPane;
 
 import java.io.PrintStream;
+import java.nio.charset.Charset;
+import java.util.Locale;
 
 import org.testng.annotations.BeforeClass;
 import org.testng.annotations.BeforeMethod;
 import org.testng.annotations.Test;
 
+import com.google.common.base.Charsets;
+
 /**
  * Test the alignment -> Mapping routines
  * 
@@ -83,16 +87,44 @@ public class TestAlignSeq
     assertEquals(as.getAStr1(), as.getAStr2());
 
     Mapping s1tos2 = as.getMappingFromS1(false);
+    checkMapping(s1tos2,s1,s2);
+  }
+
+  public void checkMapping(Mapping s1tos2,SequenceI _s1,SequenceI _s2)
+  {
     System.out.println(s1tos2.getMap().toString());
-    for (int i = s2.getStart(); i < s2.getEnd(); i++)
+    for (int i = _s2.getStart(); i < _s2.getEnd(); i++)
     {
-      System.out.println("Position in s2: " + i
-              + " maps to position in s1: " + s1tos2.getPosition(i));
-      // TODO fails: getCharAt doesn't allow for the start position??
-      // assertEquals(String.valueOf(s2.getCharAt(i)),
-      // String.valueOf(s1.getCharAt(s1tos2.getPosition(i))));
+      int p=s1tos2.getPosition(i);
+      char s2c=_s2.getCharAt(i-_s2.getStart());
+      char s1c=_s1.getCharAt(p-_s1.getStart());
+      System.out.println("Position in s2: " + i +s2c 
+      + " maps to position in s1: " +p+s1c);
+      assertEquals(s1c,s2c);
     }
   }
+  @Test(groups = { "Functional" })
+  /**
+   * simple test that mapping from alignment corresponds identical positions.
+   */
+  public void testGetMappingForS1_withLowerCase()
+  {
+    // make one of the sequences lower case
+    SequenceI ns2 = new Sequence(s2);
+    ns2.replace('D', 'd');
+    AlignSeq as = AlignSeq.doGlobalNWAlignment(s1, ns2, AlignSeq.PEP);
+    System.out.println("s1: " + as.getAStr1());
+    System.out.println("s2: " + as.getAStr2());
+
+    // aligned results match
+    assertEquals("ASDFA", as.getAStr1());
+    assertEquals(as.getAStr1(), as.getAStr2().toUpperCase(Locale.ROOT));
+
+    Mapping s1tos2 = as.getMappingFromS1(false);
+    assertEquals("ASdFA",as.getAStr2());
+    // verify mapping is consistent between original all-caps sequences
+    checkMapping(s1tos2,s1,s2);
+  }
 
   @Test(groups = { "Functional" })
   public void testExtractGaps()
index 61892df..81f0e1f 100644 (file)
@@ -19,7 +19,6 @@ import jalview.gui.Desktop;
 import jalview.gui.JvOptionPane;
 import jalview.util.ArrayUtils;
 
-@Test
 public class CommandsTest
 {
   private static final String testfiles = "test/jalview/bin/argparser/testfiles";
@@ -77,39 +76,49 @@ public class CommandsTest
   public void commandsOpenTest(String cmdLine, boolean cmdArgs,
           int numFrames, String[] sequences)
   {
-    String[] args = (cmdLine + " --gui").split("\\s+");
-    callJalviewMain(args);
-    Commands cmds = Jalview.getInstance().getCommands();
-    Assert.assertNotNull(cmds);
-    Assert.assertEquals(cmds.commandArgsProvided(), cmdArgs,
-            "Commands were not provided in the args");
-    Assert.assertEquals(cmds.argsWereParsed(), cmdArgs,
-            "Overall command parse and operation is false");
+    try
+    {
+      String[] args = (cmdLine + " --gui").split("\\s+");
+      callJalviewMain(args);
+      Commands cmds = Jalview.getInstance().getCommands();
+      Assert.assertNotNull(cmds);
+      Assert.assertEquals(cmds.commandArgsProvided(), cmdArgs,
+              "Commands were not provided in the args");
+      Assert.assertEquals(cmds.argsWereParsed(), cmdArgs,
+              "Overall command parse and operation is false");
 
-    Assert.assertEquals(Desktop.getAlignFrames().length, numFrames,
-            "Wrong number of AlignFrames");
+      Assert.assertEquals(Desktop.getAlignFrames().length, numFrames,
+              "Wrong number of AlignFrames");
 
-    if (sequences != null)
-    {
-      Set<String> openedSequenceNames = new HashSet<>();
-      AlignFrame[] afs = Desktop.getAlignFrames();
-      for (AlignFrame af : afs)
+      if (sequences != null)
       {
-        openedSequenceNames
-                .addAll(af.getViewport().getAlignment().getSequenceNames());
-      }
-      for (String sequence : sequences)
-      {
-        Assert.assertTrue(openedSequenceNames.contains(sequence),
-                "Sequence '" + sequence
-                        + "' was not found in opened alignment files: "
-                        + cmdLine + ".\nOpened sequence names are:\n"
-                        + String.join("\n", openedSequenceNames));
+        Set<String> openedSequenceNames = new HashSet<>();
+        AlignFrame[] afs = Desktop.getAlignFrames();
+        for (AlignFrame af : afs)
+        {
+          openedSequenceNames.addAll(
+                  af.getViewport().getAlignment().getSequenceNames());
+        }
+        for (String sequence : sequences)
+        {
+          Assert.assertTrue(openedSequenceNames.contains(sequence),
+                  "Sequence '" + sequence
+                          + "' was not found in opened alignment files: "
+                          + cmdLine + ".\nOpened sequence names are:\n"
+                          + String.join("\n", openedSequenceNames));
+        }
       }
-    }
 
-    Assert.assertFalse(
-            lookForSequenceName("THIS_SEQUENCE_ID_DOESN'T_EXIST"));
+      Assert.assertFalse(
+              lookForSequenceName("THIS_SEQUENCE_ID_DOESN'T_EXIST"));
+    } catch (Exception x)
+    {
+      Assert.fail("Unexpected exception during commandsOpenTest", x);
+    } finally
+    {
+      tearDown();
+
+    }
   }
 
   @Test(groups = "Functional", dataProvider = "structureImageOutputFiles")
@@ -118,26 +127,35 @@ public class CommandsTest
   {
     cleanupFiles(filenames);
     String[] args = (cmdLine + " --gui").split("\\s+");
-    callJalviewMain(args);
-    Commands cmds = Jalview.getInstance().getCommands();
-    Assert.assertNotNull(cmds);
-    File lastFile = null;
-    for (String filename : filenames)
+    try
     {
-      File file = new File(filename);
-      Assert.assertTrue(file.exists(), "File '" + filename
-              + "' was not created by '" + cmdLine + "'");
-      Assert.assertTrue(file.isFile(), "File '" + filename
-              + "' is not a file from '" + cmdLine + "'");
-      Assert.assertTrue(Files.size(file.toPath()) > 0, "File '" + filename
-              + "' has no content from '" + cmdLine + "'");
-      // make sure the successive output files get bigger!
-      if (lastFile != null)
-        Assert.assertTrue(
-                Files.size(file.toPath()) > Files.size(lastFile.toPath()));
+      callJalviewMain(args);
+      Commands cmds = Jalview.getInstance().getCommands();
+      Assert.assertNotNull(cmds);
+      File lastFile = null;
+      for (String filename : filenames)
+      {
+        File file = new File(filename);
+        Assert.assertTrue(file.exists(), "File '" + filename
+                + "' was not created by '" + cmdLine + "'");
+        Assert.assertTrue(file.isFile(), "File '" + filename
+                + "' is not a file from '" + cmdLine + "'");
+        Assert.assertTrue(Files.size(file.toPath()) > 0, "File '" + filename
+                + "' has no content from '" + cmdLine + "'");
+        // make sure the successive output files get bigger!
+        if (lastFile != null)
+          Assert.assertTrue(Files.size(file.toPath()) > Files
+                  .size(lastFile.toPath()));
+      }
+    } catch (Exception x)
+    {
+      Assert.fail("Unexpected exception during structureImageOutputTest",
+              x);
+    } finally
+    {
+      cleanupFiles(filenames);
+      tearDown();
     }
-    cleanupFiles(filenames);
-    tearDown();
   }
 
   @Test(groups = "Functional", dataProvider = "argfileOutputFiles")
@@ -146,6 +164,7 @@ public class CommandsTest
   {
     cleanupFiles(filenames);
     String[] args = (cmdLine + " --gui").split("\\s+");
+    try {
     callJalviewMain(args);
     Commands cmds = Jalview.getInstance().getCommands();
     Assert.assertNotNull(cmds);
@@ -164,8 +183,15 @@ public class CommandsTest
         Assert.assertTrue(
                 Files.size(file.toPath()) > Files.size(lastFile.toPath()));
     }
-    cleanupFiles(filenames);
-    tearDown();
+    } catch (Exception x)
+    {
+      Assert.fail("Unexpected exception during argFilesGlobAndSubstitutions",
+              x);
+    } finally
+    {
+      cleanupFiles(filenames);
+      tearDown();
+    }
   }
 
   @DataProvider(name = "structureImageOutputFiles")
index 344d74d..7bbb9f3 100644 (file)
@@ -2315,8 +2315,16 @@ public class SequenceTest
   {
     Sequence origSeq = new Sequence("MYSEQ", "THISISASEQ");
     Sequence toSeq = new Sequence("MYSEQ", "THISISASEQ");
+    origSeq.setDescription("DESCRIPTION");
     origSeq.addDBRef(new DBRefEntry("UNIPROT", "0", "Q12345", null, true));
+
+    toSeq.transferAnnotation(origSeq, null);
+    assertEquals("DESCRIPTION",toSeq.getDescription());
+    toSeq = new Sequence("MYSEQ", "THISISASEQ");
+    toSeq.setDescription("unchanged");
     toSeq.transferAnnotation(origSeq, null);
+    assertEquals("unchanged",toSeq.getDescription());
+    
     assertTrue(toSeq.getDBRefs().size() == 1);
 
     assertTrue(toSeq.getDBRefs().get(0).isCanonical());
index 44a6a02..0f5bd4d 100644 (file)
@@ -195,8 +195,10 @@ public class SiftsClientTest
     SiftsSettings.setCacheThresholdInDays("2");
     SiftsSettings.setFailSafePIDThreshold("70");
     PDBfile pdbFile;
+    
     pdbFile = new PDBfile(false, false, false,
             "test/jalview/io/" + testPDBId + ".pdb", DataSourceType.FILE);
+    // TODO: this uses a network connection - we should mock the sifts testPDBId.xml.gz
     siftsClient = new SiftsClient(pdbFile);
   }
 
@@ -205,6 +207,12 @@ public class SiftsClientTest
   {
     siftsClient = null;
   }
+  
+  @Test(groups= {"Functional"})
+  public void testSIFTsDownloadURL() {
+    String expectedUrl = "https://ftp.ebi.ac.uk/pub/databases/msd/sifts/split_xml/xy/1xyz.sifts.xml.gz";
+    Assert.assertEquals(SiftsClient.getDownloadUrlFor("1xyz.sifts.xml.gz"), expectedUrl);
+  }
 
   @Test(groups = { "Network" })
   public void getSIFTsFileTest() throws SiftsException, IOException
@@ -215,7 +223,7 @@ public class SiftsClientTest
     long t1 = siftsFile.lastModified();
 
     // re-read file should be returned from cache
-    siftsFile = SiftsClient.downloadSiftsFile(testPDBId);
+    siftsFile = SiftsClient.getSiftsFile(testPDBId);
     FileAssert.assertFile(siftsFile);
     long t2 = siftsFile.lastModified();
     assertEquals(t1, t2);
@@ -368,7 +376,7 @@ public class SiftsClientTest
   {
     SequenceI invalidTestSeq = new Sequence("testSeq", "ABCDEFGH");
     DBRefEntry invalidDBRef = new DBRefEntry();
-    invalidDBRef.setAccessionId("BLAR");
+    invalidDBRef.setAccessionId("BLAR"); // note no version is set, so also invalid
     invalidTestSeq.addDBRef(invalidDBRef);
     siftsClient.getValidSourceDBRef(invalidTestSeq);
   }