JAL-2826 added action performed for hiding collapsed sequences
[jalview.git] / src / jalview / util / DBRefUtils.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.util;
22
23 import jalview.datamodel.DBRefEntry;
24 import jalview.datamodel.DBRefSource;
25 import jalview.datamodel.PDBEntry;
26 import jalview.datamodel.SequenceI;
27
28 import java.util.ArrayList;
29 import java.util.Arrays;
30 import java.util.HashMap;
31 import java.util.HashSet;
32 import java.util.List;
33 import java.util.Map;
34 import java.util.Set;
35
36 import com.stevesoft.pat.Regex;
37
38 /**
39  * Utilities for handling DBRef objects and their collections.
40  */
41 public class DBRefUtils
42 {
43   /*
44    * lookup from lower-case form of a name to its canonical (standardised) form
45    */
46   private static Map<String, String> canonicalSourceNameLookup = new HashMap<>();
47
48   private static Map<String, String> dasCoordinateSystemsLookup = new HashMap<>();
49
50   static
51   {
52     // TODO load these from a resource file?
53     canonicalSourceNameLookup.put("uniprotkb/swiss-prot",
54             DBRefSource.UNIPROT);
55     canonicalSourceNameLookup.put("uniprotkb/trembl", DBRefSource.UNIPROT);
56
57     // Ensembl values for dbname in xref REST service:
58     canonicalSourceNameLookup.put("uniprot/sptrembl", DBRefSource.UNIPROT);
59     canonicalSourceNameLookup.put("uniprot/swissprot", DBRefSource.UNIPROT);
60
61     canonicalSourceNameLookup.put("pdb", DBRefSource.PDB);
62     canonicalSourceNameLookup.put("ensembl", DBRefSource.ENSEMBL);
63     // Ensembl Gn and Tr are for Ensembl genomic and transcript IDs as served
64     // from ENA.
65     canonicalSourceNameLookup.put("ensembl-tr", DBRefSource.ENSEMBL);
66     canonicalSourceNameLookup.put("ensembl-gn", DBRefSource.ENSEMBL);
67
68     canonicalSourceNameLookup.put("pfam", DBRefSource.PFAM);
69
70     // Make sure we have lowercase entries for all canonical string lookups
71     Set<String> keys = canonicalSourceNameLookup.keySet();
72     for (String k : keys)
73     {
74       canonicalSourceNameLookup.put(k.toLowerCase(),
75               canonicalSourceNameLookup.get(k));
76     }
77
78     dasCoordinateSystemsLookup.put("pdbresnum", DBRefSource.PDB);
79     dasCoordinateSystemsLookup.put("uniprot", DBRefSource.UNIPROT);
80     dasCoordinateSystemsLookup.put("embl", DBRefSource.EMBL);
81     // dasCoordinateSystemsLookup.put("embl", DBRefSource.EMBLCDS);
82   }
83
84   /**
85    * Returns those DBRefEntry objects whose source identifier (once converted to
86    * Jalview's canonical form) is in the list of sources to search for. Returns
87    * null if no matches found.
88    * 
89    * @param dbrefs
90    *          DBRefEntry objects to search
91    * @param sources
92    *          array of sources to select
93    * @return
94    */
95   public static DBRefEntry[] selectRefs(DBRefEntry[] dbrefs,
96           String[] sources)
97   {
98     if (dbrefs == null || sources == null)
99     {
100       return dbrefs;
101     }
102     HashSet<String> srcs = new HashSet<>();
103     for (String src : sources)
104     {
105       srcs.add(src.toUpperCase());
106     }
107
108     List<DBRefEntry> res = new ArrayList<>();
109     for (DBRefEntry dbr : dbrefs)
110     {
111       String source = getCanonicalName(dbr.getSource());
112       if (srcs.contains(source.toUpperCase()))
113       {
114         res.add(dbr);
115       }
116     }
117
118     if (res.size() > 0)
119     {
120       DBRefEntry[] reply = new DBRefEntry[res.size()];
121       return res.toArray(reply);
122     }
123     return null;
124   }
125
126   /**
127    * isDasCoordinateSystem
128    * 
129    * @param string
130    *          String
131    * @param dBRefEntry
132    *          DBRefEntry
133    * @return boolean true if Source DBRefEntry is compatible with DAS
134    *         CoordinateSystem name
135    */
136
137   public static boolean isDasCoordinateSystem(String string,
138           DBRefEntry dBRefEntry)
139   {
140     if (string == null || dBRefEntry == null)
141     {
142       return false;
143     }
144     String coordsys = dasCoordinateSystemsLookup.get(string.toLowerCase());
145     return coordsys == null ? false
146             : coordsys.equals(dBRefEntry.getSource());
147   }
148
149   /**
150    * look up source in an internal list of database reference sources and return
151    * the canonical jalview name for the source, or the original string if it has
152    * no canonical form.
153    * 
154    * @param source
155    * @return canonical jalview source (one of jalview.datamodel.DBRefSource.*)
156    *         or original source
157    */
158   public static String getCanonicalName(String source)
159   {
160     if (source == null)
161     {
162       return null;
163     }
164     String canonical = canonicalSourceNameLookup.get(source.toLowerCase());
165     return canonical == null ? source : canonical;
166   }
167
168   /**
169    * Returns a (possibly empty) list of those references that match the given
170    * entry. Currently uses a comparator which matches if
171    * <ul>
172    * <li>database sources are the same</li>
173    * <li>accession ids are the same</li>
174    * <li>both have no mapping, or the mappings are the same</li>
175    * </ul>
176    * 
177    * @param ref
178    *          Set of references to search
179    * @param entry
180    *          pattern to match
181    * @return
182    */
183   public static List<DBRefEntry> searchRefs(DBRefEntry[] ref,
184           DBRefEntry entry)
185   {
186     return searchRefs(ref, entry,
187             matchDbAndIdAndEitherMapOrEquivalentMapList);
188   }
189
190   /**
191    * Returns a list of those references that match the given accession id
192    * <ul>
193    * <li>database sources are the same</li>
194    * <li>accession ids are the same</li>
195    * <li>both have no mapping, or the mappings are the same</li>
196    * </ul>
197    * 
198    * @param refs
199    *          Set of references to search
200    * @param accId
201    *          accession id to match
202    * @return
203    */
204   public static List<DBRefEntry> searchRefs(DBRefEntry[] refs, String accId)
205   {
206     return searchRefs(refs, new DBRefEntry("", "", accId), matchId);
207   }
208
209   /**
210    * Returns a (possibly empty) list of those references that match the given
211    * entry, according to the given comparator.
212    * 
213    * @param refs
214    *          an array of database references to search
215    * @param entry
216    *          an entry to compare against
217    * @param comparator
218    * @return
219    */
220   static List<DBRefEntry> searchRefs(DBRefEntry[] refs, DBRefEntry entry,
221           DbRefComp comparator)
222   {
223     List<DBRefEntry> rfs = new ArrayList<>();
224     if (refs == null || entry == null)
225     {
226       return rfs;
227     }
228     for (int i = 0; i < refs.length; i++)
229     {
230       if (comparator.matches(entry, refs[i]))
231       {
232         rfs.add(refs[i]);
233       }
234     }
235     return rfs;
236   }
237
238   interface DbRefComp
239   {
240     public boolean matches(DBRefEntry refa, DBRefEntry refb);
241   }
242
243   /**
244    * match on all non-null fields in refa
245    */
246   // TODO unused - remove?
247   public static DbRefComp matchNonNullonA = new DbRefComp()
248   {
249     @Override
250     public boolean matches(DBRefEntry refa, DBRefEntry refb)
251     {
252       if (refa.getSource() == null
253               || DBRefUtils.getCanonicalName(refb.getSource()).equals(
254                       DBRefUtils.getCanonicalName(refa.getSource())))
255       {
256         if (refa.getVersion() == null
257                 || refb.getVersion().equals(refa.getVersion()))
258         {
259           if (refa.getAccessionId() == null
260                   || refb.getAccessionId().equals(refa.getAccessionId()))
261           {
262             if (refa.getMap() == null || (refb.getMap() != null
263                     && refb.getMap().equals(refa.getMap())))
264             {
265               return true;
266             }
267           }
268         }
269       }
270       return false;
271     }
272   };
273
274   /**
275    * either field is null or field matches for all of source, version, accession
276    * id and map.
277    */
278   // TODO unused - remove?
279   public static DbRefComp matchEitherNonNull = new DbRefComp()
280   {
281     @Override
282     public boolean matches(DBRefEntry refa, DBRefEntry refb)
283     {
284       if (nullOrEqualSource(refa.getSource(), refb.getSource())
285               && nullOrEqual(refa.getVersion(), refb.getVersion())
286               && nullOrEqual(refa.getAccessionId(), refb.getAccessionId())
287               && nullOrEqual(refa.getMap(), refb.getMap()))
288       {
289         return true;
290       }
291       return false;
292     }
293   };
294
295   /**
296    * accession ID and DB must be identical. Version is ignored. Map is either
297    * not defined or is a match (or is compatible?)
298    */
299   // TODO unused - remove?
300   public static DbRefComp matchDbAndIdAndEitherMap = new DbRefComp()
301   {
302     @Override
303     public boolean matches(DBRefEntry refa, DBRefEntry refb)
304     {
305       if (refa.getSource() != null && refb.getSource() != null
306               && DBRefUtils.getCanonicalName(refb.getSource()).equals(
307                       DBRefUtils.getCanonicalName(refa.getSource())))
308       {
309         // We dont care about version
310         if (refa.getAccessionId() != null && refb.getAccessionId() != null
311                 // FIXME should be && not || here?
312                 || refb.getAccessionId().equals(refa.getAccessionId()))
313         {
314           if ((refa.getMap() == null || refb.getMap() == null)
315                   || (refa.getMap() != null && refb.getMap() != null
316                           && refb.getMap().equals(refa.getMap())))
317           {
318             return true;
319           }
320         }
321       }
322       return false;
323     }
324   };
325
326   /**
327    * accession ID and DB must be identical. Version is ignored. No map on either
328    * or map but no maplist on either or maplist of map on a is the complement of
329    * maplist of map on b.
330    */
331   // TODO unused - remove?
332   public static DbRefComp matchDbAndIdAndComplementaryMapList = new DbRefComp()
333   {
334     @Override
335     public boolean matches(DBRefEntry refa, DBRefEntry refb)
336     {
337       if (refa.getSource() != null && refb.getSource() != null
338               && DBRefUtils.getCanonicalName(refb.getSource()).equals(
339                       DBRefUtils.getCanonicalName(refa.getSource())))
340       {
341         // We dont care about version
342         if (refa.getAccessionId() != null && refb.getAccessionId() != null
343                 || refb.getAccessionId().equals(refa.getAccessionId()))
344         {
345           if ((refa.getMap() == null && refb.getMap() == null)
346                   || (refa.getMap() != null && refb.getMap() != null))
347           {
348             if ((refb.getMap().getMap() == null
349                     && refa.getMap().getMap() == null)
350                     || (refb.getMap().getMap() != null
351                             && refa.getMap().getMap() != null
352                             && refb.getMap().getMap().getInverse()
353                                     .equals(refa.getMap().getMap())))
354             {
355               return true;
356             }
357           }
358         }
359       }
360       return false;
361     }
362   };
363
364   /**
365    * accession ID and DB must be identical. Version is ignored. No map on both
366    * or or map but no maplist on either or maplist of map on a is equivalent to
367    * the maplist of map on b.
368    */
369   // TODO unused - remove?
370   public static DbRefComp matchDbAndIdAndEquivalentMapList = new DbRefComp()
371   {
372     @Override
373     public boolean matches(DBRefEntry refa, DBRefEntry refb)
374     {
375       if (refa.getSource() != null && refb.getSource() != null
376               && DBRefUtils.getCanonicalName(refb.getSource()).equals(
377                       DBRefUtils.getCanonicalName(refa.getSource())))
378       {
379         // We dont care about version
380         // if ((refa.getVersion()==null || refb.getVersion()==null)
381         // || refb.getVersion().equals(refa.getVersion()))
382         // {
383         if (refa.getAccessionId() != null && refb.getAccessionId() != null
384                 || refb.getAccessionId().equals(refa.getAccessionId()))
385         {
386           if (refa.getMap() == null && refb.getMap() == null)
387           {
388             return true;
389           }
390           if (refa.getMap() != null && refb.getMap() != null
391                   && ((refb.getMap().getMap() == null
392                           && refa.getMap().getMap() == null)
393                           || (refb.getMap().getMap() != null
394                                   && refa.getMap().getMap() != null
395                                   && refb.getMap().getMap()
396                                           .equals(refa.getMap().getMap()))))
397           {
398             return true;
399           }
400         }
401       }
402       return false;
403     }
404   };
405
406   /**
407    * accession ID and DB must be identical, or null on a. Version is ignored. No
408    * map on either or map but no maplist on either or maplist of map on a is
409    * equivalent to the maplist of map on b.
410    */
411   public static DbRefComp matchDbAndIdAndEitherMapOrEquivalentMapList = new DbRefComp()
412   {
413     @Override
414     public boolean matches(DBRefEntry refa, DBRefEntry refb)
415     {
416       if (refa.getSource() != null && refb.getSource() != null
417               && DBRefUtils.getCanonicalName(refb.getSource()).equals(
418                       DBRefUtils.getCanonicalName(refa.getSource())))
419       {
420         // We dont care about version
421
422         if (refa.getAccessionId() == null
423                 || refa.getAccessionId().equals(refb.getAccessionId()))
424         {
425           if (refa.getMap() == null || refb.getMap() == null)
426           {
427             return true;
428           }
429           if ((refa.getMap() != null && refb.getMap() != null)
430                   && (refb.getMap().getMap() == null
431                           && refa.getMap().getMap() == null)
432                   || (refb.getMap().getMap() != null
433                           && refa.getMap().getMap() != null
434                           && (refb.getMap().getMap()
435                                   .equals(refa.getMap().getMap()))))
436           {
437             return true;
438           }
439         }
440       }
441       return false;
442     }
443   };
444
445   /**
446    * accession ID only must be identical.
447    */
448   public static DbRefComp matchId = new DbRefComp()
449   {
450     @Override
451     public boolean matches(DBRefEntry refa, DBRefEntry refb)
452     {
453       if (refa.getAccessionId() != null && refb.getAccessionId() != null
454               && refb.getAccessionId().equals(refa.getAccessionId()))
455       {
456         return true;
457       }
458       return false;
459     }
460   };
461
462   /**
463    * Parses a DBRefEntry and adds it to the sequence, also a PDBEntry if the
464    * database is PDB.
465    * <p>
466    * Used by file parsers to generate DBRefs from annotation within file (eg
467    * Stockholm)
468    * 
469    * @param dbname
470    * @param version
471    * @param acn
472    * @param seq
473    *          where to annotate with reference
474    * @return parsed version of entry that was added to seq (if any)
475    */
476   public static DBRefEntry parseToDbRef(SequenceI seq, String dbname,
477           String version, String acn)
478   {
479     DBRefEntry ref = null;
480     if (dbname != null)
481     {
482       String locsrc = DBRefUtils.getCanonicalName(dbname);
483       if (locsrc.equals(DBRefSource.PDB))
484       {
485         /*
486          * Check for PFAM style stockhom PDB accession id citation e.g.
487          * "1WRI A; 7-80;"
488          */
489         Regex r = new com.stevesoft.pat.Regex(
490                 "([0-9][0-9A-Za-z]{3})\\s*(.?)\\s*;\\s*([0-9]+)-([0-9]+)");
491         if (r.search(acn.trim()))
492         {
493           String pdbid = r.stringMatched(1);
494           String chaincode = r.stringMatched(2);
495           if (chaincode == null)
496           {
497             chaincode = " ";
498           }
499           // String mapstart = r.stringMatched(3);
500           // String mapend = r.stringMatched(4);
501           if (chaincode.equals(" "))
502           {
503             chaincode = "_";
504           }
505           // construct pdb ref.
506           ref = new DBRefEntry(locsrc, version, pdbid + chaincode);
507           PDBEntry pdbr = new PDBEntry();
508           pdbr.setId(pdbid);
509           pdbr.setType(PDBEntry.Type.PDB);
510           pdbr.setChainCode(chaincode);
511           seq.addPDBId(pdbr);
512         }
513         else
514         {
515           System.err.println("Malformed PDB DR line:" + acn);
516         }
517       }
518       else
519       {
520         // default:
521         ref = new DBRefEntry(locsrc, version, acn);
522       }
523     }
524     if (ref != null)
525     {
526       seq.addDBRef(ref);
527     }
528     return ref;
529   }
530
531   /**
532    * Returns true if either object is null, or they are equal
533    * 
534    * @param o1
535    * @param o2
536    * @return
537    */
538   public static boolean nullOrEqual(Object o1, Object o2)
539   {
540     if (o1 == null || o2 == null)
541     {
542       return true;
543     }
544     return o1.equals(o2);
545   }
546
547   /**
548    * canonicalise source string before comparing. null is always wildcard
549    * 
550    * @param o1
551    *          - null or source string to compare
552    * @param o2
553    *          - null or source string to compare
554    * @return true if either o1 or o2 are null, or o1 equals o2 under
555    *         DBRefUtils.getCanonicalName
556    *         (o1).equals(DBRefUtils.getCanonicalName(o2))
557    */
558   public static boolean nullOrEqualSource(String o1, String o2)
559   {
560     if (o1 == null || o2 == null)
561     {
562       return true;
563     }
564     return DBRefUtils.getCanonicalName(o1)
565             .equals(DBRefUtils.getCanonicalName(o2));
566   }
567
568   /**
569    * Selects just the DNA or protein references from a set of references
570    * 
571    * @param selectDna
572    *          if true, select references to 'standard' DNA databases, else to
573    *          'standard' peptide databases
574    * @param refs
575    *          a set of references to select from
576    * @return
577    */
578   public static DBRefEntry[] selectDbRefs(boolean selectDna,
579           DBRefEntry[] refs)
580   {
581     return selectRefs(refs,
582             selectDna ? DBRefSource.DNACODINGDBS : DBRefSource.PROTEINDBS);
583     // could attempt to find other cross
584     // refs here - ie PDB xrefs
585     // (not dna, not protein seq)
586   }
587
588   /**
589    * Returns the (possibly empty) list of those supplied dbrefs which have the
590    * specified source database, with a case-insensitive match of source name
591    * 
592    * @param dbRefs
593    * @param source
594    * @return
595    */
596   public static List<DBRefEntry> searchRefsForSource(DBRefEntry[] dbRefs,
597           String source)
598   {
599     List<DBRefEntry> matches = new ArrayList<>();
600     if (dbRefs != null && source != null)
601     {
602       for (DBRefEntry dbref : dbRefs)
603       {
604         if (source.equalsIgnoreCase(dbref.getSource()))
605         {
606           matches.add(dbref);
607         }
608       }
609     }
610     return matches;
611   }
612
613   /**
614    * promote direct database references to primary for nucleotide or protein
615    * sequences if they have an appropriate primary ref
616    * <table>
617    * <tr>
618    * <th>Seq Type</th>
619    * <th>Primary DB</th>
620    * <th>Direct which will be promoted</th>
621    * </tr>
622    * <tr align=center>
623    * <td>peptides</td>
624    * <td>Ensembl</td>
625    * <td>Uniprot</td>
626    * </tr>
627    * <tr align=center>
628    * <td>peptides</td>
629    * <td>Ensembl</td>
630    * <td>Uniprot</td>
631    * </tr>
632    * <tr align=center>
633    * <td>dna</td>
634    * <td>Ensembl</td>
635    * <td>ENA</td>
636    * </tr>
637    * </table>
638    * 
639    * @param sequence
640    */
641   public static void ensurePrimaries(SequenceI sequence)
642   {
643     List<DBRefEntry> pr = sequence.getPrimaryDBRefs();
644     if (pr.size() == 0)
645     {
646       // nothing to do
647       return;
648     }
649     List<DBRefEntry> selfs = new ArrayList<>();
650     {
651       DBRefEntry[] selfArray = selectDbRefs(!sequence.isProtein(),
652               sequence.getDBRefs());
653       if (selfArray == null || selfArray.length == 0)
654       {
655         // nothing to do
656         return;
657       }
658       selfs.addAll(Arrays.asList(selfArray));
659     }
660
661     // filter non-primary refs
662     for (DBRefEntry p : pr)
663     {
664       while (selfs.contains(p))
665       {
666         selfs.remove(p);
667       }
668     }
669     List<DBRefEntry> toPromote = new ArrayList<>();
670
671     for (DBRefEntry p : pr)
672     {
673       List<String> promType = new ArrayList<>();
674       if (sequence.isProtein())
675       {
676         switch (getCanonicalName(p.getSource()))
677         {
678         case DBRefSource.UNIPROT:
679           // case DBRefSource.UNIPROTKB:
680           // case DBRefSource.UP_NAME:
681           // search for and promote ensembl
682           promType.add(DBRefSource.ENSEMBL);
683           break;
684         case DBRefSource.ENSEMBL:
685           // search for and promote Uniprot
686           promType.add(DBRefSource.UNIPROT);
687           break;
688         }
689       }
690       else
691       {
692         // TODO: promote transcript refs
693       }
694
695       // collate candidates and promote them
696       DBRefEntry[] candidates = selectRefs(selfs.toArray(new DBRefEntry[0]),
697               promType.toArray(new String[0]));
698       if (candidates != null)
699       {
700         for (DBRefEntry cand : candidates)
701         {
702           if (cand.hasMap())
703           {
704             if (cand.getMap().getTo() != null
705                     && cand.getMap().getTo() != sequence)
706             {
707               // can't promote refs with mappings to other sequences
708               continue;
709             }
710             if (cand.getMap().getMap().getFromLowest() != sequence
711                     .getStart()
712                     && cand.getMap().getMap().getFromHighest() != sequence
713                             .getEnd())
714             {
715               // can't promote refs with mappings from a region of this sequence
716               // - eg CDS
717               continue;
718             }
719           }
720           // and promote
721           cand.setVersion(p.getVersion() + " (promoted)");
722           selfs.remove(cand);
723           toPromote.add(cand);
724           if (!cand.isPrimaryCandidate())
725           {
726             System.out.println(
727                     "Warning: Couldn't promote dbref " + cand.toString()
728                             + " for sequence " + sequence.toString());
729           }
730         }
731       }
732     }
733   }
734
735 }