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