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