JAL-3949 - refactor logging from jalview.bin.Cache to jalview.bin.Console
[jalview.git] / src / jalview / io / EMBLLikeFlatFile.java
1 package jalview.io;
2
3 import java.io.IOException;
4 import java.text.ParseException;
5 import java.util.ArrayList;
6 import java.util.Arrays;
7 import java.util.HashMap;
8 import java.util.Hashtable;
9 import java.util.List;
10 import java.util.Locale;
11 import java.util.Map;
12 import java.util.Map.Entry;
13 import java.util.TreeMap;
14
15 import jalview.bin.Console;
16 import jalview.datamodel.DBRefEntry;
17 import jalview.datamodel.DBRefSource;
18 import jalview.datamodel.FeatureProperties;
19 import jalview.datamodel.Mapping;
20 import jalview.datamodel.Sequence;
21 import jalview.datamodel.SequenceFeature;
22 import jalview.datamodel.SequenceI;
23 import jalview.util.DBRefUtils;
24 import jalview.util.DnaUtils;
25 import jalview.util.MapList;
26 import jalview.util.MappingUtils;
27
28 /**
29  * A base class to support parsing of GenBank, EMBL or DDBJ flat file format
30  * data. Example files (rather than formal specifications) are provided at
31  * 
32  * <pre>
33  * https://ena-docs.readthedocs.io/en/latest/submit/fileprep/flat-file-example.html
34  * https://www.ncbi.nlm.nih.gov/Sitemap/samplerecord.html
35  * </pre>
36  * 
37  * or to compare the same entry, see
38  * 
39  * <pre>
40  * https://www.ebi.ac.uk/ena/browser/api/embl/X81322.1
41  * https://www.ncbi.nlm.nih.gov/nuccore/X81322.1
42  * </pre>
43  * 
44  * The feature table part of the file has a common definition, only the start of
45  * each line is formatted differently in GenBank and EMBL. See
46  * http://www.insdc.org/files/feature_table.html#7.1.
47  */
48 public abstract class EMBLLikeFlatFile extends AlignFile
49 {
50   protected static final String LOCATION = "location";
51
52   protected static final String QUOTE = "\"";
53
54   protected static final String DOUBLED_QUOTE = QUOTE + QUOTE;
55
56   protected static final String WHITESPACE = "\\s+";
57
58   /**
59    * Removes leading or trailing double quotes (") unless doubled, and changes
60    * any 'escaped' (doubled) double quotes to single characters. As per the
61    * Feature Table specification for Qualifiers, Free Text.
62    * 
63    * @param value
64    * @return
65    */
66   protected static String removeQuotes(String value)
67   {
68     if (value == null)
69     {
70       return null;
71     }
72     if (value.startsWith(QUOTE) && !value.startsWith(DOUBLED_QUOTE))
73     {
74       value = value.substring(1);
75     }
76     if (value.endsWith(QUOTE) && !value.endsWith(DOUBLED_QUOTE))
77     {
78       value = value.substring(0, value.length() - 1);
79     }
80     value = value.replace(DOUBLED_QUOTE, QUOTE);
81     return value;
82   }
83
84   /**
85    * Truncates (if necessary) the exon intervals to match 3 times the length of
86    * the protein(including truncation for stop codon included in exon)
87    * 
88    * @param proteinLength
89    * @param exon
90    *          an array of [start, end, start, end...] intervals
91    * @return the same array (if unchanged) or a truncated copy
92    */
93   protected static int[] adjustForProteinLength(int proteinLength,
94           int[] exon)
95   {
96     if (proteinLength <= 0 || exon == null)
97     {
98       return exon;
99     }
100     int expectedCdsLength = proteinLength * 3;
101     int exonLength = MappingUtils.getLength(Arrays.asList(exon));
102
103     /*
104      * if exon length matches protein, or is shorter, then leave it unchanged
105      */
106     if (expectedCdsLength >= exonLength)
107     {
108       return exon;
109     }
110
111     int origxon[];
112     int sxpos = -1;
113     int endxon = 0;
114     origxon = new int[exon.length];
115     System.arraycopy(exon, 0, origxon, 0, exon.length);
116     int cdspos = 0;
117     for (int x = 0; x < exon.length; x += 2)
118     {
119       cdspos += Math.abs(exon[x + 1] - exon[x]) + 1;
120       if (expectedCdsLength <= cdspos)
121       {
122         // advanced beyond last codon.
123         sxpos = x;
124         if (expectedCdsLength != cdspos)
125         {
126           // System.err
127           // .println("Truncating final exon interval on region by "
128           // + (cdspos - cdslength));
129         }
130
131         /*
132          * shrink the final exon - reduce end position if forward
133          * strand, increase it if reverse
134          */
135         if (exon[x + 1] >= exon[x])
136         {
137           endxon = exon[x + 1] - cdspos + expectedCdsLength;
138         }
139         else
140         {
141           endxon = exon[x + 1] + cdspos - expectedCdsLength;
142         }
143         break;
144       }
145     }
146
147     if (sxpos != -1)
148     {
149       // and trim the exon interval set if necessary
150       int[] nxon = new int[sxpos + 2];
151       System.arraycopy(exon, 0, nxon, 0, sxpos + 2);
152       nxon[sxpos + 1] = endxon; // update the end boundary for the new exon
153                                 // set
154       exon = nxon;
155     }
156     return exon;
157   }
158
159   /*
160    * when true, interpret the mol_type 'source' feature attribute
161    * and generate an RNA sequence from the DNA record
162    */
163   protected boolean produceRna=true;
164     
165
166   /*
167    * values parsed from the data file
168    */
169   protected String sourceDb;
170
171   protected String accession;
172
173   protected String version;
174
175   protected String description;
176
177   protected int length = 128;
178
179   protected List<DBRefEntry> dbrefs;
180
181   protected boolean sequenceStringIsRNA=false;
182
183   protected String sequenceString;
184
185   protected Map<String, CdsData> cds;
186
187   /**
188    * Constructor
189    * 
190    * @param fp
191    * @param sourceId
192    * @throws IOException
193    */
194   public EMBLLikeFlatFile(FileParse fp, String sourceId) throws IOException
195   {
196     super(false, fp); // don't parse immediately
197     this.sourceDb = sourceId;
198     dbrefs = new ArrayList<>();
199
200     /*
201      * using TreeMap gives CDS sequences in alphabetical, so readable, order
202      */
203     cds = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
204     
205     parse();
206   }
207
208   /**
209    * process attributes for 'source' until the next FT feature entry
210    * only interested in 'mol_type'
211    * @param tokens
212    * @return
213    * @throws IOException
214    */
215   private String parseSourceQualifiers(String[] tokens) throws IOException
216   {
217     if (!"source".equals(tokens[0]))
218     {
219       throw (new RuntimeException("Not given a 'source' qualifier line"));
220     }
221     // search for mol_type attribute
222
223     StringBuilder sb = new StringBuilder().append(tokens[1]); // extent of
224                                                               // sequence
225
226     String line = parseFeatureQualifier(sb, false);
227     while (line != null)
228     {
229       if (!line.startsWith("FT    ")) // four spaces, end of this feature table
230                                       // entry
231       {
232         return line;
233       }
234
235       // case sensitive ?
236       int p = line.indexOf("\\mol_type");
237       int qs = line.indexOf("\"", p);
238       int qe = line.indexOf("\"", qs + 1);
239       String qualifier=line.substring(qs,qe).toLowerCase(Locale.ROOT);
240       if (qualifier.indexOf("rna") > -1)
241       {
242         sequenceStringIsRNA = true;
243       }
244       if (qualifier.indexOf("dna") > -1)
245       {
246         sequenceStringIsRNA = false;
247       }
248       line=parseFeatureQualifier(sb, false);
249     }
250     return line;
251   }
252
253    
254   /**
255    * Parses one (GenBank or EMBL format) CDS feature, saves the parsed data, and
256    * returns the next line
257    * 
258    * @param location
259    * @return
260    * @throws IOException
261    */
262   protected String parseCDSFeature(String location) throws IOException
263   {
264     String line;
265
266     /*
267      * parse location, which can be over >1 line e.g. EAW51554
268      */
269     CdsData data = new CdsData();
270     StringBuilder sb = new StringBuilder().append(location);
271     line = parseFeatureQualifier(sb, false);
272     data.cdsLocation = sb.toString();
273
274     while (line != null)
275     {
276       if (!isFeatureContinuationLine(line))
277       {
278         // e.g. start of next feature "FT source..."
279         break;
280       }
281
282       /*
283        * extract qualifier, e.g. FT    /protein_id="CAA37824.1"
284        * - the value may extend over more than one line
285        * - if the value has enclosing quotes, these are removed
286        * - escaped double quotes ("") are reduced to a single character
287        */
288       int slashPos = line.indexOf('/');
289       if (slashPos == -1)
290       {
291         Console.error("Unexpected EMBL line ignored: " + line);
292         line = nextLine();
293         continue;
294       }
295       int eqPos = line.indexOf('=', slashPos + 1);
296       if (eqPos == -1)
297       {
298         // can happen, e.g. /ribosomal_slippage
299         line = nextLine();
300         continue;
301       }
302       String qualifier = line.substring(slashPos + 1, eqPos);
303       String value = line.substring(eqPos + 1);
304       value = removeQuotes(value);
305       sb = new StringBuilder().append(value);
306       boolean asText = !"translation".equals(qualifier);
307       line = parseFeatureQualifier(sb, asText);
308       String featureValue = sb.toString();
309
310       if ("protein_id".equals(qualifier))
311       {
312         data.proteinId = featureValue;
313       }
314       else if ("codon_start".equals(qualifier))
315       {
316         try
317         {
318           data.codonStart = Integer.parseInt(featureValue.trim());
319         } catch (NumberFormatException e)
320         {
321           Console.error("Invalid codon_start in XML for " + this.accession
322                   + ": " + e.getMessage());
323         }
324       }
325       else if ("db_xref".equals(qualifier))
326       {
327         String[] parts = featureValue.split(":");
328         if (parts.length == 2)
329         {
330           String db = parts[0].trim();
331           db = DBRefUtils.getCanonicalName(db);
332           DBRefEntry dbref = new DBRefEntry(db, "0", parts[1].trim());
333           data.xrefs.add(dbref);
334         }
335       }
336       else if ("product".equals(qualifier))
337       {
338         data.proteinName = featureValue;
339       }
340       else if ("translation".equals(qualifier))
341       {
342         data.translation = featureValue;
343       }
344       else if (!"".equals(featureValue))
345       {
346         // throw anything else into the additional properties hash
347         data.cdsProps.put(qualifier, featureValue);
348       }
349     }
350
351     if (data.proteinId != null)
352     {
353       this.cds.put(data.proteinId, data);
354     }
355     else
356     {
357       Console.error("Ignoring CDS feature with no protein_id for "
358               + sourceDb + ":" + accession);
359     }
360
361     return line;
362   }
363
364   protected abstract boolean isFeatureContinuationLine(String line);
365
366   /**
367    * Output (print) is not (yet) implemented for flat file format
368    */
369   @Override
370   public String print(SequenceI[] seqs, boolean jvsuffix)
371   {
372     return null;
373   }
374
375   /**
376    * Constructs and saves the sequence from parsed components
377    */
378   protected void buildSequence()
379   {
380     if (this.accession == null || this.sequenceString == null)
381     {
382       Console.error("Failed to parse data from EMBL");
383       return;
384     }
385
386     String name = this.accession;
387     if (this.sourceDb != null)
388     {
389       name = this.sourceDb + "|" + name;
390     }
391
392     if (produceRna && sequenceStringIsRNA)
393     {
394       sequenceString = sequenceString.replace('T', 'U').replace('t', 'u');
395     }
396
397     SequenceI seq = new Sequence(name, this.sequenceString);
398     seq.setDescription(this.description);
399
400     /*
401      * add a DBRef to itself
402      */
403     DBRefEntry selfRef = new DBRefEntry(sourceDb, version, accession);
404     int[] startEnd = new int[] { 1, seq.getLength() };
405     selfRef.setMap(new Mapping(null, startEnd, startEnd, 1, 1));
406     seq.addDBRef(selfRef);
407
408     for (DBRefEntry dbref : this.dbrefs)
409     {
410       seq.addDBRef(dbref);
411     }
412
413     processCDSFeatures(seq);
414
415     seq.deriveSequence();
416
417     addSequence(seq);
418   }
419
420   /**
421    * Process the CDS features, including generation of cross-references and
422    * mappings to the protein products (translation)
423    * 
424    * @param seq
425    */
426   protected void processCDSFeatures(SequenceI seq)
427   {
428     /*
429      * record protein products found to avoid duplication i.e. >1 CDS with 
430      * the same /protein_id [though not sure I can find an example of this]
431      */
432     Map<String, SequenceI> proteins = new HashMap<>();
433     for (CdsData data : cds.values())
434     {
435       processCDSFeature(seq, data, proteins);
436     }
437   }
438
439   /**
440    * Processes data for one parsed CDS feature to
441    * <ul>
442    * <li>create a protein product sequence for the translation</li>
443    * <li>create a cross-reference to protein with mapping from dna</li>
444    * <li>add a CDS feature to the sequence for each CDS start-end range</li>
445    * <li>add any CDS dbrefs to the sequence and to the protein product</li>
446    * </ul>
447    * 
448    * @param SequenceI
449    *          dna
450    * @param proteins
451    *          map of protein products so far derived from CDS data
452    */
453   void processCDSFeature(SequenceI dna, CdsData data,
454           Map<String, SequenceI> proteins)
455   {
456     /*
457      * parse location into a list of [start, end, start, end] positions
458      */
459     int[] exons = getCdsRanges(this.accession, data.cdsLocation);
460
461     MapList maplist = buildMappingToProtein(dna, exons, data);
462
463     int exonNumber = 0;
464
465     for (int xint = 0; exons != null && xint < exons.length - 1; xint += 2)
466     {
467       int exonStart = exons[xint];
468       int exonEnd = exons[xint + 1];
469       int begin = Math.min(exonStart, exonEnd);
470       int end = Math.max(exonStart, exonEnd);
471       exonNumber++;
472       String desc = String.format("Exon %d for protein EMBLCDS:%s",
473               exonNumber, data.proteinId);
474
475       SequenceFeature sf = new SequenceFeature("CDS", desc, begin, end,
476               this.sourceDb);
477       for (Entry<String, String> val : data.cdsProps.entrySet())
478       {
479         sf.setValue(val.getKey(), val.getValue());
480       }
481
482       sf.setEnaLocation(data.cdsLocation);
483       boolean forwardStrand = exonStart <= exonEnd;
484       sf.setStrand(forwardStrand ? "+" : "-");
485       sf.setPhase(String.valueOf(data.codonStart - 1));
486       sf.setValue(FeatureProperties.EXONPOS, exonNumber);
487       sf.setValue(FeatureProperties.EXONPRODUCT, data.proteinName);
488
489       dna.addSequenceFeature(sf);
490     }
491
492     boolean hasUniprotDbref = false;
493     for (DBRefEntry xref : data.xrefs)
494     {
495       dna.addDBRef(xref);
496       if (xref.getSource().equals(DBRefSource.UNIPROT))
497       {
498         /*
499          * construct (or find) the sequence for (data.protein_id, data.translation)
500          */
501         SequenceI protein = buildProteinProduct(dna, xref, data, proteins);
502         Mapping map = new Mapping(protein, maplist);
503         map.setMappedFromId(data.proteinId);
504         xref.setMap(map);
505
506         /*
507          * add DBRefs with mappings from dna to protein and the inverse
508          */
509         DBRefEntry db1 = new DBRefEntry(sourceDb, version, accession);
510         db1.setMap(new Mapping(dna, maplist.getInverse()));
511         protein.addDBRef(db1);
512
513         hasUniprotDbref = true;
514       }
515     }
516
517     /*
518      * if we have a product (translation) but no explicit Uniprot dbref
519      * (example: EMBL M19487 protein_id AAB02592.1)
520      * then construct mappings to an assumed EMBLCDSPROTEIN accession
521      */
522     if (!hasUniprotDbref)
523     {
524       SequenceI protein = proteins.get(data.proteinId);
525       if (protein == null)
526       {
527         protein = new Sequence(data.proteinId, data.translation);
528         protein.setDescription(data.proteinName);
529         proteins.put(data.proteinId, protein);
530       }
531       // assuming CDSPROTEIN sequence version = dna version (?!)
532       DBRefEntry db1 = new DBRefEntry(DBRefSource.EMBLCDSProduct,
533               this.version, data.proteinId);
534       protein.addDBRef(db1);
535
536       DBRefEntry dnaToEmblProteinRef = new DBRefEntry(
537               DBRefSource.EMBLCDSProduct, this.version, data.proteinId);
538       Mapping map = new Mapping(protein, maplist);
539       map.setMappedFromId(data.proteinId);
540       dnaToEmblProteinRef.setMap(map);
541       dna.addDBRef(dnaToEmblProteinRef);
542     }
543
544     /*
545      * comment brought forward from EmblXmlSource, lines 447-451:
546      * TODO: if retrieved from EMBLCDS, add a DBRef back to the parent EMBL
547      * sequence with the exon  map; if given a dataset reference, search
548      * dataset for parent EMBL sequence if it exists and set its map;
549      * make a new feature annotating the coding contig
550      */
551   }
552
553   /**
554    * Computes a mapping from CDS positions in DNA sequence to protein product
555    * positions, with allowance for stop codon or incomplete start codon
556    * 
557    * @param dna
558    * @param exons
559    * @param data
560    * @return
561    */
562   MapList buildMappingToProtein(final SequenceI dna, final int[] exons,
563           final CdsData data)
564   {
565     MapList dnaToProteinMapping = null;
566     int peptideLength = data.translation.length();
567
568     int[] proteinRange = new int[] { 1, peptideLength };
569     if (exons != null && exons.length > 0)
570     {
571       /*
572        * We were able to parse 'location'; do a final 
573        * product length truncation check
574        */
575       int[] cdsRanges = adjustForProteinLength(peptideLength, exons);
576       dnaToProteinMapping = new MapList(cdsRanges, proteinRange, 3, 1);
577     }
578     else
579     {
580       /*
581        * workaround until we handle all 'location' formats fully
582        * e.g. X53828.1:60..1058 or <123..>289
583        */
584       Console.error(String.format(
585               "Implementation Notice: EMBLCDS location '%s'not properly supported yet"
586                       + " - Making up the CDNA region of (%s:%s)... may be incorrect",
587               data.cdsLocation, sourceDb, this.accession));
588
589       int completeCodonsLength = 1 - data.codonStart + dna.getLength();
590       int mappedDnaEnd = dna.getEnd();
591       if (peptideLength * 3 == completeCodonsLength)
592       {
593         // this might occur for CDS sequences where no features are marked
594         Console.warn("Assuming no stop codon at end of cDNA fragment");
595         mappedDnaEnd = dna.getEnd();
596       }
597       else if ((peptideLength + 1) * 3 == completeCodonsLength)
598       {
599         Console.warn("Assuming stop codon at end of cDNA fragment");
600         mappedDnaEnd = dna.getEnd() - 3;
601       }
602
603       if (mappedDnaEnd != -1)
604       {
605         int[] cdsRanges = new int[] {
606             dna.getStart() + (data.codonStart - 1), mappedDnaEnd };
607         dnaToProteinMapping = new MapList(cdsRanges, proteinRange, 3, 1);
608       }
609     }
610
611     return dnaToProteinMapping;
612   }
613
614   /**
615    * Constructs a sequence for the protein product for the CDS data (if there is
616    * one), and dbrefs with mappings from CDS to protein and the reverse
617    * 
618    * @param dna
619    * @param xref
620    * @param data
621    * @param proteins
622    * @return
623    */
624   SequenceI buildProteinProduct(SequenceI dna, DBRefEntry xref,
625           CdsData data, Map<String, SequenceI> proteins)
626   {
627     /*
628      * check we have some data to work with
629      */
630     if (data.proteinId == null || data.translation == null)
631     {
632       return null;
633     }
634
635     /*
636      * Construct the protein sequence (if not already seen)
637      */
638     String proteinSeqName = xref.getSource() + "|" + xref.getAccessionId();
639     SequenceI protein = proteins.get(proteinSeqName);
640     if (protein == null)
641     {
642       protein = new Sequence(proteinSeqName, data.translation, 1,
643               data.translation.length());
644       protein.setDescription(data.proteinName != null ? data.proteinName
645               : "Protein Product from " + sourceDb);
646       proteins.put(proteinSeqName, protein);
647     }
648
649     return protein;
650   }
651
652   /**
653    * Returns the CDS location as a single array of [start, end, start, end...]
654    * positions. If on the reverse strand, these will be in descending order.
655    * 
656    * @param accession
657    * @param location
658    * @return
659    */
660   protected int[] getCdsRanges(String accession, String location)
661   {
662     if (location == null)
663     {
664       return new int[] {};
665     }
666
667     try
668     {
669       List<int[]> ranges = DnaUtils.parseLocation(location);
670       return MappingUtils.rangeListToArray(ranges);
671     } catch (ParseException e)
672     {
673       Console.warn(
674               String.format("Not parsing inexact CDS location %s in ENA %s",
675                       location, accession));
676       return new int[] {};
677     }
678   }
679
680   /**
681    * Reads the value of a feature (FT) qualifier from one or more lines of the
682    * file, and returns the next line after that. Values are appended to the
683    * string buffer, which should be already primed with the value read from the
684    * first line for the qualifier (with any leading double quote removed).
685    * Enclosing double quotes are removed, and escaped (repeated) double quotes
686    * reduced to one only. For example for
687    * 
688    * <pre>
689    * FT      /note="gene_id=hCG28070.3 
690    * FT      ""foobar"" isoform=CRA_b"
691    * the returned value is
692    * gene_id=hCG28070.3 "foobar" isoform=CRA_b
693    * </pre>
694    * 
695    * Note the side-effect of this method, to advance data reading to the next
696    * line after the feature qualifier (which could be another qualifier, a
697    * different feature, a non-feature line, or null at end of file).
698    * 
699    * @param sb
700    *          a string buffer primed with the first line of the value
701    * @param asText
702    * @return
703    * @throws IOException
704    */
705   String parseFeatureQualifier(StringBuilder sb, boolean asText)
706           throws IOException
707   {
708     String line;
709     while ((line = nextLine()) != null)
710     {
711       if (!isFeatureContinuationLine(line))
712       {
713         break; // reached next feature or other input line
714       }
715       String[] tokens = line.split(WHITESPACE);
716       if (tokens.length < 2)
717       {
718         Console.error("Ignoring bad EMBL line for " + this.accession
719                 + ": " + line);
720         break;
721       }
722       if (tokens[1].startsWith("/"))
723       {
724         break; // next feature qualifier
725       }
726
727       /*
728        * if text (e.g. /product), add a word separator for a new line,
729        * else (e.g. /translation) don't
730        */
731       if (asText)
732       {
733         sb.append(" ");
734       }
735
736       /*
737        * remove trailing " and unescape doubled ""
738        */
739       String data = removeQuotes(tokens[1]);
740       sb.append(data);
741     }
742
743     return line;
744   }
745
746   /**
747    * Reads and saves the sequence, read from the lines following the ORIGIN
748    * (GenBank) or SQ (EMBL) line. Whitespace and position counters are
749    * discarded. Returns the next line following the sequence data (the next line
750    * that doesn't start with whitespace).
751    * 
752    * @throws IOException
753    */
754   protected String parseSequence() throws IOException
755   {
756     StringBuilder sb = new StringBuilder(this.length);
757     String line = nextLine();
758     while (line != null && line.startsWith(" "))
759     {
760       line = line.trim();
761       String[] blocks = line.split(WHITESPACE);
762
763       /*
764        * the first or last block on each line might be a position count - omit
765        */
766       for (int i = 0; i < blocks.length; i++)
767       {
768         try
769         {
770           Long.parseLong(blocks[i]);
771           // position counter - ignore it
772         } catch (NumberFormatException e)
773         {
774           // sequence data - append it
775           sb.append(blocks[i]);
776         }
777       }
778       line = nextLine();
779     }
780     this.sequenceString = sb.toString();
781
782     return line;
783   }
784
785   /**
786    * Processes a feature line. If it declares a feature type of interest
787    * (currently, only CDS is processed), processes all of the associated lines
788    * (feature qualifiers), and returns the next line after that, otherwise
789    * simply returns the next line.
790    * 
791    * @param line
792    *          the first line for the feature (with initial FT omitted for EMBL
793    *          format)
794    * @return
795    * @throws IOException
796    */
797   protected String parseFeature(String line) throws IOException
798   {
799     String[] tokens = line.trim().split(WHITESPACE);
800     if (tokens.length < 2 || (!"CDS".equals(tokens[0]) && (!"source".equals(tokens[0]))))
801     {
802       return nextLine();
803     }
804     if (tokens[0].equals("source"))
805     {
806       return parseSourceQualifiers(tokens);
807     }
808     return parseCDSFeature(tokens[1]);
809   }
810 }
811
812 /**
813  * A data bean class to hold values parsed from one CDS Feature
814  */
815 class CdsData
816 {
817   String translation; // from /translation qualifier
818
819   String cdsLocation; // the raw value e.g. join(1..1234,2012..2837)
820
821   int codonStart = 1; // from /codon_start qualifier
822
823   String proteinName; // from /product qualifier; used for protein description
824
825   String proteinId; // from /protein_id qualifier
826
827   List<DBRefEntry> xrefs = new ArrayList<>(); // from /db_xref qualifiers
828
829   Map<String, String> cdsProps = new Hashtable<>(); // other qualifiers
830 }