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