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