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