28c41e91c28d00851be7f41f05039cefd98d7ffc
[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   private 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     final int up_start_idx = restable.getColumn("Uniprot Start")
391             .getModelIndex();
392     final int up_end_idx = restable.getColumn("Uniprot End")
393             .getModelIndex();
394     int i = 0;
395
396     // bleugh!
397     Integer[] sellist = new Integer[selectedRows.length];
398     for (Integer row : selectedRows)
399     {
400       sellist[i++] = row;
401     }
402     // Sort rows by coverage
403     Arrays.sort(sellist, new Comparator<Integer>()
404     {
405       @Override
406       public int compare(Integer o1, Integer o2)
407       {
408         int o1_xt = ((Integer) restable.getValueAt(o1, up_end_idx))
409                 - (Integer) restable.getValueAt(o1, up_start_idx);
410         int o2_xt = ((Integer) restable.getValueAt(o2, up_end_idx))
411                 - (Integer) restable.getValueAt(o2, up_start_idx);
412         return o2_xt - o1_xt;
413       }
414     });
415
416     for (int row : sellist)
417     {
418       // unique id - could be a horrible hash
419
420       String pdbIdStr = restable.getValueAt(row, idColumnIndex).toString();
421       String urlStr = restable.getValueAt(row, urlColumnIndex).toString();
422       String typeColumn = restable.getValueAt(row, typeColumnIndex)
423               .toString();
424       String modelPage = humanUrl < 1 ? null
425               : (String) restable.getValueAt(row, humanUrl);
426       String strucFormat = restable.getValueAt(row, modelformat).toString();
427
428       SequenceI selectedSeq = (SequenceI) restable.getValueAt(row,
429               refSeqColIndex);
430       selectedSeqsToView.add(selectedSeq);
431       PDBEntry pdbEntry = selectedSeq.getPDBEntry(pdbIdStr);
432       if (pdbEntry == null)
433       {
434         pdbEntry = getFindEntry(pdbIdStr, selectedSeq.getAllPDBEntries());
435       }
436
437       if (pdbEntry == null)
438       {
439         pdbEntry = new PDBEntry();
440         pdbEntry.setId(pdbIdStr);
441         pdbEntry.setAuthoritative(true);
442         try
443         {
444           pdbEntry.setType(PDBEntry.Type.valueOf(strucFormat));
445         } catch (Exception q)
446         {
447           Console.warn("Unknown filetype for 3D Beacons Model from: "
448                   + strucFormat + " - " + pdbIdStr + " - " + modelPage);
449         }
450
451         if (!"PDBe".equalsIgnoreCase(typeColumn))
452         {
453           pdbEntry.setRetrievalUrl(urlStr);
454         }
455         pdbEntry.setProvider(typeColumn);
456         pdbEntry.setProviderPage(modelPage);
457         selectedSeq.getDatasetSequence().addPDBId(pdbEntry);
458       }
459       pdbEntriesToView[count++] = pdbEntry;
460     }
461     return pdbEntriesToView;
462   }
463
464   @Override
465   protected FTSRestRequest getLastFTSRequest()
466   {
467     return lastTdbRequest;
468   }
469
470   /**
471    * generate a query for PDBFTS to retrieve structure metadata
472    * 
473    * @param ftsRestRequest
474    * @param upResponse
475    * @return
476    */
477
478   public List<String> buildPDBFTSQueryFor(FTSRestResponse upResponse)
479   {
480     List<String> ftsQueries = new ArrayList<String>();
481     Set<String> pdbIds = new HashSet<String>();
482     int idx_modelId = getLastFTSRequest().getFieldIndex("Model id");
483     int idx_provider = getLastFTSRequest().getFieldIndex("Provider");
484     for (FTSData row : upResponse.getSearchSummary())
485     {
486       String id = (String) row.getSummaryData()[idx_modelId];
487       String provider = (String) row.getSummaryData()[idx_provider];
488       if ("PDBe".equalsIgnoreCase(provider))
489       {
490         pdbIds.add(id);
491       }
492     }
493     StringBuilder sb = new StringBuilder();
494     for (String pdbId : pdbIds)
495     {
496       if (sb.length() > 2500)
497       {
498         ftsQueries.add(sb.toString());
499         sb.setLength(0);
500       }
501       if (sb.length() > 0)
502       {
503         sb.append(" OR ");
504       }
505       sb.append(pdbId);
506     }
507     if (sb.length() > 0)
508     {
509       ftsQueries.add(sb.toString());
510     }
511     return ftsQueries;
512   }
513
514   /**
515    * query PDBe for structure metadata
516    * 
517    * @param pdbquery
518    * @param upResponse
519    * @return FTSRestResponse via PDBStructureChooserQuerySource
520    */
521   public List<FTSRestResponse> fetchStructuresMetaDataFor(
522           PDBStructureChooserQuerySource pdbquery,
523           FTSRestResponse upResponse) throws Exception
524   {
525     List<String> pdb_Queries = buildPDBFTSQueryFor(upResponse);
526     if (pdb_Queries.size() == 0)
527     {
528       return null;
529     }
530     List<FTSRestResponse> results = new ArrayList<FTSRestResponse>();
531
532     for (String pdb_Query : pdb_Queries)
533     {
534       FTSRestResponse resultList;
535       FTSRestRequest pdbRequest = new FTSRestRequest();
536       pdbRequest.setAllowEmptySeq(false);
537       pdbRequest.setResponseSize(500);
538       pdbRequest.setFieldToSearchBy("(");
539       // pdbRequest.setFieldToSortBy("pdb_id");
540       pdbRequest.setWantedFields(
541               pdbquery.getDocFieldPrefs().getStructureSummaryFields());
542       pdbRequest.setSearchTerm(pdb_Query + ")");
543
544       // handle exceptions like server errors here - means the threedbeacons
545       // discovery isn't broken by issues to do with the PDBe SOLR api
546       try
547       {
548         resultList = pdbquery.executePDBFTSRestRequest(pdbRequest);
549         if (resultList.getNumberOfItemsFound() == 0)
550         {
551           Console.info("Unexpectedly returned no results for pdbe query: "
552                   + pdb_Query);
553         }
554         results.add(resultList);
555         lastPdbRequest = pdbRequest;
556       } catch (Exception ex)
557       {
558         Console.error("PDBFTSQuery failed", ex);
559       }
560
561     }
562
563     return results;
564   }
565
566   public FTSRestResponse joinResponses(FTSRestResponse upResponse,
567           List<FTSRestResponse> pdbResponses)
568   {
569     boolean hasPdbResp = lastPdbRequest != null;
570
571     int idx_provider = getLastFTSRequest().getFieldIndex("Provider");
572     // join on
573     int idx_modelId = getLastFTSRequest().getFieldIndex("Model id");
574     int pdbIdx = hasPdbResp ? lastPdbRequest.getFieldIndex("PDB Id") : -1;
575     int pdbTitle_idx = hasPdbResp ? lastPdbRequest.getFieldIndex("Title")
576             : -1;
577     int tdbTitle_idx = getLastFTSRequest().getFieldIndex("Title");
578
579     for (final FTSData row : upResponse.getSearchSummary())
580     {
581       String id = (String) row.getSummaryData()[idx_modelId];
582       String provider = (String) row.getSummaryData()[idx_provider];
583       if ("PDBe".equalsIgnoreCase(provider))
584       {
585         if (!hasPdbResp)
586         {
587           System.out.println(
588                   "Warning: seems like we couldn't get to the PDBe search interface.");
589         }
590         else
591         {
592           for (final FTSRestResponse pdbResponse : pdbResponses)
593           {
594             for (final FTSData pdbrow : pdbResponse.getSearchSummary())
595             {
596               String pdbid = (String) pdbrow.getSummaryData()[pdbIdx];
597               if (id.equalsIgnoreCase(pdbid))
598               {
599                 row.getSummaryData()[tdbTitle_idx] = pdbrow
600                         .getSummaryData()[pdbTitle_idx];
601               }
602             }
603           }
604         }
605
606       }
607       else
608       {
609         row.getSummaryData()[tdbTitle_idx] = "Model from TDB";
610       }
611     }
612     return upResponse;
613   }
614
615   public TDB_FTSData getFTSDataFor(JTable restable, int selectedRow,
616           Collection<FTSData> discoveredStructuresSet)
617   {
618     int idColumnIndex = restable.getColumn("Model id").getModelIndex();
619
620     String modelId = (String) restable.getValueAt(selectedRow,
621             idColumnIndex);
622     for (FTSData row : discoveredStructuresSet)
623     {
624       if (row instanceof TDB_FTSData
625               && ((TDB_FTSData) row).getModelId().equals(modelId))
626       {
627         return ((TDB_FTSData) row);
628       }
629     }
630     return null;
631   }
632
633 }