Merge commit 'alpha/update_2_12_for_2_11_2_series_merge^2' into HEAD
[jalview.git] / src / jalview / fts / service / pdb / PDBFTSRestClient.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 package jalview.fts.service.pdb;
22
23 import java.io.File;
24 import java.io.FileInputStream;
25 import java.io.FileReader;
26 import java.net.URI;
27 import java.nio.CharBuffer;
28 import java.util.ArrayList;
29 import java.util.Collection;
30 import java.util.Iterator;
31 import java.util.List;
32 import java.util.Map;
33 import java.util.Objects;
34
35 import javax.ws.rs.core.MediaType;
36
37 import org.json.simple.parser.ParseException;
38
39 import com.sun.jersey.api.client.Client;
40 import com.sun.jersey.api.client.ClientResponse;
41 import com.sun.jersey.api.client.WebResource;
42 import com.sun.jersey.api.client.config.DefaultClientConfig;
43
44 import jalview.bin.ApplicationSingletonProvider;
45 import jalview.bin.ApplicationSingletonProvider.ApplicationSingletonI;
46 import jalview.datamodel.SequenceI;
47 import jalview.fts.api.FTSData;
48 import jalview.fts.api.FTSDataColumnI;
49 import jalview.fts.api.FTSRestClientI;
50 import jalview.fts.api.StructureFTSRestClientI;
51 import jalview.fts.core.FTSDataColumnPreferences;
52 import jalview.fts.core.FTSDataColumnPreferences.PreferenceSource;
53 import jalview.fts.core.FTSRestClient;
54 import jalview.fts.core.FTSRestRequest;
55 import jalview.fts.core.FTSRestResponse;
56 import jalview.fts.service.alphafold.AlphafoldRestClient;
57 import jalview.util.JSONUtils;
58 import jalview.util.MessageManager;
59 import jalview.util.Platform;
60
61 /**
62  * A rest client for querying the Search endpoint of the PDB API
63  * 
64  * @author tcnofoegbu
65  */
66 public class PDBFTSRestClient extends FTSRestClient
67         implements StructureFTSRestClientI,ApplicationSingletonI
68 {
69
70   private static FTSRestClientI instance = null;
71
72   public static final String PDB_SEARCH_ENDPOINT = "https://www.ebi.ac.uk/pdbe/search/pdb/select?";
73
74   public static FTSRestClientI getInstance()
75   {
76     return (FTSRestClientI) ApplicationSingletonProvider
77             .getInstance(PDBFTSRestClient.class);
78   }
79   protected PDBFTSRestClient()
80   {
81   }
82
83   /**
84    * Takes a PDBRestRequest object and returns a response upon execution
85    * 
86    * @param pdbRestRequest
87    *          the PDBRestRequest instance to be processed
88    * @return the pdbResponse object for the given request
89    * @throws Exception
90    */
91   @SuppressWarnings({ "unused", "unchecked" })
92   @Override
93   public FTSRestResponse executeRequest(FTSRestRequest pdbRestRequest)
94           throws Exception
95   {
96     try
97     {
98       String wantedFields = getDataColumnsFieldsAsCommaDelimitedString(
99               pdbRestRequest.getWantedFields());
100       int responseSize = (pdbRestRequest.getResponseSize() == 0)
101               ? getDefaultResponsePageSize()
102               : pdbRestRequest.getResponseSize();
103       int offSet = pdbRestRequest.getOffSet();
104       String sortParam = null;
105       if (pdbRestRequest.getFieldToSortBy() == null
106               || pdbRestRequest.getFieldToSortBy().trim().isEmpty())
107       {
108         sortParam = "";
109       }
110       else
111       {
112         if (pdbRestRequest.getFieldToSortBy()
113                 .equalsIgnoreCase("Resolution"))
114         {
115           sortParam = pdbRestRequest.getFieldToSortBy()
116                   + (pdbRestRequest.isAscending() ? " asc" : " desc");
117         }
118         else
119         {
120           sortParam = pdbRestRequest.getFieldToSortBy()
121                   + (pdbRestRequest.isAscending() ? " desc" : " asc");
122         }
123       }
124
125       String facetPivot = (pdbRestRequest.getFacetPivot() == null
126               || pdbRestRequest.getFacetPivot().isEmpty()) ? ""
127                       : pdbRestRequest.getFacetPivot();
128       String facetPivotMinCount = String
129               .valueOf(pdbRestRequest.getFacetPivotMinCount());
130
131       String query = pdbRestRequest.getFieldToSearchBy()
132               + pdbRestRequest.getSearchTerm()
133               + (pdbRestRequest.isAllowEmptySeq() ? ""
134                       : " AND molecule_sequence:['' TO *]")
135               + (pdbRestRequest.isAllowUnpublishedEntries() ? ""
136                       : " AND status:REL");
137
138       // Build request parameters for the REST Request
139
140       // BH 2018 the trick here is to coerce the classes in Javascript to be
141       // different from the ones in Java yet still allow this to be correct for
142       // Java
143       Client client;
144       Class<ClientResponse> clientResponseClass;
145       if (Platform.isJS())
146       {
147         // JavaScript only -- coerce types to Java types for Java
148         client = (Client) (Object) new jalview.javascript.web.Client();
149         clientResponseClass = (Class<ClientResponse>) (Object) jalview.javascript.web.ClientResponse.class;
150       }
151       else
152       /**
153        * Java only
154        * 
155        * @j2sIgnore
156        */
157       {
158         client = Client.create(new DefaultClientConfig());
159         clientResponseClass = ClientResponse.class;
160       }
161
162       WebResource webResource;
163       if (pdbRestRequest.isFacet())
164       {
165         webResource = client.resource(PDB_SEARCH_ENDPOINT)
166                 .queryParam("wt", "json").queryParam("fl", wantedFields)
167                 .queryParam("rows", String.valueOf(responseSize))
168                 .queryParam("q", query)
169                 .queryParam("start", String.valueOf(offSet))
170                 .queryParam("sort", sortParam).queryParam("facet", "true")
171                 .queryParam("facet.pivot", facetPivot)
172                 .queryParam("facet.pivot.mincount", facetPivotMinCount);
173       }
174       else
175       {
176         webResource = client.resource(PDB_SEARCH_ENDPOINT)
177                 .queryParam("wt", "json").queryParam("fl", wantedFields)
178                 .queryParam("rows", String.valueOf(responseSize))
179                 .queryParam("start", String.valueOf(offSet))
180                 .queryParam("q", query).queryParam("sort", sortParam);
181       }
182
183       URI uri = webResource.getURI();
184
185       System.out.println(uri);
186       ClientResponse clientResponse = null;
187       int responseStatus = -1;
188
189       // Get the JSON string from the response object or directly from the
190       // client (JavaScript)
191       Map<String, Object> jsonObj = null;
192       String responseString = null;
193
194       System.out.println("query >>>>>>> " + pdbRestRequest.toString());
195
196       if (!isMocked())
197       {
198         // Execute the REST request
199         clientResponse = webResource.accept(MediaType.APPLICATION_JSON)
200                 .get(clientResponseClass);
201         responseStatus = clientResponse.getStatus();
202       }
203       else
204       {
205         // mock response
206         if (mockQueries.containsKey(uri.toString()))
207         {
208           responseStatus = 200;
209         }
210         else
211         {
212           // FIXME - may cause unexpected exceptions for callers when mocked
213           responseStatus = 400;
214         }
215       }
216
217       // Check the response status and report exception if one occurs
218       switch (responseStatus)
219       {
220       case 200:
221         if (isMocked())
222         {
223           responseString = mockQueries.get(uri.toString());
224         }
225         else
226         {
227           if (Platform.isJS())
228           {
229             jsonObj = clientResponse.getEntity(Map.class);
230           }
231           else
232           {
233             responseString = clientResponse.getEntity(String.class);
234           }
235         }
236         break;
237       case 400:
238         throw new Exception(isMocked() ? "400 response (Mocked)"
239                 : parseJsonExceptionString(responseString));
240       default:
241         throw new Exception(
242                 getMessageByHTTPStatusCode(responseStatus, "PDB"));
243       }
244
245       // Process the response and return the result to the caller.
246       return parsePDBJsonResponse(responseString, jsonObj, pdbRestRequest);
247     } catch (Exception e)
248     {
249       if (e.getMessage() == null)
250       {
251         throw (e);
252       }
253       String exceptionMsg = e.getMessage();
254       if (exceptionMsg.contains("SocketException"))
255       {
256         // No internet connection
257         throw new Exception(MessageManager.getString(
258                 "exception.unable_to_detect_internet_connection"));
259       }
260       else if (exceptionMsg.contains("UnknownHostException"))
261       {
262         // The server 'www.ebi.ac.uk' is unreachable
263         throw new Exception(MessageManager.formatMessage(
264                 "exception.fts_server_unreachable", "PDB Solr"));
265       }
266       else
267       {
268         throw e;
269       }
270     }
271   }
272
273   /**
274    * Process error response from PDB server if/when one occurs.
275    * 
276    * @param jsonResponse
277    *          the JSON string containing error message from the server
278    * @return the processed error message from the JSON string
279    */
280   @SuppressWarnings("unchecked")
281   public static String parseJsonExceptionString(String jsonErrorResponse)
282   {
283     StringBuilder errorMessage = new StringBuilder(
284             "\n============= PDB Rest Client RunTime error =============\n");
285
286     // {
287     // "responseHeader":{
288     // "status":0,
289     // "QTime":0,
290     // "params":{
291     // "q":"(text:q93xj9_soltu) AND molecule_sequence:['' TO *] AND status:REL",
292     // "fl":"pdb_id,title,experimental_method,resolution",
293     // "start":"0",
294     // "sort":"overall_quality desc",
295     // "rows":"500",
296     // "wt":"json"}},
297     // "response":{"numFound":1,"start":0,"docs":[
298     // {
299     // "experimental_method":["X-ray diffraction"],
300     // "pdb_id":"4zhp",
301     // "resolution":2.46,
302     // "title":"The crystal structure of Potato ferredoxin I with 2Fe-2S
303     // cluster"}]
304     // }}
305     //
306     try
307     {
308       Map<String, Object> jsonObj = (Map<String, Object>) JSONUtils
309               .parse(jsonErrorResponse);
310       Map<String, Object> errorResponse = (Map<String, Object>) jsonObj
311               .get("error");
312
313       Map<String, Object> responseHeader = (Map<String, Object>) jsonObj
314               .get("responseHeader");
315       Map<String, Object> paramsObj = (Map<String, Object>) responseHeader
316               .get("params");
317       String status = responseHeader.get("status").toString();
318       String message = errorResponse.get("msg").toString();
319       String query = paramsObj.get("q").toString();
320       String fl = paramsObj.get("fl").toString();
321
322       errorMessage.append("Status: ").append(status).append("\n");
323       errorMessage.append("Message: ").append(message).append("\n");
324       errorMessage.append("query: ").append(query).append("\n");
325       errorMessage.append("fl: ").append(fl).append("\n");
326
327     } catch (ParseException e)
328     {
329       e.printStackTrace();
330     }
331     return errorMessage.toString();
332   }
333
334   /**
335    * Parses the JSON response string from PDB REST API. The response is dynamic
336    * hence, only fields specifically requested for in the 'wantedFields'
337    * parameter is fetched/processed
338    * 
339    * @param pdbJsonResponseString
340    *          the JSON string to be parsed
341    * @param pdbRestRequest
342    *          the request object which contains parameters used to process the
343    *          JSON string
344    * @return
345    */
346   public static FTSRestResponse parsePDBJsonResponse(
347           String pdbJsonResponseString, FTSRestRequest pdbRestRequest)
348   {
349     return parsePDBJsonResponse(pdbJsonResponseString,
350             (Map<String, Object>) null, pdbRestRequest);
351   }
352
353   @SuppressWarnings("unchecked")
354   public static FTSRestResponse parsePDBJsonResponse(
355           String pdbJsonResponseString, Map<String, Object> jsonObj,
356           FTSRestRequest pdbRestRequest)
357   {
358     FTSRestResponse searchResult = new FTSRestResponse();
359     List<FTSData> result = null;
360     try
361     {
362       if (jsonObj == null)
363       {
364         jsonObj = (Map<String, Object>) JSONUtils
365                 .parse(pdbJsonResponseString);
366       }
367       Map<String, Object> pdbResponse = (Map<String, Object>) jsonObj
368               .get("response");
369       String queryTime = ((Map<String, Object>) jsonObj
370               .get("responseHeader")).get("QTime").toString();
371       int numFound = Integer
372               .valueOf(pdbResponse.get("numFound").toString());
373       List<Object> docs = (List<Object>) pdbResponse.get("docs");
374
375       result = new ArrayList<FTSData>();
376       if (numFound > 0)
377       {
378         for (Iterator<Object> docIter = docs.iterator(); docIter.hasNext();)
379         {
380           Map<String, Object> doc = (Map<String, Object>) docIter.next();
381           result.add(getFTSData(doc, pdbRestRequest));
382         }
383       }
384       // this is the total number found by the query,
385       // rather than the set returned in SearchSummary
386       searchResult.setNumberOfItemsFound(numFound);
387       searchResult.setResponseTime(queryTime);
388       searchResult.setSearchSummary(result);
389     } catch (ParseException e)
390     {
391       e.printStackTrace();
392     }
393     return searchResult;
394   }
395
396   public static FTSData getFTSData(Map<String, Object> pdbJsonDoc,
397           FTSRestRequest request)
398   {
399
400     String primaryKey = null;
401
402     Object[] summaryRowData;
403
404     SequenceI associatedSequence;
405
406     Collection<FTSDataColumnI> diplayFields = request.getWantedFields();
407     SequenceI associatedSeq = request.getAssociatedSequence();
408     int colCounter = 0;
409     summaryRowData = new Object[(associatedSeq != null)
410             ? diplayFields.size() + 1
411             : diplayFields.size()];
412     if (associatedSeq != null)
413     {
414       associatedSequence = associatedSeq;
415       summaryRowData[0] = associatedSequence;
416       colCounter = 1;
417     }
418
419     for (FTSDataColumnI field : diplayFields)
420     {
421       // System.out.println("Field " + field);
422       String fieldData = (pdbJsonDoc.get(field.getCode()) == null) ? ""
423               : pdbJsonDoc.get(field.getCode()).toString();
424       // System.out.println("Field Data : " + fieldData);
425       if (field.isPrimaryKeyColumn())
426       {
427         primaryKey = fieldData;
428         summaryRowData[colCounter++] = primaryKey;
429       }
430       else if (fieldData == null || fieldData.isEmpty())
431       {
432         summaryRowData[colCounter++] = null;
433       }
434       else
435       {
436         try
437         {
438           summaryRowData[colCounter++] = (field.getDataType()
439                   .getDataTypeClass() == Integer.class)
440                           ? Integer.valueOf(fieldData)
441                           : (field.getDataType()
442                                   .getDataTypeClass() == Double.class)
443                                           ? Double.valueOf(fieldData)
444                                           : sanitiseData(fieldData);
445         } catch (Exception e)
446         {
447           e.printStackTrace();
448           System.out.println("offending value:" + fieldData);
449         }
450       }
451     }
452
453     final String primaryKey1 = primaryKey;
454
455     final Object[] summaryRowData1 = summaryRowData;
456     return new FTSData()
457     {
458       @Override
459       public Object[] getSummaryData()
460       {
461         return summaryRowData1;
462       }
463
464       @Override
465       public Object getPrimaryKey()
466       {
467         return primaryKey1;
468       }
469
470       /**
471        * Returns a string representation of this object;
472        */
473       @Override
474       public String toString()
475       {
476         StringBuilder summaryFieldValues = new StringBuilder();
477         for (Object summaryField : summaryRowData1)
478         {
479           summaryFieldValues.append(
480                   summaryField == null ? " " : summaryField.toString())
481                   .append("\t");
482         }
483         return summaryFieldValues.toString();
484       }
485
486       /**
487        * Returns hash code value for this object
488        */
489       @Override
490       public int hashCode()
491       {
492         return Objects.hash(primaryKey1, this.toString());
493       }
494
495       @Override
496       public boolean equals(Object that)
497       {
498         return this.toString().equals(that.toString());
499       }
500     };
501   }
502
503   private static String sanitiseData(String data)
504   {
505     String cleanData = data.replaceAll("\\[\"", "").replaceAll("\\]\"", "")
506             .replaceAll("\\[", "").replaceAll("\\]", "")
507             .replaceAll("\",\"", ", ").replaceAll("\"", "");
508     return cleanData;
509   }
510
511   @Override
512   public String getColumnDataConfigFileName()
513   {
514     return "/fts/pdb_data_columns.txt";
515   }
516
517
518   private Collection<FTSDataColumnI> allDefaultDisplayedStructureDataColumns;
519
520   @Override
521   public Collection<FTSDataColumnI> getAllDefaultDisplayedStructureDataColumns()
522   {
523     if (allDefaultDisplayedStructureDataColumns == null
524             || allDefaultDisplayedStructureDataColumns.isEmpty())
525     {
526       allDefaultDisplayedStructureDataColumns = new ArrayList<>();
527       allDefaultDisplayedStructureDataColumns
528               .addAll(super.getAllDefaultDisplayedFTSDataColumns());
529     }
530     return allDefaultDisplayedStructureDataColumns;
531   }
532   @Override
533   public String[] getPreferencesColumnsFor(PreferenceSource source)
534   {
535     String[] columnNames = null;
536     switch (source)
537     {
538     case SEARCH_SUMMARY:
539       columnNames = new String[] { "", "Display", "Group" };
540       break;
541     case STRUCTURE_CHOOSER:
542       columnNames = new String[] { "", "Display", "Group" };
543       break;
544     case PREFERENCES:
545       columnNames = new String[] { "PDB Field", "Show in search summary",
546           "Show in structure summary" };
547       break;
548     default:
549       break;
550     }
551     return columnNames;
552   }
553 }