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