JAL-4311 fix test checking pdbentry gets quality score metadata from 3dbeacons
[jalview.git] / src / jalview / gui / structurechooser / ThreeDBStructureChooserQuerySource.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.gui.structurechooser;
22
23 import java.util.ArrayList;
24 import java.util.Arrays;
25 import java.util.Collection;
26 import java.util.Comparator;
27 import java.util.HashSet;
28 import java.util.LinkedHashSet;
29 import java.util.List;
30 import java.util.Locale;
31 import java.util.Set;
32
33 import javax.swing.JTable;
34
35 import jalview.bin.Console;
36 import jalview.datamodel.DBRefEntry;
37 import jalview.datamodel.DBRefSource;
38 import jalview.datamodel.PDBEntry;
39 import jalview.datamodel.SequenceI;
40 import jalview.fts.api.FTSData;
41 import jalview.fts.api.FTSDataColumnI;
42 import jalview.fts.api.FTSRestClientI;
43 import jalview.fts.core.FTSDataColumnPreferences;
44 import jalview.fts.core.FTSDataColumnPreferences.PreferenceSource;
45 import jalview.fts.core.FTSRestRequest;
46 import jalview.fts.core.FTSRestResponse;
47 import jalview.fts.service.threedbeacons.TDB_FTSData;
48 import jalview.fts.service.threedbeacons.TDBeaconsFTSRestClient;
49 import jalview.jbgui.FilterOption;
50
51 /**
52  * logic for querying the 3DBeacons API for structures of sequences
53  * 
54  * @author jprocter
55  */
56 public class ThreeDBStructureChooserQuerySource
57         extends StructureChooserQuerySource
58 {
59
60   private Set<String> tdBeaconsFilters = null, defaultFilters = null;
61
62   public static final String FILTER_TDBEACONS_COVERAGE = "3d_beacons_coverage";
63
64   public static final String FILTER_FIRST_BEST_COVERAGE = "3d_beacons_first_best_coverage";
65
66   private static final String FILTER_SOURCE_PREFIX = "only_";
67
68   protected FTSRestRequest lastTdbRequest;
69
70   protected FTSRestClientI tdbRestClient;
71
72   private FTSRestRequest lastPdbRequest;
73
74   public ThreeDBStructureChooserQuerySource()
75   {
76     defaultFilters = new LinkedHashSet<String>();
77     defaultFilters.add(FILTER_TDBEACONS_COVERAGE);
78     defaultFilters.add(FILTER_FIRST_BEST_COVERAGE);
79
80     tdbRestClient = TDBeaconsFTSRestClient.getInstance();
81     docFieldPrefs = new FTSDataColumnPreferences(
82             PreferenceSource.STRUCTURE_CHOOSER,
83             TDBeaconsFTSRestClient.getInstance());
84   }
85
86   /**
87    * Builds a query string for a given sequences using its DBRef entries 3d
88    * Beacons is only useful for uniprot IDs
89    * 
90    * @param seq
91    *          the sequences to build a query for
92    * @return the built query string
93    */
94
95   @Override
96   public String buildQuery(SequenceI seq)
97   {
98     List<DBRefEntry> refs = seq.getDBRefs();
99     int ib = checkUniprotRefs(refs);
100     if (ib > -1)
101     {
102       return getDBRefId(refs.get(ib));
103     }
104     return null;
105   }
106
107   /**
108    * Searches DBRefEntry for uniprot refs
109    * 
110    * @param seq
111    * @return -2 if no uniprot refs, -1 if no canonical ref., otherwise index of
112    *         Uniprot canonical DBRefEntry
113    */
114   public static int checkUniprotRefs(List<DBRefEntry> refs)
115   {
116     boolean hasUniprot = false;
117     if (refs != null && refs.size() != 0)
118     {
119       for (int ib = 0, nb = refs.size(); ib < nb; ib++)
120       {
121         DBRefEntry dbRef = refs.get(ib);
122         if (dbRef.getSource().equalsIgnoreCase(DBRefSource.UNIPROT))
123         {
124           hasUniprot = true;
125           if (dbRef.isCanonical())
126           {
127             return ib;
128           }
129         }
130       }
131     }
132     return hasUniprot ? -1 : -2;
133   }
134
135   /**
136    * Ensures sequence ref names are not less than 3 characters and does not
137    * contain a database name
138    * 
139    * @param seqName
140    * @return
141    */
142   static boolean isValidSeqName(String seqName)
143   {
144     String ignoreList = "pdb,uniprot,swiss-prot";
145     if (seqName.length() < 3)
146     {
147       return false;
148     }
149     if (seqName.contains(":"))
150     {
151       return false;
152     }
153     seqName = seqName.toLowerCase(Locale.ROOT);
154     for (String ignoredEntry : ignoreList.split(","))
155     {
156       if (seqName.contains(ignoredEntry))
157       {
158         return false;
159       }
160     }
161     return true;
162   }
163
164   static String getDBRefId(DBRefEntry dbRef)
165   {
166     String ref = dbRef.getAccessionId().replaceAll("GO:", "");
167     return ref;
168   }
169
170   /**
171    * FTSRestClient specific query builder to recover associated structure data
172    * records for a sequence
173    * 
174    * @param seq
175    *          - seq to generate a query for
176    * @param wantedFields
177    *          - fields to retrieve
178    * @param selectedFilterOpt
179    *          - criterion for ranking results (e.g. resolution)
180    * @param b
181    *          - sort ascending or descending
182    * @return
183    * @throws Exception
184    */
185   @Override
186   public FTSRestResponse fetchStructuresMetaData(SequenceI seq,
187           Collection<FTSDataColumnI> wantedFields,
188           FilterOption selectedFilterOpt, boolean b) throws Exception
189   {
190     FTSRestResponse resultList;
191     if (selectedFilterOpt != null
192             && tdBeaconsFilter(selectedFilterOpt.getValue()))
193     {
194       FTSRestRequest tdbRequest = getTDBeaconsRequest(seq, wantedFields);
195       resultList = tdbRestClient.executeRequest(tdbRequest);
196
197       lastTdbRequest = tdbRequest;
198       if (resultList != null)
199       { // Query the PDB and add additional metadata
200         List<FTSRestResponse> pdbResponse = fetchStructuresMetaDataFor(
201                 getPDBQuerySource(), resultList);
202
203         resultList = joinResponses(resultList, pdbResponse);
204       }
205       return resultList;
206     }
207     // use the PDBFTS directly
208     resultList = getPDBQuerySource().fetchStructuresMetaData(seq,
209             wantedFields, selectedFilterOpt, b);
210     lastTdbRequest = getPDBQuerySource().lastPdbRequest;
211     lastPdbRequest = lastTdbRequest; // both queries the same - indicates we
212     // rank using PDBe
213     return resultList;
214
215   }
216
217   PDBStructureChooserQuerySource pdbQuerySource = null;
218
219   private PDBStructureChooserQuerySource getPDBQuerySource()
220   {
221     if (pdbQuerySource == null)
222     {
223       pdbQuerySource = new PDBStructureChooserQuerySource();
224     }
225     return pdbQuerySource;
226   }
227
228   private FTSRestRequest getTDBeaconsRequest(SequenceI seq,
229           Collection<FTSDataColumnI> wantedFields)
230   {
231     FTSRestRequest pdbRequest = new FTSRestRequest();
232     pdbRequest.setAllowEmptySeq(false);
233     pdbRequest.setResponseSize(500);
234     pdbRequest.setWantedFields(wantedFields);
235     String query = buildQuery(seq);
236     if (query == null)
237     {
238       return null;
239     }
240     pdbRequest.setSearchTerm(query + ".json");
241     pdbRequest.setAssociatedSequence(seq);
242     return pdbRequest;
243   }
244
245   @Override
246   public List<FilterOption> getAvailableFilterOptions(String VIEWS_FILTER)
247   {
248     List<FilterOption> filters = getPDBQuerySource()
249             .getAvailableFilterOptions(VIEWS_FILTER);
250     tdBeaconsFilters = new LinkedHashSet<String>();
251     tdBeaconsFilters.addAll(defaultFilters);
252     filters.add(0, new FilterOption("Best 3D-Beacons Coverage",
253             FILTER_FIRST_BEST_COVERAGE, VIEWS_FILTER, false, this));
254     filters.add(1, new FilterOption("Multiple 3D-Beacons Coverage",
255             FILTER_TDBEACONS_COVERAGE, VIEWS_FILTER, true, this));
256
257     return filters;
258   }
259
260   @Override
261   public void updateAvailableFilterOptions(String VIEWS_FILTER,
262           List<FilterOption> xtantOptions, Collection<FTSData> tdbEntries)
263   {
264     if (tdbEntries != null && lastTdbRequest != null)
265     {
266       boolean hasPDBe = false;
267       for (FTSData _row : tdbEntries)
268       {
269         // tdb returns custom object
270         TDB_FTSData row = (TDB_FTSData) _row;
271         String provider = row.getProvider();
272         FilterOption providerOpt = new FilterOption(
273                 "3DB Provider - " + provider,
274                 FILTER_SOURCE_PREFIX + provider, VIEWS_FILTER, false, this);
275         if (!xtantOptions.contains(providerOpt))
276         {
277           xtantOptions.add(1, providerOpt);
278           tdBeaconsFilters.add(FILTER_SOURCE_PREFIX + provider);
279           if ("PDBe".equalsIgnoreCase(provider))
280           {
281             hasPDBe = true;
282           }
283         }
284       }
285       if (!hasPDBe)
286       {
287         // remove the PDBe options from the available filters
288         int op = 0;
289         while (op < xtantOptions.size())
290         {
291           FilterOption filter = xtantOptions.get(op);
292           if (filter
293                   .getQuerySource() instanceof PDBStructureChooserQuerySource)
294           {
295             xtantOptions.remove(op);
296           }
297           else
298           {
299             op++;
300           }
301         }
302       }
303     }
304
305   }
306
307   private boolean tdBeaconsFilter(String fieldToFilterBy)
308   {
309     return tdBeaconsFilters != null
310             && tdBeaconsFilters.contains(fieldToFilterBy);
311   }
312
313   protected String remove_prefix(String fieldToFilterBy)
314   {
315     if (tdBeaconsFilters != null
316             && tdBeaconsFilters.contains(fieldToFilterBy)
317             && !defaultFilters.contains(fieldToFilterBy))
318     {
319       return fieldToFilterBy.substring(FILTER_SOURCE_PREFIX.length());
320     }
321     else
322     {
323       return null;
324     }
325   }
326
327   @Override
328   public boolean needsRefetch(FilterOption selectedFilterOpt)
329   {
330     return selectedFilterOpt == null
331             || !tdBeaconsFilter(selectedFilterOpt.getValue())
332                     && lastPdbRequest != lastTdbRequest;
333   }
334
335   /**
336    * FTSRestClient specific query builder to pick top ranked entry from a
337    * fetchStructuresMetaData query
338    * 
339    * @param seq
340    *          - seq to generate a query for
341    * @param wantedFields
342    *          - fields to retrieve
343    * @param selectedFilterOpt
344    *          - criterion for ranking results (e.g. resolution)
345    * @param b
346    *          - sort ascending or descending
347    * @return
348    * @throws Exception
349    */
350   @Override
351   public FTSRestResponse selectFirstRankedQuery(SequenceI seq,
352           Collection<FTSData> collectedResults,
353           Collection<FTSDataColumnI> wantedFields, String fieldToFilterBy,
354           boolean b) throws Exception
355   {
356     if (fieldToFilterBy != null && tdBeaconsFilter(fieldToFilterBy))
357     {
358       TDBResultAnalyser analyser = new TDBResultAnalyser(seq,
359               collectedResults, lastTdbRequest, fieldToFilterBy,
360               remove_prefix(fieldToFilterBy));
361
362       FTSRestResponse resultList = new FTSRestResponse();
363
364       List<FTSData> filteredResponse = analyser.getFilteredResponse();
365
366       List<FTSData> selectedStructures = analyser
367               .selectStructures(filteredResponse);
368       resultList.setNumberOfItemsFound(selectedStructures.size());
369       resultList.setSearchSummary(selectedStructures);
370       return resultList;
371     }
372     // Fall back to PDBe rankings
373     return getPDBQuerySource().selectFirstRankedQuery(seq, collectedResults,
374             wantedFields, fieldToFilterBy, b);
375   }
376
377   @Override
378   public PDBEntry[] collectSelectedRows(JTable restable, int[] selectedRows,
379           List<SequenceI> selectedSeqsToView)
380   {
381     int refSeqColIndex = restable.getColumn("Ref Sequence").getModelIndex();
382
383     PDBEntry[] pdbEntriesToView = new PDBEntry[selectedRows.length];
384     int count = 0;
385     int idColumnIndex = restable.getColumn("Model id").getModelIndex();
386     int urlColumnIndex = restable.getColumn("Url").getModelIndex();
387     int typeColumnIndex = restable.getColumn("Provider").getModelIndex();
388     int humanUrl = restable.getColumn("Page URL").getModelIndex();
389     int modelformat = restable.getColumn("Model Format").getModelIndex();
390     int tftype = restable.getColumn("Confidence Score Type").getModelIndex();
391     int tftypev = restable.getColumn("Confidence Score Version").getModelIndex();
392     
393     final int up_start_idx = restable.getColumn("Uniprot Start")
394             .getModelIndex();
395     final int up_end_idx = restable.getColumn("Uniprot End")
396             .getModelIndex();
397     int i = 0;
398
399     // bleugh!
400     Integer[] sellist = new Integer[selectedRows.length];
401     for (Integer row : selectedRows)
402     {
403       sellist[i++] = row;
404     }
405     // Sort rows by coverage
406     Arrays.sort(sellist, new Comparator<Integer>()
407     {
408       @Override
409       public int compare(Integer o1, Integer o2)
410       {
411         int o1_xt = ((Integer) restable.getValueAt(o1, up_end_idx))
412                 - (Integer) restable.getValueAt(o1, up_start_idx);
413         int o2_xt = ((Integer) restable.getValueAt(o2, up_end_idx))
414                 - (Integer) restable.getValueAt(o2, up_start_idx);
415         return o2_xt - o1_xt;
416       }
417     });
418
419     for (int row : sellist)
420     {
421       // unique id - could be a horrible hash
422
423       String pdbIdStr = restable.getValueAt(row, idColumnIndex).toString();
424       String urlStr = restable.getValueAt(row, urlColumnIndex).toString();
425       String typeColumn = restable.getValueAt(row, typeColumnIndex)
426               .toString();
427       String modelPage = humanUrl < 1 ? null
428               : (String) restable.getValueAt(row, humanUrl);
429       String strucFormat = restable.getValueAt(row, modelformat).toString();
430       String strucTfType = tftype<1 ? null : restable.getValueAt(row, tftype)==null ? null : restable.getValueAt(row, tftype).toString();
431       String strucTfType_v = tftypev<1 ? null : restable.getValueAt(row, tftypev)==null ? null : restable.getValueAt(row, tftypev).toString();
432       SequenceI selectedSeq = (SequenceI) restable.getValueAt(row,
433               refSeqColIndex);
434       selectedSeqsToView.add(selectedSeq);
435       PDBEntry pdbEntry = selectedSeq.getPDBEntry(pdbIdStr);
436       if (pdbEntry == null)
437       {
438         pdbEntry = getFindEntry(pdbIdStr, selectedSeq.getAllPDBEntries());
439       }
440
441       if (pdbEntry == null)
442       {
443         pdbEntry = new PDBEntry();
444         pdbEntry.setId(pdbIdStr);
445         pdbEntry.setAuthoritative(true);
446         try
447         {
448           pdbEntry.setType(PDBEntry.Type.valueOf(strucFormat));
449         } catch (Exception q)
450         {
451           Console.warn("Unknown filetype for 3D Beacons Model from: "
452                   + strucFormat + " - " + pdbIdStr + " - " + modelPage);
453         }
454
455         if (!"PDBe".equalsIgnoreCase(typeColumn))
456         {
457           pdbEntry.setRetrievalUrl(urlStr);
458         }
459         pdbEntry.setProvider(typeColumn);
460         if (modelPage != null)
461         {
462           pdbEntry.setProviderPage(modelPage);
463         }
464         try
465         {
466           if (strucTfType!=null && !"".equals(strucTfType)) {
467             pdbEntry.setTempFacType(strucTfType);
468           }
469           if (strucTfType_v!=null && !"".equals(strucTfType_v)) {
470             pdbEntry.setTempFacTypeVersion(strucTfType_v);
471           }
472         } catch (Exception q)
473         {
474           Console.warn("Unknown filetype for 3D Beacons Model from: "
475                   + strucFormat + " - " + pdbIdStr + " - " + modelPage);
476         }
477
478         selectedSeq.getDatasetSequence().addPDBId(pdbEntry);
479       }
480       pdbEntriesToView[count++] = pdbEntry;
481     }
482     return pdbEntriesToView;
483   }
484
485   @Override
486   protected FTSRestRequest getLastFTSRequest()
487   {
488     return lastTdbRequest;
489   }
490
491   /**
492    * generate a query for PDBFTS to retrieve structure metadata
493    * 
494    * @param ftsRestRequest
495    * @param upResponse
496    * @return
497    */
498
499   public List<String> buildPDBFTSQueryFor(FTSRestResponse upResponse)
500   {
501     List<String> ftsQueries = new ArrayList<String>();
502     Set<String> pdbIds = new HashSet<String>();
503     int idx_modelId = getLastFTSRequest().getFieldIndex("Model id");
504     int idx_provider = getLastFTSRequest().getFieldIndex("Provider");
505     for (FTSData row : upResponse.getSearchSummary())
506     {
507       String id = (String) row.getSummaryData()[idx_modelId];
508       String provider = (String) row.getSummaryData()[idx_provider];
509       if ("PDBe".equalsIgnoreCase(provider))
510       {
511         pdbIds.add(id);
512       }
513     }
514     StringBuilder sb = new StringBuilder();
515     for (String pdbId : pdbIds)
516     {
517       if (sb.length() > 2500)
518       {
519         ftsQueries.add(sb.toString());
520         sb.setLength(0);
521       }
522       if (sb.length() > 0)
523       {
524         sb.append(" OR ");
525       }
526       sb.append(pdbId);
527     }
528     if (sb.length() > 0)
529     {
530       ftsQueries.add(sb.toString());
531     }
532     return ftsQueries;
533   }
534
535   /**
536    * query PDBe for structure metadata
537    * 
538    * @param pdbquery
539    * @param upResponse
540    * @return FTSRestResponse via PDBStructureChooserQuerySource
541    */
542   public List<FTSRestResponse> fetchStructuresMetaDataFor(
543           PDBStructureChooserQuerySource pdbquery,
544           FTSRestResponse upResponse) throws Exception
545   {
546     List<String> pdb_Queries = buildPDBFTSQueryFor(upResponse);
547     if (pdb_Queries.size() == 0)
548     {
549       return null;
550     }
551     List<FTSRestResponse> results = new ArrayList<FTSRestResponse>();
552
553     for (String pdb_Query : pdb_Queries)
554     {
555       FTSRestResponse resultList;
556       FTSRestRequest pdbRequest = new FTSRestRequest();
557       pdbRequest.setAllowEmptySeq(false);
558       pdbRequest.setResponseSize(500);
559       pdbRequest.setFieldToSearchBy("(");
560       // pdbRequest.setFieldToSortBy("pdb_id");
561       pdbRequest.setWantedFields(
562               pdbquery.getDocFieldPrefs().getStructureSummaryFields());
563       pdbRequest.setSearchTerm(pdb_Query + ")");
564
565       // handle exceptions like server errors here - means the threedbeacons
566       // discovery isn't broken by issues to do with the PDBe SOLR api
567       try
568       {
569         resultList = pdbquery.executePDBFTSRestRequest(pdbRequest);
570         if (resultList.getNumberOfItemsFound() == 0)
571         {
572           Console.info("Unexpectedly returned no results for pdbe query: "
573                   + pdb_Query);
574         }
575         results.add(resultList);
576         lastPdbRequest = pdbRequest;
577       } catch (Exception ex)
578       {
579         Console.error("PDBFTSQuery failed", ex);
580       }
581
582     }
583
584     return results;
585   }
586
587   public FTSRestResponse joinResponses(FTSRestResponse upResponse,
588           List<FTSRestResponse> pdbResponses)
589   {
590     boolean hasPdbResp = lastPdbRequest != null;
591
592     int idx_provider = getLastFTSRequest().getFieldIndex("Provider");
593     // join on
594     int idx_modelId = getLastFTSRequest().getFieldIndex("Model id");
595     int pdbIdx = hasPdbResp ? lastPdbRequest.getFieldIndex("PDB Id") : -1;
596     int pdbTitle_idx = hasPdbResp ? lastPdbRequest.getFieldIndex("Title")
597             : -1;
598     int tdbTitle_idx = getLastFTSRequest().getFieldIndex("Title");
599
600     for (final FTSData row : upResponse.getSearchSummary())
601     {
602       String id = (String) row.getSummaryData()[idx_modelId];
603       String provider = (String) row.getSummaryData()[idx_provider];
604       if ("PDBe".equalsIgnoreCase(provider))
605       {
606         if (!hasPdbResp)
607         {
608           jalview.bin.Console.outPrintln(
609                   "Warning: seems like we couldn't get to the PDBe search interface.");
610         }
611         else
612         {
613           for (final FTSRestResponse pdbResponse : pdbResponses)
614           {
615             for (final FTSData pdbrow : pdbResponse.getSearchSummary())
616             {
617               String pdbid = (String) pdbrow.getSummaryData()[pdbIdx];
618               if (id.equalsIgnoreCase(pdbid))
619               {
620                 row.getSummaryData()[tdbTitle_idx] = pdbrow
621                         .getSummaryData()[pdbTitle_idx];
622               }
623             }
624           }
625         }
626
627       }
628       else
629       {
630         row.getSummaryData()[tdbTitle_idx] = "Model from TDB";
631       }
632     }
633     return upResponse;
634   }
635
636   public TDB_FTSData getFTSDataFor(JTable restable, int selectedRow,
637           Collection<FTSData> discoveredStructuresSet)
638   {
639     int idColumnIndex = restable.getColumn("Model id").getModelIndex();
640
641     String modelId = (String) restable.getValueAt(selectedRow,
642             idColumnIndex);
643     for (FTSData row : discoveredStructuresSet)
644     {
645       if (row instanceof TDB_FTSData
646               && ((TDB_FTSData) row).getModelId().equals(modelId))
647       {
648         return ((TDB_FTSData) row);
649       }
650     }
651     return null;
652   }
653
654 }