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