JAL-4036 New configuration, target and pagination process for 2022-07 new Uniprot API
[jalview.git] / src / jalview / fts / service / uniprot / UniProtFTSRestClient.java
index 2606b62..05ccba7 100644 (file)
  * The Jalview Authors are detailed in the 'AUTHORS' file.
  */
 
+/*
+ * 2022-07-20 bsoares
+ * See https://issues.jalview.org/browse/JAL-4036
+ * The new Uniprot API is not dissimilar to the old one, but has some important changes.
+ * Some group names have changed slightly, some old groups have gone and there are quite a few new groups.
+ * 
+ * Most changes are mappings of old column ids to new field ids. There are a handful of old
+ * columns not mapped to new fields, and new fields without an old column.
+ * [aside: not all possible columns were listed in the resources/fts/uniprot_data_columns.txt file.
+ * These were presumably additions after the file was created]
+ * For existing/mapped fields, the same preferences found in the resource file have been migrated to
+ * the new file with the new field name, id and group.
+ * 
+ * The new mapped groups and files are stored and read from resources/fts/uniprot_data_columns-2022.txt.
+ * 
+ * There is now no "sort" query string parameter.
+ * 
+ * See https://www.uniprot.org/help/api_queries
+ * 
+ * SIGNIFICANT CHANGE: Pagination is no longer performed using a record offset, but with a "cursor"
+ * query string parameter that is not really a cursor.  The value is an opaque string that is passed (or
+ * rather a whole URL is passed) in the "Link" header of the HTTP response of the previous page.
+ * Where such a link is passed it is put into the cursors ArrayList.
+ * There are @Overridden methods in UniprotFTSPanel.
+ */
+
 package jalview.fts.service.uniprot;
 
+import java.lang.invoke.MethodHandles;
+import java.net.MalformedURLException;
+import java.net.URL;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.List;
@@ -37,31 +66,37 @@ import jalview.bin.Cache;
 import jalview.bin.Console;
 import jalview.fts.api.FTSData;
 import jalview.fts.api.FTSDataColumnI;
-import jalview.fts.api.FTSRestClientI;
 import jalview.fts.core.FTSRestClient;
 import jalview.fts.core.FTSRestRequest;
 import jalview.fts.core.FTSRestResponse;
+import jalview.util.ChannelProperties;
 import jalview.util.MessageManager;
 import jalview.util.Platform;
 
 public class UniProtFTSRestClient extends FTSRestClient
 {
-  private static final String DEFAULT_UNIPROT_DOMAIN = "https://legacy.uniprot.org";
+  private static final String DEFAULT_UNIPROT_DOMAIN = "https://rest.uniprot.org";
+
+  private static final String USER_AGENT = ChannelProperties
+          .getProperty("app_name", "Jalview") + " "
+          + Cache.getDefault("VERSION", "Unknown") + " "
+          + MethodHandles.lookup().lookupClass() + " help@jalview.org";
 
   static
   {
     Platform.addJ2SDirectDatabaseCall(DEFAULT_UNIPROT_DOMAIN);
   }
 
-  private static FTSRestClientI instance = null;
+  private static UniProtFTSRestClient instance = null;
 
   public final String uniprotSearchEndpoint;
 
   public UniProtFTSRestClient()
   {
     super();
+    this.clearCursors();
     uniprotSearchEndpoint = Cache.getDefault("UNIPROT_DOMAIN",
-            DEFAULT_UNIPROT_DOMAIN) + "/uniprot/";
+            DEFAULT_UNIPROT_DOMAIN) + "/uniprotkb/search";
   }
 
   @SuppressWarnings("unchecked")
@@ -69,6 +104,12 @@ public class UniProtFTSRestClient extends FTSRestClient
   public FTSRestResponse executeRequest(FTSRestRequest uniprotRestRequest)
           throws Exception
   {
+    return executeRequest(uniprotRestRequest, null);
+  }
+
+  public FTSRestResponse executeRequest(FTSRestRequest uniprotRestRequest,
+          String cursor) throws Exception
+  {
     try
     {
       String wantedFields = getDataColumnsFieldsAsCommaDelimitedString(
@@ -85,11 +126,10 @@ public class UniProtFTSRestClient extends FTSRestClient
       }
       else
       {
-        query = uniprotRestRequest.getFieldToSearchBy()
-                .equalsIgnoreCase("Search All")
-                        ? uniprotRestRequest.getSearchTerm()
-                                + " or mnemonic:"
-                                + uniprotRestRequest.getSearchTerm()
+        query = uniprotRestRequest.getFieldToSearchBy().equalsIgnoreCase(
+                "Search All") ? uniprotRestRequest.getSearchTerm()
+                        // + " or mnemonic:"
+                        // + uniprotRestRequest.getSearchTerm()
                         : uniprotRestRequest.getFieldToSearchBy() + ":"
                                 + uniprotRestRequest.getSearchTerm();
       }
@@ -119,18 +159,62 @@ public class UniProtFTSRestClient extends FTSRestClient
 
       WebResource webResource = null;
       webResource = client.resource(uniprotSearchEndpoint)
-              .queryParam("format", "tab")
-              .queryParam("columns", wantedFields)
-              .queryParam("limit", String.valueOf(responseSize))
-              .queryParam("offset", String.valueOf(offSet))
-              .queryParam("sort", "score").queryParam("query", query);
-      if (Console.isDebugEnabled())
+              .queryParam("format", "tsv")
+              .queryParam("fields", wantedFields)
+              .queryParam("size", String.valueOf(responseSize))
+              /* 2022 new api has no "sort"
+               * .queryParam("sort", "score")
+               */
+              .queryParam("query", query);
+      if (offSet != 0 && cursor != null && cursor.length() > 0)
+      // 2022 new api does not do pagination with an offset, it requires a
+      // "cursor" parameter with a key (given for the next page).
+      // (see https://www.uniprot.org/help/pagination)
       {
-        Console.debug("Uniprot FTS Request: " + webResource.toString());
+        webResource = webResource.queryParam("cursor", cursor);
       }
+      Console.debug(
+              "Uniprot FTS Request: " + webResource.getURI().toString());
       // Execute the REST request
-      ClientResponse clientResponse = webResource
-              .accept(MediaType.TEXT_PLAIN).get(clientResponseClass);
+      WebResource.Builder wrBuilder = webResource
+              .accept(MediaType.TEXT_PLAIN);
+      if (!Platform.isJS())
+      /**
+       * Java only
+       * 
+       * @j2sIgnore
+       */
+      {
+        wrBuilder.header("User-Agent", USER_AGENT);
+      }
+      ClientResponse clientResponse = wrBuilder.get(clientResponseClass);
+
+      if (!Platform.isJS())
+      /**
+       * Java only
+       * 
+       * @j2sIgnore
+       */
+      {
+        if (clientResponse.getHeaders().containsKey("Link"))
+        {
+          // extract the URL from the 'Link: <URL>; ref="stuff"' header
+          String linkHeader = clientResponse.getHeaders().get("Link")
+                  .get(0);
+          if (linkHeader.indexOf("<") > -1)
+          {
+            String temp = linkHeader.substring(linkHeader.indexOf("<") + 1);
+            if (temp.indexOf(">") > -1)
+            {
+              String nextUrl = temp.substring(0, temp.indexOf(">"));
+              // then get the cursor value from the query string parameters
+              String nextCursor = getQueryParam("cursor", nextUrl);
+              setCursor(cursorPage + 1, nextCursor);
+            }
+          }
+        }
+      }
+
       String uniProtTabDelimittedResponseString = clientResponse
               .getEntity(String.class);
       // Make redundant objects eligible for garbage collection to conserve
@@ -144,15 +228,26 @@ public class UniProtFTSRestClient extends FTSRestClient
         throw new Exception(errorMessage);
 
       }
-      int xTotalResults = Platform.isJS() ? 1
-              : Integer.valueOf(clientResponse.getHeaders()
-                      .get("X-Total-Results").get(0));
+      // new Uniprot API is not including a "X-Total-Results" header when there
+      // are 0 results
+      List<String> resultsHeaders = clientResponse.getHeaders()
+              .get("X-Total-Results");
+      int xTotalResults = 0;
+      if (Platform.isJS())
+      {
+        xTotalResults = 1;
+      }
+      else if (resultsHeaders != null && resultsHeaders.size() >= 1)
+      {
+        xTotalResults = Integer.valueOf(resultsHeaders.get(0));
+      }
       clientResponse = null;
       client = null;
       return parseUniprotResponse(uniProtTabDelimittedResponseString,
               uniprotRestRequest, xTotalResults);
     } catch (Exception e)
     {
+      Console.debug("Exception caught from response", e);
       String exceptionMsg = e.getMessage();
       if (exceptionMsg.contains("SocketException"))
       {
@@ -352,7 +447,7 @@ public class UniProtFTSRestClient extends FTSRestClient
     };
   }
 
-  public static FTSRestClientI getInstance()
+  public static UniProtFTSRestClient getInstance()
   {
     if (instance == null)
     {
@@ -364,7 +459,95 @@ public class UniProtFTSRestClient extends FTSRestClient
   @Override
   public String getColumnDataConfigFileName()
   {
-    return "/fts/uniprot_data_columns.txt";
+    return "/fts/uniprot_data_columns-2022.txt";
+  }
+
+  /* 2022-07-20 bsoares
+   * used for the new API "cursor" pagination. See https://www.uniprot.org/help/pagination
+   */
+  private ArrayList<String> cursors;
+
+  private int cursorPage = 0;
+
+  protected int getCursorPage()
+  {
+    return cursorPage;
+  }
+
+  protected void setCursorPage(int i)
+  {
+    cursorPage = i;
+  }
+
+  protected void setPrevCursorPage()
+  {
+    if (cursorPage > 0)
+      cursorPage--;
+  }
+
+  protected void setNextCursorPage()
+  {
+    cursorPage++;
+  }
+
+  protected void clearCursors()
+  {
+    cursors = new ArrayList(10);
   }
 
-}
+  protected String getCursor(int i)
+  {
+    return cursors.get(i);
+  }
+
+  protected String getNextCursor()
+  {
+    if (cursors.size() < cursorPage + 2)
+      return null;
+    return cursors.get(cursorPage + 1);
+  }
+
+  protected String getPrevCursor()
+  {
+    if (cursorPage == 0)
+      return null;
+    return cursors.get(cursorPage - 1);
+  }
+
+  protected void setCursor(int i, String c)
+  {
+    cursors.ensureCapacity(i + 1);
+    while (cursors.size() <= i)
+    {
+      cursors.add(null);
+    }
+    cursors.set(i, c);
+    Console.debug(
+            "Set UniprotFRSRestClient cursors[" + i + "] to '" + c + "'");
+    // cursors.add(c);
+  }
+
+  public static String getQueryParam(String param, String u)
+  {
+    if (param == null || u == null)
+      return null;
+    try
+    {
+      URL url = new URL(u);
+      String[] kevs = url.getQuery().split("&");
+      for (int j = 0; j < kevs.length; j++)
+      {
+        String[] kev = kevs[j].split("=", 2);
+        if (param.equals(kev[0]))
+        {
+          return kev[1];
+        }
+      }
+    } catch (MalformedURLException e)
+    {
+      // TODO Auto-generated catch block
+      e.printStackTrace();
+    }
+    return null;
+  }
+}
\ No newline at end of file