JAL-2089 patch broken merge to master for Release 2.10.0b1
[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     // Make sure we have lowercase entries for all canonical string lookups
69     Set<String> keys = canonicalSourceNameLookup.keySet();
70     for (String k : keys)
71     {
72       canonicalSourceNameLookup.put(k.toLowerCase(),
73               canonicalSourceNameLookup.get(k));
74     }
75
76     dasCoordinateSystemsLookup.put("pdbresnum", DBRefSource.PDB);
77     dasCoordinateSystemsLookup.put("uniprot", DBRefSource.UNIPROT);
78     dasCoordinateSystemsLookup.put("embl", DBRefSource.EMBL);
79     // dasCoordinateSystemsLookup.put("embl", DBRefSource.EMBLCDS);
80   }
81
82   /**
83    * Returns those DBRefEntry objects whose source identifier (once converted to
84    * Jalview's canonical form) is in the list of sources to search for. Returns
85    * null if no matches found.
86    * 
87    * @param dbrefs
88    *          DBRefEntry objects to search
89    * @param sources
90    *          array of sources to select
91    * @return
92    */
93   public static DBRefEntry[] selectRefs(DBRefEntry[] dbrefs,
94           String[] sources)
95   {
96     if (dbrefs == null || sources == null)
97     {
98       return dbrefs;
99     }
100     HashSet<String> srcs = new HashSet<String>();
101     for (String src : sources)
102     {
103       srcs.add(src);
104     }
105
106     List<DBRefEntry> res = new ArrayList<DBRefEntry>();
107     for (DBRefEntry dbr : dbrefs)
108     {
109       String source = getCanonicalName(dbr.getSource());
110       if (srcs.contains(source))
111       {
112         res.add(dbr);
113       }
114     }
115
116     if (res.size() > 0)
117     {
118       DBRefEntry[] reply = new DBRefEntry[res.size()];
119       return res.toArray(reply);
120     }
121     return null;
122   }
123
124   /**
125    * isDasCoordinateSystem
126    * 
127    * @param string
128    *          String
129    * @param dBRefEntry
130    *          DBRefEntry
131    * @return boolean true if Source DBRefEntry is compatible with DAS
132    *         CoordinateSystem name
133    */
134
135   public static boolean isDasCoordinateSystem(String string,
136           DBRefEntry dBRefEntry)
137   {
138     if (string == null || dBRefEntry == null)
139     {
140       return false;
141     }
142     String coordsys = dasCoordinateSystemsLookup.get(string.toLowerCase());
143     return coordsys == null ? false : coordsys.equals(dBRefEntry
144             .getSource());
145   }
146
147   /**
148    * look up source in an internal list of database reference sources and return
149    * the canonical jalview name for the source, or the original string if it has
150    * no canonical form.
151    * 
152    * @param source
153    * @return canonical jalview source (one of jalview.datamodel.DBRefSource.*)
154    *         or original source
155    */
156   public static String getCanonicalName(String source)
157   {
158     if (source == null)
159     {
160       return null;
161     }
162     String canonical = canonicalSourceNameLookup.get(source.toLowerCase());
163     return canonical == null ? source : canonical;
164   }
165
166   /**
167    * Returns a (possibly empty) list of those references that match the given
168    * entry. Currently uses a comparator which matches if
169    * <ul>
170    * <li>database sources are the same</li>
171    * <li>accession ids are the same</li>
172    * <li>both have no mapping, or the mappings are the same</li>
173    * </ul>
174    * 
175    * @param ref
176    *          Set of references to search
177    * @param entry
178    *          pattern to match
179    * @return
180    */
181   public static List<DBRefEntry> searchRefs(DBRefEntry[] ref,
182           DBRefEntry entry)
183   {
184     return searchRefs(ref, entry,
185             matchDbAndIdAndEitherMapOrEquivalentMapList);
186   }
187
188   /**
189    * Returns a list of those references that match the given accession id
190    * <ul>
191    * <li>database sources are the same</li>
192    * <li>accession ids are the same</li>
193    * <li>both have no mapping, or the mappings are the same</li>
194    * </ul>
195    * 
196    * @param refs
197    *          Set of references to search
198    * @param accId
199    *          accession id to match
200    * @return
201    */
202   public static List<DBRefEntry> searchRefs(DBRefEntry[] refs, String accId)
203   {
204     return searchRefs(refs, new DBRefEntry("", "", accId), matchId);
205   }
206
207   /**
208    * Returns a (possibly empty) list of those references that match the given
209    * entry, according to the given comparator.
210    * 
211    * @param refs
212    *          an array of database references to search
213    * @param entry
214    *          an entry to compare against
215    * @param comparator
216    * @return
217    */
218   static List<DBRefEntry> searchRefs(DBRefEntry[] refs, DBRefEntry entry,
219           DbRefComp comparator)
220   {
221     List<DBRefEntry> rfs = new ArrayList<DBRefEntry>();
222     if (refs == null || entry == null)
223     {
224       return rfs;
225     }
226     for (int i = 0; i < refs.length; i++)
227     {
228       if (comparator.matches(entry, refs[i]))
229       {
230         rfs.add(refs[i]);
231       }
232     }
233     return rfs;
234   }
235
236   interface DbRefComp
237   {
238     public boolean matches(DBRefEntry refa, DBRefEntry refb);
239   }
240
241   /**
242    * match on all non-null fields in refa
243    */
244   // TODO unused - remove?
245   public static DbRefComp matchNonNullonA = new DbRefComp()
246   {
247     @Override
248     public boolean matches(DBRefEntry refa, DBRefEntry refb)
249     {
250       if (refa.getSource() == null
251               || DBRefUtils.getCanonicalName(refb.getSource()).equals(
252                       DBRefUtils.getCanonicalName(refa.getSource())))
253       {
254         if (refa.getVersion() == null
255                 || refb.getVersion().equals(refa.getVersion()))
256         {
257           if (refa.getAccessionId() == null
258                   || refb.getAccessionId().equals(refa.getAccessionId()))
259           {
260             if (refa.getMap() == null
261                     || (refb.getMap() != null && refb.getMap().equals(
262                             refa.getMap())))
263             {
264               return true;
265             }
266           }
267         }
268       }
269       return false;
270     }
271   };
272
273   /**
274    * either field is null or field matches for all of source, version, accession
275    * id and map.
276    */
277   // TODO unused - remove?
278   public static DbRefComp matchEitherNonNull = new DbRefComp()
279   {
280     @Override
281     public boolean matches(DBRefEntry refa, DBRefEntry refb)
282     {
283       if (nullOrEqualSource(refa.getSource(), refb.getSource())
284               && nullOrEqual(refa.getVersion(), refb.getVersion())
285               && nullOrEqual(refa.getAccessionId(), refb.getAccessionId())
286               && nullOrEqual(refa.getMap(), refb.getMap()))
287       {
288         return true;
289       }
290       return false;
291     }
292   };
293
294   /**
295    * accession ID and DB must be identical. Version is ignored. Map is either
296    * not defined or is a match (or is compatible?)
297    */
298   // TODO unused - remove?
299   public static DbRefComp matchDbAndIdAndEitherMap = new DbRefComp()
300   {
301     @Override
302     public boolean matches(DBRefEntry refa, DBRefEntry refb)
303     {
304       if (refa.getSource() != null
305               && 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 && refb
316                           .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
338               && refb.getSource() != null
339               && DBRefUtils.getCanonicalName(refb.getSource()).equals(
340                       DBRefUtils.getCanonicalName(refa.getSource())))
341       {
342         // We dont care about version
343         if (refa.getAccessionId() != null && refb.getAccessionId() != null
344                 || refb.getAccessionId().equals(refa.getAccessionId()))
345         {
346           if ((refa.getMap() == null && refb.getMap() == null)
347                   || (refa.getMap() != null && refb.getMap() != null))
348           {
349             if ((refb.getMap().getMap() == null && refa.getMap().getMap() == null)
350                     || (refb.getMap().getMap() != null
351                             && refa.getMap().getMap() != null && refb
352                             .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
376               && refb.getSource() != null
377               && DBRefUtils.getCanonicalName(refb.getSource()).equals(
378                       DBRefUtils.getCanonicalName(refa.getSource())))
379       {
380         // We dont care about version
381         // if ((refa.getVersion()==null || refb.getVersion()==null)
382         // || refb.getVersion().equals(refa.getVersion()))
383         // {
384         if (refa.getAccessionId() != null && refb.getAccessionId() != null
385                 || refb.getAccessionId().equals(refa.getAccessionId()))
386         {
387           if (refa.getMap() == null && refb.getMap() == null)
388           {
389             return true;
390           }
391           if (refa.getMap() != null
392                   && refb.getMap() != null
393                   && ((refb.getMap().getMap() == null && refa.getMap()
394                           .getMap() == null) || (refb.getMap().getMap() != null
395                           && refa.getMap().getMap() != null && refb
396                           .getMap().getMap().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
417               && refb.getSource() != null
418               && DBRefUtils.getCanonicalName(refb.getSource()).equals(
419                       DBRefUtils.getCanonicalName(refa.getSource())))
420       {
421         // We dont care about version
422
423         if (refa.getAccessionId() == null
424                 || refa.getAccessionId().equals(refb.getAccessionId()))
425         {
426           if (refa.getMap() == null || refb.getMap() == null)
427           {
428             return true;
429           }
430           if ((refa.getMap() != null && refb.getMap() != null)
431                   && (refb.getMap().getMap() == null && refa.getMap()
432                           .getMap() == null)
433                   || (refb.getMap().getMap() != null
434                           && refa.getMap().getMap() != null && (refb
435                           .getMap().getMap().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).equals(
565             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, selectDna ? DBRefSource.DNACODINGDBS
582             : 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<DBRefEntry>();
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<DBRefEntry>();
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<DBRefEntry>();
670
671     for (DBRefEntry p : pr)
672     {
673       List<String> promType = new ArrayList<String>();
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(
697               selfs.toArray(new DBRefEntry[0]),
698               promType.toArray(new String[0]));
699       if (candidates != null)
700       {
701         for (DBRefEntry cand : candidates)
702         {
703           if (cand.hasMap())
704           {
705             if (cand.getMap().getTo() != null
706                     && cand.getMap().getTo() != sequence)
707             {
708               // can't promote refs with mappings to other sequences
709               continue;
710             }
711             if (cand.getMap().getMap().getFromLowest() != sequence
712                     .getStart()
713                     && cand.getMap().getMap().getFromHighest() != sequence
714                             .getEnd())
715             {
716               // can't promote refs with mappings from a region of this sequence
717               // - eg CDS
718               continue;
719             }
720           }
721           // and promote
722           cand.setVersion(p.getVersion() + " (promoted)");
723           selfs.remove(cand);
724           toPromote.add(cand);
725           if (!cand.isPrimaryCandidate())
726           {
727             System.out.println("Warning: Couldn't promote dbref "
728                     + cand.toString() + " for sequence "
729                     + sequence.toString());
730           }
731         }
732       }
733     }
734   }
735
736 }