JAL-4114 model_page_url not required in v2.0 response
[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         if (modelPage != null)
457         {
458           pdbEntry.setProviderPage(modelPage);
459         }
460         selectedSeq.getDatasetSequence().addPDBId(pdbEntry);
461       }
462       pdbEntriesToView[count++] = pdbEntry;
463     }
464     return pdbEntriesToView;
465   }
466
467   @Override
468   protected FTSRestRequest getLastFTSRequest()
469   {
470     return lastTdbRequest;
471   }
472
473   /**
474    * generate a query for PDBFTS to retrieve structure metadata
475    * 
476    * @param ftsRestRequest
477    * @param upResponse
478    * @return
479    */
480
481   public List<String> buildPDBFTSQueryFor(FTSRestResponse upResponse)
482   {
483     List<String> ftsQueries = new ArrayList<String>();
484     Set<String> pdbIds = new HashSet<String>();
485     int idx_modelId = getLastFTSRequest().getFieldIndex("Model id");
486     int idx_provider = getLastFTSRequest().getFieldIndex("Provider");
487     for (FTSData row : upResponse.getSearchSummary())
488     {
489       String id = (String) row.getSummaryData()[idx_modelId];
490       String provider = (String) row.getSummaryData()[idx_provider];
491       if ("PDBe".equalsIgnoreCase(provider))
492       {
493         pdbIds.add(id);
494       }
495     }
496     StringBuilder sb = new StringBuilder();
497     for (String pdbId : pdbIds)
498     {
499       if (sb.length() > 2500)
500       {
501         ftsQueries.add(sb.toString());
502         sb.setLength(0);
503       }
504       if (sb.length() > 0)
505       {
506         sb.append(" OR ");
507       }
508       sb.append(pdbId);
509     }
510     if (sb.length() > 0)
511     {
512       ftsQueries.add(sb.toString());
513     }
514     return ftsQueries;
515   }
516
517   /**
518    * query PDBe for structure metadata
519    * 
520    * @param pdbquery
521    * @param upResponse
522    * @return FTSRestResponse via PDBStructureChooserQuerySource
523    */
524   public List<FTSRestResponse> fetchStructuresMetaDataFor(
525           PDBStructureChooserQuerySource pdbquery,
526           FTSRestResponse upResponse) throws Exception
527   {
528     List<String> pdb_Queries = buildPDBFTSQueryFor(upResponse);
529     if (pdb_Queries.size() == 0)
530     {
531       return null;
532     }
533     List<FTSRestResponse> results = new ArrayList<FTSRestResponse>();
534
535     for (String pdb_Query : pdb_Queries)
536     {
537       FTSRestResponse resultList;
538       FTSRestRequest pdbRequest = new FTSRestRequest();
539       pdbRequest.setAllowEmptySeq(false);
540       pdbRequest.setResponseSize(500);
541       pdbRequest.setFieldToSearchBy("(");
542       // pdbRequest.setFieldToSortBy("pdb_id");
543       pdbRequest.setWantedFields(
544               pdbquery.getDocFieldPrefs().getStructureSummaryFields());
545       pdbRequest.setSearchTerm(pdb_Query + ")");
546
547       // handle exceptions like server errors here - means the threedbeacons
548       // discovery isn't broken by issues to do with the PDBe SOLR api
549       try
550       {
551         resultList = pdbquery.executePDBFTSRestRequest(pdbRequest);
552         if (resultList.getNumberOfItemsFound() == 0)
553         {
554           Console.info("Unexpectedly returned no results for pdbe query: "
555                   + pdb_Query);
556         }
557         results.add(resultList);
558         lastPdbRequest = pdbRequest;
559       } catch (Exception ex)
560       {
561         Console.error("PDBFTSQuery failed", ex);
562       }
563
564     }
565
566     return results;
567   }
568
569   public FTSRestResponse joinResponses(FTSRestResponse upResponse,
570           List<FTSRestResponse> pdbResponses)
571   {
572     boolean hasPdbResp = lastPdbRequest != null;
573
574     int idx_provider = getLastFTSRequest().getFieldIndex("Provider");
575     // join on
576     int idx_modelId = getLastFTSRequest().getFieldIndex("Model id");
577     int pdbIdx = hasPdbResp ? lastPdbRequest.getFieldIndex("PDB Id") : -1;
578     int pdbTitle_idx = hasPdbResp ? lastPdbRequest.getFieldIndex("Title")
579             : -1;
580     int tdbTitle_idx = getLastFTSRequest().getFieldIndex("Title");
581
582     for (final FTSData row : upResponse.getSearchSummary())
583     {
584       String id = (String) row.getSummaryData()[idx_modelId];
585       String provider = (String) row.getSummaryData()[idx_provider];
586       if ("PDBe".equalsIgnoreCase(provider))
587       {
588         if (!hasPdbResp)
589         {
590           System.out.println(
591                   "Warning: seems like we couldn't get to the PDBe search interface.");
592         }
593         else
594         {
595           for (final FTSRestResponse pdbResponse : pdbResponses)
596           {
597             for (final FTSData pdbrow : pdbResponse.getSearchSummary())
598             {
599               String pdbid = (String) pdbrow.getSummaryData()[pdbIdx];
600               if (id.equalsIgnoreCase(pdbid))
601               {
602                 row.getSummaryData()[tdbTitle_idx] = pdbrow
603                         .getSummaryData()[pdbTitle_idx];
604               }
605             }
606           }
607         }
608
609       }
610       else
611       {
612         row.getSummaryData()[tdbTitle_idx] = "Model from TDB";
613       }
614     }
615     return upResponse;
616   }
617
618   public TDB_FTSData getFTSDataFor(JTable restable, int selectedRow,
619           Collection<FTSData> discoveredStructuresSet)
620   {
621     int idColumnIndex = restable.getColumn("Model id").getModelIndex();
622
623     String modelId = (String) restable.getValueAt(selectedRow,
624             idColumnIndex);
625     for (FTSData row : discoveredStructuresSet)
626     {
627       if (row instanceof TDB_FTSData
628               && ((TDB_FTSData) row).getModelId().equals(modelId))
629       {
630         return ((TDB_FTSData) row);
631       }
632     }
633     return null;
634   }
635
636 }