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