af80b7a0efbe2ae52d4c0c3ca269bd44d22fed30
[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     ZipInputStream zipStream = null;
81     try
82     {
83       String zipFile = ontologyFile + ".zip";
84       System.out.println("Loading Sequence Ontology from " + zipFile);
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     } catch (Exception e)
97     {
98       e.printStackTrace();
99     } finally
100     {
101       closeStream(zipStream);
102     }
103   }
104
105   /**
106    * Closes the input stream, swallowing all exceptions
107    * 
108    * @param is
109    */
110   protected void closeStream(InputStream is)
111   {
112     if (is != null)
113     {
114       try
115       {
116         is.close();
117       } catch (IOException e)
118       {
119         // ignore
120       }
121     }
122   }
123
124   /**
125    * Reads, parses and stores the OBO file data
126    * 
127    * @param is
128    * @throws ParseException
129    * @throws IOException
130    */
131   protected void loadOboFile(InputStream is) throws ParseException,
132           IOException
133   {
134     BufferedReader oboFile = new BufferedReader(new InputStreamReader(is));
135     OboParser parser = new OboParser();
136     ontology = parser.parseOBO(oboFile, "SO", "the SO ontology");
137     isA = ontology.getTerm("is_a");
138     storeTermNames();
139   }
140
141   /**
142    * Stores a lookup table of terms by description. Note that description is not
143    * guaranteed unique. Where duplicate descriptions are found, try to discard
144    * the term that is flagged as obsolete. However we do store obsolete terms
145    * where there is no duplication of description.
146    */
147   protected void storeTermNames()
148   {
149     for (Term term : ontology.getTerms())
150     {
151       if (term instanceof Impl)
152       {
153         String description = term.getDescription();
154         if (description != null)
155         {
156           Term replaced = termsByDescription.get(description);
157           if (replaced != null)
158           {
159             boolean newTermIsObsolete = isObsolete(term);
160             boolean oldTermIsObsolete = isObsolete(replaced);
161             if (newTermIsObsolete && !oldTermIsObsolete)
162             {
163               System.err.println("Ignoring " + term.getName()
164                       + " as obsolete and duplicated by "
165                       + replaced.getName());
166               term = replaced;
167             }
168             else if (!newTermIsObsolete && oldTermIsObsolete)
169             {
170               System.err.println("Ignoring " + replaced.getName()
171                       + " as obsolete and duplicated by " + term.getName());
172             }
173             else
174             {
175             System.err.println("Warning: " + term.getName()
176                     + " has replaced " + replaced.getName()
177                     + " for lookup of '" + description + "'");
178             }
179           }
180           termsByDescription.put(description, term);
181         }
182       }
183     }
184   }
185
186   /**
187    * Answers true if the term has property "is_obsolete" with value true, else
188    * false
189    * 
190    * @param term
191    * @return
192    */
193   public static boolean isObsolete(Term term)
194   {
195     Annotation ann = term.getAnnotation();
196     if (ann != null)
197     {
198       try
199       {
200       if (Boolean.TRUE.equals(ann.getProperty("is_obsolete")))
201       {
202           return true;
203         }
204       } catch (NoSuchElementException e)
205       {
206         // fall through to false
207       }
208     }
209     return false;
210   }
211
212   /**
213    * Test whether the given Sequence Ontology term is nucleotide_match (either
214    * directly or via is_a relationship)
215    * 
216    * @param soTerm
217    *          SO name or description
218    * @return
219    */
220   public boolean isNucleotideMatch(String soTerm)
221   {
222     return isA(soTerm, NUCLEOTIDE_MATCH);
223   }
224
225   /**
226    * Test whether the given Sequence Ontology term is protein_match (either
227    * directly or via is_a relationship)
228    * 
229    * @param soTerm
230    *          SO name or description
231    * @return
232    */
233   public boolean isProteinMatch(String soTerm)
234   {
235     return isA(soTerm, PROTEIN_MATCH);
236   }
237
238   /**
239    * Test whether the given Sequence Ontology term is polypeptide (either
240    * directly or via is_a relationship)
241    * 
242    * @param soTerm
243    *          SO name or description
244    * @return
245    */
246   public boolean isPolypeptide(String soTerm)
247   {
248     return isA(soTerm, POLYPEPTIDE);
249   }
250
251   /**
252    * Returns true if the given term has a (direct or indirect) 'isA'
253    * relationship with the parent
254    * 
255    * @param child
256    * @param parent
257    * @return
258    */
259   @Override
260   public boolean isA(String child, String parent)
261   {
262     if (child == null || parent == null)
263     {
264       return false;
265     }
266     /*
267      * optimise trivial checks like isA("CDS", "CDS")
268      */
269     if (child.equals(parent))
270     {
271       termFound(child);
272       return true;
273     }
274
275     Term childTerm = getTerm(child);
276     if (childTerm != null)
277     {
278       termFound(child);
279     }
280     else
281     {
282       termNotFound(child);
283     }
284     Term parentTerm = getTerm(parent);
285
286     return termIsA(childTerm, parentTerm);
287   }
288
289   /**
290    * Records a valid term queried for, for reporting purposes
291    * 
292    * @param term
293    */
294   private void termFound(String term)
295   {
296     synchronized (termsFound)
297     {
298       if (!termsFound.contains(term))
299       {
300         termsFound.add(term);
301       }
302     }
303   }
304
305   /**
306    * Records an invalid term queried for, for reporting purposes
307    * 
308    * @param term
309    */
310   private void termNotFound(String term)
311   {
312     synchronized (termsNotFound)
313     {
314       if (!termsNotFound.contains(term))
315       {
316         System.err.println("SO term " + term + " invalid");
317         termsNotFound.add(term);
318       }
319     }
320   }
321
322   /**
323    * Returns true if the childTerm 'isA' parentTerm (directly or indirectly).
324    * 
325    * @param childTerm
326    * @param parentTerm
327    * @return
328    */
329   protected synchronized boolean termIsA(Term childTerm, Term parentTerm)
330   {
331     /*
332      * null term could arise from a misspelled SO description
333      */
334     if (childTerm == null || parentTerm == null)
335     {
336       return false;
337     }
338
339     /*
340      * recursive search endpoint:
341      */
342     if (childTerm == parentTerm)
343     {
344       return true;
345     }
346
347     /*
348      * lazy initialisation - find all of a term's parents (recursively) 
349      * the first time this is called, and save them in a map.
350      */
351     if (!termIsA.containsKey(childTerm))
352     {
353       findParents(childTerm);
354     }
355
356     List<Term> parents = termIsA.get(childTerm);
357     for (Term parent : parents)
358     {
359       if (termIsA(parent, parentTerm))
360       {
361         /*
362          * add (great-)grandparents to parents list as they are discovered,
363          * for faster lookup next time
364          */
365         if (!parents.contains(parentTerm))
366         {
367           parents.add(parentTerm);
368         }
369         return true;
370       }
371     }
372
373     return false;
374   }
375
376   /**
377    * Finds all the 'isA' parents of the childTerm and stores them as a (possibly
378    * empty) list.
379    * 
380    * @param childTerm
381    */
382   protected synchronized void findParents(Term childTerm)
383   {
384     List<Term> result = new ArrayList<Term>();
385     for (Triple triple : ontology.getTriples(childTerm, null, isA))
386     {
387       Term parent = triple.getObject();
388       result.add(parent);
389
390       /*
391        * and search for the parent's parents recursively
392        */
393       findParents(parent);
394     }
395     termIsA.put(childTerm, result);
396   }
397
398   /**
399    * Returns the Term for a given name (e.g. "SO:0000735") or description (e.g.
400    * "sequence_location"), or null if not found.
401    * 
402    * @param child
403    * @return
404    */
405   protected Term getTerm(String nameOrDescription)
406   {
407     Term t = termsByDescription.get(nameOrDescription);
408     if (t == null)
409     {
410       try
411       {
412         t = ontology.getTerm(nameOrDescription);
413       } catch (NoSuchElementException e)
414       {
415         // not found
416       }
417     }
418     return t;
419   }
420
421   public boolean isSequenceVariant(String soTerm)
422   {
423     return isA(soTerm, SEQUENCE_VARIANT);
424   }
425
426   /**
427    * Sorts (case-insensitive) and returns the list of valid terms queried for
428    */
429   @Override
430   public List<String> termsFound()
431   {
432     synchronized (termsFound)
433     {
434       Collections.sort(termsFound, String.CASE_INSENSITIVE_ORDER);
435       return termsFound;
436     }
437   }
438
439   /**
440    * Sorts (case-insensitive) and returns the list of invalid terms queried for
441    */
442   @Override
443   public List<String> termsNotFound()
444   {
445     synchronized (termsNotFound)
446     {
447       Collections.sort(termsNotFound, String.CASE_INSENSITIVE_ORDER);
448       return termsNotFound;
449     }
450   }
451 }