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