JAL-2717 adjusted code so default correctly returned if i18n fails
[jalview.git] / src / jalview / analysis / GeneticCodes.java
1 package jalview.analysis;
2
3 import jalview.bin.Cache;
4
5 import java.io.BufferedReader;
6 import java.io.IOException;
7 import java.io.InputStream;
8 import java.io.InputStreamReader;
9 import java.util.HashMap;
10 import java.util.LinkedHashMap;
11 import java.util.Map;
12 import java.util.StringTokenizer;
13
14 /**
15  * A singleton that provides instances of genetic code translation tables
16  * 
17  * @author gmcarstairs
18  * @see https://www.ncbi.nlm.nih.gov/Taxonomy/Utils/wprintgc.cgi
19  */
20 public final class GeneticCodes
21 {
22   private static final int CODON_LENGTH = 3;
23
24   private static final String QUOTE = "\"";
25
26   /*
27    * nucleotides as ordered in data file
28    */
29   private static final String NUCS = "TCAG";
30
31   private static final int NUCS_COUNT = NUCS.length();
32
33   private static final int NUCS_COUNT_SQUARED = NUCS_COUNT * NUCS_COUNT;
34
35   private static final int NUCS_COUNT_CUBED = NUCS_COUNT * NUCS_COUNT
36           * NUCS_COUNT;
37
38   private static final String AMBIGUITY_CODES_FILE = "/AmbiguityCodes.dat";
39
40   private static final String RESOURCE_FILE = "/GeneticCodes.dat";
41
42   private static GeneticCodes instance = new GeneticCodes();
43
44   private Map<String, String> ambiguityCodes;
45
46   /*
47    * loaded code tables, with keys in order of loading 
48    */
49   private Map<String, GeneticCodeI> codeTables;
50
51   /**
52    * Private constructor enforces singleton
53    */
54   private GeneticCodes()
55   {
56     if (instance == null)
57     {
58       ambiguityCodes = new HashMap<>();
59
60       /*
61        * LinkedHashMap preserves order of addition of entries,
62        * so we can assume the Standard Code Table is the first
63        */
64       codeTables = new LinkedHashMap<>();
65       loadAmbiguityCodes(AMBIGUITY_CODES_FILE);
66       loadCodes(RESOURCE_FILE);
67     }
68   };
69
70   /**
71    * Returns the singleton instance of this class
72    * 
73    * @return
74    */
75   public static GeneticCodes getInstance()
76   {
77     return instance;
78   }
79
80   /**
81    * Returns the known code tables, in order of loading.
82    * 
83    * @return
84    */
85   public Iterable<GeneticCodeI> getCodeTables()
86   {
87     return codeTables.values();
88   }
89
90   /**
91    * Answers the code table with the given id
92    * 
93    * @param id
94    * @return
95    */
96   public GeneticCodeI getCodeTable(String id)
97   {
98     return codeTables.get(id);
99   }
100
101   /**
102    * A convenience method that returns the standard code table (table 1). As
103    * implemented, this has to be the first table defined in the data file.
104    * 
105    * @return
106    */
107   public GeneticCodeI getStandardCodeTable()
108   {
109     return codeTables.values().iterator().next();
110   }
111
112   /**
113    * Loads the code tables from a data file
114    */
115   protected void loadCodes(String fileName)
116   {
117     try
118     {
119       InputStream is = getClass().getResourceAsStream(fileName);
120       BufferedReader dataIn = new BufferedReader(new InputStreamReader(is));
121
122       /*
123        * skip comments and start of table
124        */
125       String line = "";
126       while (line != null && !line.startsWith("Genetic-code-table"))
127       {
128         line = readLine(dataIn);
129       }
130       line = readLine(dataIn);
131
132       while (line.startsWith("{"))
133       {
134         line = loadOneTable(dataIn);
135       }
136     } catch (IOException | NullPointerException e)
137     {
138       Cache.log.error(
139               "Error reading genetic codes data file: "
140               + e.getMessage());
141     }
142   }
143
144   /**
145    * Reads and saves Nucleotide ambiguity codes from a data file. The file may
146    * include comment lines (starting with #), a header 'DNA', and one line per
147    * ambiguity code, for example:
148    * <p>
149    * R&lt;tab&gt;AG
150    * <p>
151    * means that R is an ambiguity code meaning "A or G"
152    * 
153    * @param fileName
154    */
155   protected void loadAmbiguityCodes(String fileName)
156   {
157     try
158     {
159       InputStream is = getClass().getResourceAsStream(fileName);
160       BufferedReader dataIn = new BufferedReader(new InputStreamReader(is));
161       String line = "";
162       while (line != null)
163       {
164         line = readLine(dataIn);
165         if (line != null && !"DNA".equals(line.toUpperCase()))
166         {
167           String[] tokens = line.split("\\t");
168           ambiguityCodes.put(tokens[0].toUpperCase(),
169                   tokens[1].toUpperCase());
170         }
171       }
172     } catch (IOException e)
173     {
174       Cache.log.error(
175               "Error reading nucleotide ambiguity codes data file: "
176                       + e.getMessage());
177     }
178   }
179
180   /**
181    * Reads up to and returns the next non-comment line, trimmed. Comment lines
182    * start with a #. Returns null at end of file.
183    * 
184    * @param dataIn
185    * @return
186    * @throws IOException
187    */
188   protected String readLine(BufferedReader dataIn) throws IOException
189   {
190     String line = dataIn.readLine();
191     while (line != null && line.startsWith("#"))
192     {
193       line = readLine(dataIn);
194     }
195     return line == null ? null : line.trim();
196   }
197
198   /**
199    * Reads the lines of the data file describing one translation table, and
200    * creates and stores an instance of GeneticCodeI. Returns the '{' line
201    * starting the next table, or the '}' line at end of all tables. Data format
202    * is
203    * 
204    * <pre>
205    * {
206    *   name "Vertebrate Mitochondrial" ,
207    *   name "SGC1" ,
208    *   id 2 ,
209    *   ncbieaa  "FFLLSSSSYY**CCWWLLLLPPPPHHQQRRRRIIMMTTTTNNKKSS**VVVVAAAADDEEGGGG",
210    *   sncbieaa "----------**--------------------MMMM----------**---M------------"
211    *   -- Base1  TTTTTTTTTTTTTTTTCCCCCCCCCCCCCCCCAAAAAAAAAAAAAAAAGGGGGGGGGGGGGGGG
212    *   -- Base2  TTTTCCCCAAAAGGGGTTTTCCCCAAAAGGGGTTTTCCCCAAAAGGGGTTTTCCCCAAAAGGGG
213    *   -- Base3  TCAGTCAGTCAGTCAGTCAGTCAGTCAGTCAGTCAGTCAGTCAGTCAGTCAGTCAGTCAGTCAG
214    * },
215    * </pre>
216    * 
217    * of which we parse the first name, the id, and the ncbieaa translations for
218    * codons as ordered by the Base1/2/3 lines. Note Base1/2/3 are included for
219    * readability and are in a fixed order, these are not parsed. The sncbieaa
220    * line marks alternative start codons, these are not parsed.
221    * 
222    * @param dataIn
223    * @return
224    * @throws IOException
225    */
226   protected String loadOneTable(BufferedReader dataIn) throws IOException
227   {
228     String name = null;
229     String id = null;
230     Map<String, String> codons = new HashMap<>();
231
232     String line = readLine(dataIn);
233
234     while (line != null && !line.startsWith("}"))
235     {
236       if (line.startsWith("name") && name == null)
237       {
238         name = line.substring(line.indexOf(QUOTE) + 1,
239                 line.lastIndexOf(QUOTE));
240       }
241       else if (line.startsWith("id"))
242       {
243         id = new StringTokenizer(line.substring(2)).nextToken();
244       }
245       else if (line.startsWith("ncbieaa"))
246       {
247         String aminos = line.substring(line.indexOf(QUOTE) + 1,
248                 line.lastIndexOf(QUOTE));
249         if (aminos.length() != NUCS_COUNT_CUBED) // 4 * 4 * 4 combinations
250         {
251           Cache.log.error("wrong data length in code table: " + line);
252         }
253         else
254         {
255           for (int i = 0; i < aminos.length(); i++)
256           {
257             String peptide = String.valueOf(aminos.charAt(i));
258             char codon1 = NUCS.charAt(i / NUCS_COUNT_SQUARED);
259             char codon2 = NUCS
260                     .charAt((i % NUCS_COUNT_SQUARED) / NUCS_COUNT);
261             char codon3 = NUCS.charAt(i % NUCS_COUNT);
262             String codon = new String(
263                     new char[]
264                     { codon1, codon2, codon3 });
265             codons.put(codon, peptide);
266           }
267         }
268       }
269       line = readLine(dataIn);
270     }
271
272     registerCodeTable(id, name, codons);
273     return readLine(dataIn);
274   }
275
276   /**
277    * Constructs and registers a GeneticCodeI instance with the codon
278    * translations as defined in the data file. For all instances except the
279    * first, any undeclared translations default to those in the standard code
280    * table.
281    * 
282    * @param id
283    * @param name
284    * @param codons
285    */
286   protected void registerCodeTable(final String id, final String name,
287           final Map<String, String> codons)
288   {
289     codeTables.put(id, new GeneticCodeI()
290     {
291       /*
292        * map of ambiguous codons to their 'product'
293        * (null if not all possible translations match)
294        */
295       Map<String, String> ambiguous = new HashMap<>();
296
297       @Override
298       public String translateCanonical(String codon)
299       {
300         return codons.get(codon.toUpperCase());
301       }
302
303       @Override
304       public String translate(String codon)
305       {
306         String upper = codon.toUpperCase();
307         String peptide = translateCanonical(upper);
308
309         /*
310          * if still not translated, check for ambiguity codes
311          */
312         if (peptide == null)
313         {
314           peptide = getAmbiguousTranslation(upper, ambiguous, this);
315         }
316         return peptide;
317       }
318
319       @Override
320       public String getId()
321       {
322         return id;
323       }
324
325       @Override
326       public String getName()
327       {
328         return name;
329       }
330     });
331   }
332
333   /**
334    * Computes all possible translations of a codon including one or more
335    * ambiguity codes, and stores and returns the result (null if not all
336    * translations match). If the codon includes no ambiguity codes, simply
337    * returns null.
338    * 
339    * @param codon
340    * @param ambiguous
341    * @param codeTable
342    * @return
343    */
344   protected String getAmbiguousTranslation(String codon,
345           Map<String, String> ambiguous, GeneticCodeI codeTable)
346   {
347     if (codon.length() != CODON_LENGTH)
348     {
349       return null;
350     }
351
352     boolean isAmbiguous = false;
353
354     char[][] expanded = new char[CODON_LENGTH][];
355     for (int i = 0; i < CODON_LENGTH; i++)
356     {
357       String base = String.valueOf(codon.charAt(i));
358       if (ambiguityCodes.containsKey(base))
359       {
360         isAmbiguous = true;
361         base = ambiguityCodes.get(base);
362       }
363       expanded[i] = base.toCharArray();
364     }
365
366     if (!isAmbiguous)
367     {
368       // no ambiguity code involved here
369       return null;
370     }
371
372     /*
373      * generate and translate all permutations of the ambiguous codon
374      * only return the translation if they all agree, else null
375      */
376     String peptide = null;
377     for (char c1 : expanded[0])
378     {
379       for (char c2 : expanded[1])
380       {
381         for (char c3 : expanded[2])
382         {
383           char[] cdn = new char[] { c1, c2, c3 };
384           String possibleCodon = String.valueOf(cdn);
385           String pep = codeTable.translate(possibleCodon);
386           if (pep == null || (peptide != null && !pep.equals(peptide)))
387           {
388             ambiguous.put(codon, null);
389             return null;
390           }
391           peptide = pep;
392         }
393       }
394     }
395
396     /*
397      * all translations of ambiguous codons matched!
398      */
399     ambiguous.put(codon, peptide);
400     return peptide;
401   }
402 }