Merge branch 'develop' into JAL-1705_trialMerge
[jalview.git] / src / jalview / io / gff / SequenceOntology.java
1 package jalview.io.gff;
2
3 import java.io.BufferedInputStream;
4 import java.io.BufferedReader;
5 import java.io.IOException;
6 import java.io.InputStream;
7 import java.io.InputStreamReader;
8 import java.text.ParseException;
9 import java.util.ArrayList;
10 import java.util.HashMap;
11 import java.util.List;
12 import java.util.Map;
13 import java.util.NoSuchElementException;
14 import java.util.zip.ZipEntry;
15 import java.util.zip.ZipInputStream;
16
17 import org.biojava.nbio.ontology.Ontology;
18 import org.biojava.nbio.ontology.Term;
19 import org.biojava.nbio.ontology.Term.Impl;
20 import org.biojava.nbio.ontology.Triple;
21 import org.biojava.nbio.ontology.io.OboParser;
22 import org.biojava.nbio.ontology.utils.Annotation;
23
24 /**
25  * A wrapper class that parses the Sequence Ontology and exposes useful access
26  * methods. This version uses the BioJava parser.
27  */
28 public class SequenceOntology
29 {
30
31   /*
32    * selected commonly used values for quick reference
33    */
34   // SO:0000316
35   public static final String CDS = "CDS";
36
37   // SO:0001060
38   public static final String SEQUENCE_VARIANT = "sequence_variant";
39
40   // SO:0000147
41   public static final String EXON = "exon";
42
43   // SO:0000673
44   public static final String TRANSCRIPT = "transcript";
45
46   // SO:0000704
47   public static final String GENE = "gene";
48
49   /*
50    * singleton instance of this class
51    */
52   private static SequenceOntology instance;
53
54   /*
55    * the parsed Ontology data as modelled by BioJava
56    */
57   private Ontology ontology;
58
59   /*
60    * the ontology term for the isA relationship
61    */
62   private Term isA;
63
64   /*
65    * lookup of terms by user readable name (NB not guaranteed unique)
66    */
67   private Map<String, Term> termsByDescription;
68
69   /*
70    * Map where key is a Term and value is a (possibly empty) list of 
71    * all Terms to which the key has an 'isA' relationship, either
72    * directly or indirectly (A isA B isA C)
73    */
74   private Map<Term, List<Term>> termIsA;
75
76   /**
77    * Returns singleton instance
78    * 
79    * @return
80    */
81   public synchronized static SequenceOntology getInstance()
82   {
83     if (instance == null)
84     {
85       instance = new SequenceOntology();
86     }
87     return instance;
88   }
89
90   /**
91    * Private constructor to enforce use of singleton. Parses and caches the SO
92    * OBO data file.
93    */
94   private SequenceOntology()
95   {
96     termsByDescription = new HashMap<String, Term>();
97     termIsA = new HashMap<Term, List<Term>>();
98
99     loadOntologyZipFile("so-xp-simple.obo");
100   }
101
102   /**
103    * Loads the given ontology file from a zip file with ".zip" appended
104    * 
105    * @param ontologyFile
106    */
107   protected void loadOntologyZipFile(String ontologyFile)
108   {
109     ZipInputStream zipStream = null;
110     try
111     {
112       String zipFile = ontologyFile + ".zip";
113       System.out.println("Loading Sequence Ontology from " + zipFile);
114       InputStream inStream = this.getClass().getResourceAsStream(
115               "/" + zipFile);
116       zipStream = new ZipInputStream(new BufferedInputStream(inStream));
117       ZipEntry entry;
118       while ((entry = zipStream.getNextEntry()) != null)
119       {
120         if (entry.getName().equals(ontologyFile))
121         {
122           loadOboFile(zipStream);
123         }
124       }
125     } catch (Exception e)
126     {
127       e.printStackTrace();
128     } finally
129     {
130       closeStream(zipStream);
131     }
132   }
133
134   /**
135    * Closes the input stream, swallowing all exceptions
136    * 
137    * @param is
138    */
139   protected void closeStream(InputStream is)
140   {
141     if (is != null)
142     {
143       try
144       {
145         is.close();
146       } catch (IOException e)
147       {
148         // ignore
149       }
150     }
151   }
152
153   /**
154    * Reads, parses and stores the OBO file data
155    * 
156    * @param is
157    * @throws ParseException
158    * @throws IOException
159    */
160   protected void loadOboFile(InputStream is) throws ParseException,
161           IOException
162   {
163     BufferedReader oboFile = new BufferedReader(new InputStreamReader(is));
164     OboParser parser = new OboParser();
165     ontology = parser.parseOBO(oboFile, "SO", "the SO ontology");
166     isA = ontology.getTerm("is_a");
167     storeTermNames();
168   }
169
170   /**
171    * Stores a lookup table of terms by description. Note that description is not
172    * guaranteed unique. Where duplicate descriptions are found, try to discard
173    * the term that is flagged as obsolete. However we do store obsolete terms
174    * where there is no duplication of description.
175    */
176   protected void storeTermNames()
177   {
178     for (Term term : ontology.getTerms())
179     {
180       if (term instanceof Impl)
181       {
182         String description = term.getDescription();
183         if (description != null)
184         {
185           Term replaced = termsByDescription.get(description);
186           if (replaced != null)
187           {
188             boolean newTermIsObsolete = isObsolete(term);
189             boolean oldTermIsObsolete = isObsolete(replaced);
190             if (newTermIsObsolete && !oldTermIsObsolete)
191             {
192               System.err.println("Ignoring " + term.getName()
193                       + " as obsolete and duplicated by "
194                       + replaced.getName());
195               term = replaced;
196             }
197             else if (!newTermIsObsolete && oldTermIsObsolete)
198             {
199               System.err.println("Ignoring " + replaced.getName()
200                       + " as obsolete and duplicated by " + term.getName());
201             }
202             else
203             {
204             System.err.println("Warning: " + term.getName()
205                     + " has replaced " + replaced.getName()
206                     + " for lookup of '" + description + "'");
207             }
208           }
209           termsByDescription.put(description, term);
210         }
211       }
212     }
213   }
214
215   /**
216    * Answers true if the term has property "is_obsolete" with value true, else
217    * false
218    * 
219    * @param term
220    * @return
221    */
222   public static boolean isObsolete(Term term)
223   {
224     Annotation ann = term.getAnnotation();
225     if (ann != null)
226     {
227       try
228       {
229       if (Boolean.TRUE.equals(ann.getProperty("is_obsolete")))
230       {
231           return true;
232         }
233       } catch (NoSuchElementException e)
234       {
235         // fall through to false
236       }
237     }
238     return false;
239   }
240
241   /**
242    * Test whether the given Sequence Ontology term is nucleotide_match (either
243    * directly or via is_a relationship)
244    * 
245    * @param soTerm
246    *          SO name or description
247    * @return
248    */
249   public boolean isNucleotideMatch(String soTerm)
250   {
251     return isA(soTerm, "nucleotide_match");
252   }
253
254   /**
255    * Test whether the given Sequence Ontology term is protein_match (either
256    * directly or via is_a relationship)
257    * 
258    * @param soTerm
259    *          SO name or description
260    * @return
261    */
262   public boolean isProteinMatch(String soTerm)
263   {
264     return isA(soTerm, "protein_match");
265   }
266
267   /**
268    * Test whether the given Sequence Ontology term is polypeptide (either
269    * directly or via is_a relationship)
270    * 
271    * @param soTerm
272    *          SO name or description
273    * @return
274    */
275   public boolean isPolypeptide(String soTerm)
276   {
277     return isA(soTerm, "polypeptide");
278   }
279
280   /**
281    * Returns true if the given term has a (direct or indirect) 'isA'
282    * relationship with the parent
283    * 
284    * @param child
285    * @param parent
286    * @return
287    */
288   public boolean isA(String child, String parent)
289   {
290     /*
291      * optimise trivial checks like isA("CDS", "CDS")
292      */
293     if (child.equals(parent))
294     {
295       return true;
296     }
297
298     Term childTerm = getTerm(child);
299     Term parentTerm = getTerm(parent);
300
301     return termIsA(childTerm, parentTerm);
302   }
303
304   /**
305    * Returns true if the childTerm 'isA' parentTerm (directly or indirectly).
306    * 
307    * @param childTerm
308    * @param parentTerm
309    * @return
310    */
311   protected synchronized boolean termIsA(Term childTerm, Term parentTerm)
312   {
313     /*
314      * null term could arise from a misspelled SO description
315      */
316     if (childTerm == null || parentTerm == null)
317     {
318       return false;
319     }
320
321     /*
322      * recursive search endpoint:
323      */
324     if (childTerm == parentTerm)
325     {
326       return true;
327     }
328
329     /*
330      * lazy initialisation - find all of a term's parents (recursively) 
331      * the first time this is called, and save them in a map.
332      */
333     if (!termIsA.containsKey(childTerm))
334     {
335       findParents(childTerm);
336     }
337
338     List<Term> parents = termIsA.get(childTerm);
339     for (Term parent : parents)
340     {
341       if (termIsA(parent, parentTerm))
342       {
343         /*
344          * add (great-)grandparents to parents list as they are discovered,
345          * for faster lookup next time
346          */
347         if (!parents.contains(parentTerm))
348         {
349           parents.add(parentTerm);
350         }
351         return true;
352       }
353     }
354
355     return false;
356   }
357
358   /**
359    * Finds all the 'isA' parents of the childTerm and stores them as a (possibly
360    * empty) list.
361    * 
362    * @param childTerm
363    */
364   protected synchronized void findParents(Term childTerm)
365   {
366     List<Term> result = new ArrayList<Term>();
367     for (Triple triple : ontology.getTriples(childTerm, null, isA))
368     {
369       Term parent = triple.getObject();
370       result.add(parent);
371
372       /*
373        * and search for the parent's parents recursively
374        */
375       findParents(parent);
376     }
377     termIsA.put(childTerm, result);
378   }
379
380   /**
381    * Returns the Term for a given name (e.g. "SO:0000735") or description (e.g.
382    * "sequence_location"), or null if not found.
383    * 
384    * @param child
385    * @return
386    */
387   protected Term getTerm(String nameOrDescription)
388   {
389     Term t = termsByDescription.get(nameOrDescription);
390     if (t == null)
391     {
392       try
393       {
394         t = ontology.getTerm(nameOrDescription);
395       } catch (NoSuchElementException e)
396       {
397         // not found
398       }
399     }
400     return t;
401   }
402
403   public boolean isSequenceVariant(String soTerm)
404   {
405     return isA(soTerm, "sequence_variant");
406   }
407 }