Merge branch 'releases/Release_2_11_3_Branch'
[jalview.git] / src / jalview / fts / service / uniprot / UniProtFTSRestClient.java
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
22 package jalview.fts.service.uniprot;
23
24 import java.net.MalformedURLException;
25 import java.net.URL;
26 import java.util.ArrayList;
27 import java.util.Collection;
28 import java.util.List;
29 import java.util.Objects;
30
31 import javax.ws.rs.core.MediaType;
32
33 import com.sun.jersey.api.client.Client;
34 import com.sun.jersey.api.client.ClientResponse;
35 import com.sun.jersey.api.client.WebResource;
36 import com.sun.jersey.api.client.config.DefaultClientConfig;
37
38 import jalview.bin.Cache;
39 import jalview.bin.Console;
40 import jalview.fts.api.FTSData;
41 import jalview.fts.api.FTSDataColumnI;
42 import jalview.fts.core.FTSRestClient;
43 import jalview.fts.core.FTSRestRequest;
44 import jalview.fts.core.FTSRestResponse;
45 import jalview.util.ChannelProperties;
46 import jalview.util.MessageManager;
47 import jalview.util.Platform;
48
49 /*
50  * 2022-07-20 bsoares
51  * See https://issues.jalview.org/browse/JAL-4036
52  * The new Uniprot API is not dissimilar to the old one, but has some important changes.
53  * Some group names have changed slightly, some old groups have gone and there are quite a few new groups.
54  * 
55  * Most changes are mappings of old column ids to new field ids. There are a handful of old
56  * columns not mapped to new fields, and new fields without an old column.
57  * [aside: not all possible columns were listed in the resources/fts/uniprot_data_columns.txt file.
58  * These were presumably additions after the file was created]
59  * For existing/mapped fields, the same preferences found in the resource file have been migrated to
60  * the new file with the new field name, id and group.
61  * 
62  * The new mapped groups and files are stored and read from resources/fts/uniprot_data_columns-2022.txt.
63  * 
64  * There is now no "sort" query string parameter.
65  * 
66  * See https://www.uniprot.org/help/api_queries
67  * 
68  * SIGNIFICANT CHANGE: Pagination is no longer performed using a record offset, but with a "cursor"
69  * query string parameter that is not really a cursor.  The value is an opaque string that is passed (or
70  * rather a whole URL is passed) in the "Link" header of the HTTP response of the previous page.
71  * Where such a link is passed it is put into the cursors ArrayList.
72  * There are @Overridden methods in UniprotFTSPanel.
73  */
74
75 public class UniProtFTSRestClient extends FTSRestClient
76 {
77   private static final String DEFAULT_UNIPROT_DOMAIN = "https://rest.uniprot.org";
78
79   private static final String USER_AGENT = ChannelProperties
80           .getProperty("app_name", "Jalview") + " "
81           + Cache.getDefault("VERSION", "Unknown") + " "
82           + UniProtFTSRestClient.class.toString() + " help@jalview.org";
83
84   static
85   {
86     Platform.addJ2SDirectDatabaseCall(DEFAULT_UNIPROT_DOMAIN);
87   }
88
89   private static UniProtFTSRestClient instance = null;
90
91   public final String uniprotSearchEndpoint;
92
93   public UniProtFTSRestClient()
94   {
95     super();
96     this.clearCursors();
97     uniprotSearchEndpoint = Cache.getDefault("UNIPROT_2022_DOMAIN",
98             DEFAULT_UNIPROT_DOMAIN) + "/uniprotkb/search";
99   }
100
101   @SuppressWarnings("unchecked")
102   @Override
103   public FTSRestResponse executeRequest(FTSRestRequest uniprotRestRequest)
104           throws Exception
105   {
106     return executeRequest(uniprotRestRequest, null);
107   }
108
109   public FTSRestResponse executeRequest(FTSRestRequest uniprotRestRequest,
110           String cursor) throws Exception
111   {
112     try
113     {
114       String wantedFields = getDataColumnsFieldsAsCommaDelimitedString(
115               uniprotRestRequest.getWantedFields());
116       int responseSize = (uniprotRestRequest.getResponseSize() == 0)
117               ? getDefaultResponsePageSize()
118               : uniprotRestRequest.getResponseSize();
119
120       int offSet = uniprotRestRequest.getOffSet();
121       String query;
122       if (isAdvancedQuery(uniprotRestRequest.getSearchTerm()))
123       {
124         query = uniprotRestRequest.getSearchTerm();
125       }
126       else
127       {
128         query = uniprotRestRequest.getFieldToSearchBy().equalsIgnoreCase(
129                 "Search All") ? uniprotRestRequest.getSearchTerm()
130                         // + " or mnemonic:"
131                         // + uniprotRestRequest.getSearchTerm()
132                         : uniprotRestRequest.getFieldToSearchBy() + ":"
133                                 + uniprotRestRequest.getSearchTerm();
134       }
135
136       // BH 2018 the trick here is to coerce the classes in Javascript to be
137       // different from the ones in Java yet still allow this to be correct for
138       // Java
139       Client client;
140       Class<ClientResponse> clientResponseClass;
141       if (Platform.isJS())
142       {
143         // JavaScript only -- coerce types to Java types for Java
144         client = (Client) (Object) new jalview.javascript.web.Client();
145         clientResponseClass = (Class<ClientResponse>) (Object) jalview.javascript.web.ClientResponse.class;
146       }
147       else
148       /**
149        * Java only
150        * 
151        * @j2sIgnore
152        */
153       {
154         // Java only
155         client = Client.create(new DefaultClientConfig());
156         clientResponseClass = ClientResponse.class;
157       }
158
159       WebResource webResource = null;
160       webResource = client.resource(uniprotSearchEndpoint)
161               .queryParam("format", "tsv")
162               .queryParam("fields", wantedFields)
163               .queryParam("size", String.valueOf(responseSize))
164               /* 2022 new api has no "sort"
165                * .queryParam("sort", "score")
166                */
167               .queryParam("query", query);
168       if (offSet != 0 && cursor != null && cursor.length() > 0)
169       // 2022 new api does not do pagination with an offset, it requires a
170       // "cursor" parameter with a key (given for the next page).
171       // (see https://www.uniprot.org/help/pagination)
172       {
173         webResource = webResource.queryParam("cursor", cursor);
174       }
175       Console.debug(
176               "Uniprot FTS Request: " + webResource.getURI().toString());
177       // Execute the REST request
178       WebResource.Builder wrBuilder = webResource
179               .accept(MediaType.TEXT_PLAIN);
180       if (!Platform.isJS())
181       /**
182        * Java only
183        * 
184        * @j2sIgnore
185        */
186       {
187         wrBuilder.header("User-Agent", USER_AGENT);
188       }
189       ClientResponse clientResponse = wrBuilder.get(clientResponseClass);
190
191       if (!Platform.isJS())
192       /**
193        * Java only
194        * 
195        * @j2sIgnore
196        */
197       {
198         if (clientResponse.getHeaders().containsKey("Link"))
199         {
200           // extract the URL from the 'Link: <URL>; ref="stuff"' header
201           String linkHeader = clientResponse.getHeaders().get("Link")
202                   .get(0);
203           if (linkHeader.indexOf("<") > -1)
204           {
205             String temp = linkHeader.substring(linkHeader.indexOf("<") + 1);
206             if (temp.indexOf(">") > -1)
207             {
208               String nextUrl = temp.substring(0, temp.indexOf(">"));
209               // then get the cursor value from the query string parameters
210               String nextCursor = getQueryParam("cursor", nextUrl);
211               setCursor(cursorPage + 1, nextCursor);
212             }
213           }
214         }
215       }
216
217       String uniProtTabDelimittedResponseString = clientResponse
218               .getEntity(String.class);
219       // Make redundant objects eligible for garbage collection to conserve
220       // memory
221       // jalview.bin.Console.outPrintln(">>>>> response : "
222       // + uniProtTabDelimittedResponseString);
223       if (clientResponse.getStatus() != 200)
224       {
225         String errorMessage = getMessageByHTTPStatusCode(
226                 clientResponse.getStatus(), "Uniprot");
227         throw new Exception(errorMessage);
228
229       }
230       int xTotalResults = 0;
231       if (Platform.isJS())
232       {
233         xTotalResults = 1;
234       }
235       else
236       {
237         // new Uniprot API is not including a "X-Total-Results" header when
238         // there
239         // are 0 results
240         List<String> resultsHeaders = clientResponse.getHeaders()
241                 .get("X-Total-Results");
242         if (resultsHeaders != null && resultsHeaders.size() >= 1)
243         {
244           xTotalResults = Integer.valueOf(resultsHeaders.get(0));
245         }
246       }
247       clientResponse = null;
248       client = null;
249       return parseUniprotResponse(uniProtTabDelimittedResponseString,
250               uniprotRestRequest, xTotalResults);
251     } catch (Exception e)
252     {
253       Console.warn("Problem with the query: " + e.getMessage());
254       Console.debug("Exception stacktrace:", e);
255       String exceptionMsg = e.getMessage();
256       if (exceptionMsg.contains("SocketException"))
257       {
258         // No internet connection
259         throw new Exception(MessageManager.getString(
260                 "exception.unable_to_detect_internet_connection"));
261       }
262       else if (exceptionMsg.contains("UnknownHostException"))
263       {
264         // The server 'http://www.uniprot.org' is unreachable
265         throw new Exception(MessageManager.formatMessage(
266                 "exception.fts_server_unreachable", "Uniprot"));
267       }
268       else
269       {
270         throw e;
271       }
272     }
273   }
274
275   public boolean isAdvancedQuery(String query)
276   {
277     if (query.contains(" AND ") || query.contains(" OR ")
278             || query.contains(" NOT ") || query.contains(" ! ")
279             || query.contains(" || ") || query.contains(" && ")
280             || query.contains(":") || query.contains("-"))
281     {
282       return true;
283     }
284     return false;
285   }
286
287   public FTSRestResponse parseUniprotResponse(
288           String uniProtTabDelimittedResponseString,
289           FTSRestRequest uniprotRestRequest, int xTotalResults)
290   {
291     FTSRestResponse searchResult = new FTSRestResponse();
292     List<FTSData> result = null;
293     if (uniProtTabDelimittedResponseString == null
294             || uniProtTabDelimittedResponseString.trim().isEmpty())
295     {
296       searchResult.setNumberOfItemsFound(0);
297       return searchResult;
298     }
299     String[] foundDataRow = uniProtTabDelimittedResponseString.split("\n");
300     if (foundDataRow != null && foundDataRow.length > 0)
301     {
302       result = new ArrayList<>();
303       boolean firstRow = true;
304       for (String dataRow : foundDataRow)
305       {
306         // The first data row is usually the header data. This should be
307         // filtered out from the rest of the data See: JAL-2485
308         if (firstRow)
309         {
310           firstRow = false;
311           continue;
312         }
313         // jalview.bin.Console.outPrintln(dataRow);
314         result.add(getFTSData(dataRow, uniprotRestRequest));
315       }
316       searchResult.setNumberOfItemsFound(xTotalResults);
317       searchResult.setSearchSummary(result);
318     }
319     return searchResult;
320   }
321
322   // /**
323   // * Takes a collection of FTSDataColumnI and converts its 'code' values into
324   // a
325   // * tab delimited string.
326   // *
327   // * @param dataColumnFields
328   // * the collection of FTSDataColumnI to process
329   // * @return the generated comma delimited string from the supplied
330   // * FTSDataColumnI collection
331   // */
332   // private String getDataColumnsFieldsAsTabDelimitedString(
333   // Collection<FTSDataColumnI> dataColumnFields)
334   // {
335   // String result = "";
336   // if (dataColumnFields != null && !dataColumnFields.isEmpty())
337   // {
338   // StringBuilder returnedFields = new StringBuilder();
339   // for (FTSDataColumnI field : dataColumnFields)
340   // {
341   // if (field.getName().equalsIgnoreCase("Uniprot Id"))
342   // {
343   // returnedFields.append("\t").append("Entry");
344   // }
345   // else
346   // {
347   // returnedFields.append("\t").append(field.getName());
348   // }
349   // }
350   // returnedFields.deleteCharAt(0);
351   // result = returnedFields.toString();
352   // }
353   // return result;
354   // }
355
356   public static FTSData getFTSData(String tabDelimittedDataStr,
357           FTSRestRequest request)
358   {
359     String primaryKey = null;
360
361     Object[] summaryRowData;
362
363     Collection<FTSDataColumnI> diplayFields = request.getWantedFields();
364     int colCounter = 0;
365     summaryRowData = new Object[diplayFields.size()];
366     String[] columns = tabDelimittedDataStr.split("\t");
367     for (FTSDataColumnI field : diplayFields)
368     {
369       try
370       {
371         String fieldData = columns[colCounter];
372         if (field.isPrimaryKeyColumn())
373         {
374           primaryKey = fieldData;
375           summaryRowData[colCounter++] = primaryKey;
376         }
377         else if (fieldData == null || fieldData.isEmpty())
378         {
379           summaryRowData[colCounter++] = null;
380         }
381         else
382         {
383           try
384           {
385             summaryRowData[colCounter++] = (field.getDataType()
386                     .getDataTypeClass() == Integer.class)
387                             ? Integer.valueOf(fieldData.replace(",", ""))
388                             : (field.getDataType()
389                                     .getDataTypeClass() == Double.class)
390                                             ? Double.valueOf(fieldData)
391                                             : fieldData;
392           } catch (Exception e)
393           {
394             e.printStackTrace();
395             jalview.bin.Console.outPrintln("offending value:" + fieldData);
396           }
397         }
398       } catch (Exception e)
399       {
400         // e.printStackTrace();
401       }
402     }
403
404     final String primaryKey1 = primaryKey;
405
406     final Object[] summaryRowData1 = summaryRowData;
407     return new FTSData()
408     {
409       @Override
410       public Object[] getSummaryData()
411       {
412         return summaryRowData1;
413       }
414
415       @Override
416       public Object getPrimaryKey()
417       {
418         return primaryKey1;
419       }
420
421       /**
422        * Returns a string representation of this object;
423        */
424       @Override
425       public String toString()
426       {
427         StringBuilder summaryFieldValues = new StringBuilder();
428         for (Object summaryField : summaryRowData1)
429         {
430           summaryFieldValues.append(
431                   summaryField == null ? " " : summaryField.toString())
432                   .append("\t");
433         }
434         return summaryFieldValues.toString();
435       }
436
437       /**
438        * Returns hash code value for this object
439        */
440       @Override
441       public int hashCode()
442       {
443         return Objects.hash(primaryKey1, this.toString());
444       }
445
446       @Override
447       public boolean equals(Object that)
448       {
449         return this.toString().equals(that.toString());
450       }
451     };
452   }
453
454   public static UniProtFTSRestClient getInstance()
455   {
456     if (instance == null)
457     {
458       instance = new UniProtFTSRestClient();
459     }
460     return instance;
461   }
462
463   @Override
464   public String getColumnDataConfigFileName()
465   {
466     return "/fts/uniprot_data_columns-2022.txt";
467   }
468
469   /* 2022-07-20 bsoares
470    * used for the new API "cursor" pagination. See https://www.uniprot.org/help/pagination
471    */
472   private ArrayList<String> cursors;
473
474   private int cursorPage = 0;
475
476   protected int getCursorPage()
477   {
478     return cursorPage;
479   }
480
481   protected void setCursorPage(int i)
482   {
483     cursorPage = i;
484   }
485
486   protected void setPrevCursorPage()
487   {
488     if (cursorPage > 0)
489       cursorPage--;
490   }
491
492   protected void setNextCursorPage()
493   {
494     cursorPage++;
495   }
496
497   protected void clearCursors()
498   {
499     cursors = new ArrayList(10);
500   }
501
502   protected String getCursor(int i)
503   {
504     return cursors.get(i);
505   }
506
507   protected String getNextCursor()
508   {
509     if (cursors.size() < cursorPage + 2)
510       return null;
511     return cursors.get(cursorPage + 1);
512   }
513
514   protected String getPrevCursor()
515   {
516     if (cursorPage == 0)
517       return null;
518     return cursors.get(cursorPage - 1);
519   }
520
521   protected void setCursor(int i, String c)
522   {
523     cursors.ensureCapacity(i + 1);
524     while (cursors.size() <= i)
525     {
526       cursors.add(null);
527     }
528     cursors.set(i, c);
529     Console.debug(
530             "Set UniprotFRSRestClient cursors[" + i + "] to '" + c + "'");
531     // cursors.add(c);
532   }
533
534   public static String getQueryParam(String param, String u)
535   {
536     if (param == null || u == null)
537       return null;
538     try
539     {
540       URL url = new URL(u);
541       String[] kevs = url.getQuery().split("&");
542       for (int j = 0; j < kevs.length; j++)
543       {
544         String[] kev = kevs[j].split("=", 2);
545         if (param.equals(kev[0]))
546         {
547           return kev[1];
548         }
549       }
550     } catch (MalformedURLException e)
551     {
552       Console.warn("Could not obtain next page 'cursor' value from 'u");
553     }
554     return null;
555   }
556 }