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