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