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;
11 import java.util.Map.Entry;
12 import java.util.TreeMap;
14 import jalview.bin.Cache;
15 import jalview.datamodel.DBRefEntry;
16 import jalview.datamodel.DBRefSource;
17 import jalview.datamodel.FeatureProperties;
18 import jalview.datamodel.Mapping;
19 import jalview.datamodel.Sequence;
20 import jalview.datamodel.SequenceFeature;
21 import jalview.datamodel.SequenceI;
22 import jalview.util.DBRefUtils;
23 import jalview.util.DnaUtils;
24 import jalview.util.MapList;
25 import jalview.util.MappingUtils;
28 * A base class to support parsing of GenBank, EMBL or DDBJ flat file format
29 * data. Example files (rather than formal specifications) are provided at
32 * https://ena-docs.readthedocs.io/en/latest/submit/fileprep/flat-file-example.html
33 * https://www.ncbi.nlm.nih.gov/Sitemap/samplerecord.html
36 * or to compare the same entry, see
39 * https://www.ebi.ac.uk/ena/browser/api/embl/X81322.1
40 * https://www.ncbi.nlm.nih.gov/nuccore/X81322.1
43 * The feature table part of the file has a common definition, only the start of
44 * each line is formatted differently in GenBank and EMBL. See
45 * http://www.insdc.org/files/feature_table.html#7.1.
47 public abstract class FlatFile extends AlignFile
49 protected static final String LOCATION = "location";
51 protected static final String QUOTE = "\"";
53 protected static final String DOUBLED_QUOTE = QUOTE + QUOTE;
55 protected static final String WHITESPACE = "\\s+";
58 * Removes leading or trailing double quotes (") unless doubled, and changes
59 * any 'escaped' (doubled) double quotes to single characters. As per the
60 * Feature Table specification for Qualifiers, Free Text.
65 protected static String removeQuotes(String value)
71 if (value.startsWith(QUOTE) && !value.startsWith(DOUBLED_QUOTE))
73 value = value.substring(1);
75 if (value.endsWith(QUOTE) && !value.endsWith(DOUBLED_QUOTE))
77 value = value.substring(0, value.length() - 1);
79 value = value.replace(DOUBLED_QUOTE, QUOTE);
84 * Truncates (if necessary) the exon intervals to match 3 times the length of
85 * the protein; also accepts 3 bases longer (for stop codon not included in
88 * @param proteinLength
90 * an array of [start, end, start, end...] intervals
91 * @return the same array (if unchanged) or a truncated copy
93 protected static int[] adjustForProteinLength(int proteinLength,
96 if (proteinLength <= 0 || exon == null)
100 int expectedCdsLength = proteinLength * 3;
101 int exonLength = MappingUtils.getLength(Arrays.asList(exon));
104 * if exon length matches protein, or is shorter, or longer by the
105 * length of a stop codon (3 bases), then leave it unchanged
107 if (expectedCdsLength >= exonLength
108 || expectedCdsLength == exonLength - 3)
116 origxon = new int[exon.length];
117 System.arraycopy(exon, 0, origxon, 0, exon.length);
119 for (int x = 0; x < exon.length; x += 2)
121 cdspos += Math.abs(exon[x + 1] - exon[x]) + 1;
122 if (expectedCdsLength <= cdspos)
124 // advanced beyond last codon.
126 if (expectedCdsLength != cdspos)
129 // .println("Truncating final exon interval on region by "
130 // + (cdspos - cdslength));
134 * shrink the final exon - reduce end position if forward
135 * strand, increase it if reverse
137 if (exon[x + 1] >= exon[x])
139 endxon = exon[x + 1] - cdspos + expectedCdsLength;
143 endxon = exon[x + 1] + cdspos - expectedCdsLength;
151 // and trim the exon interval set if necessary
152 int[] nxon = new int[sxpos + 2];
153 System.arraycopy(exon, 0, nxon, 0, sxpos + 2);
154 nxon[sxpos + 1] = endxon; // update the end boundary for the new exon
162 * values parsed from the data file
164 protected String sourceDb;
166 protected String accession;
168 protected String version;
170 protected String description;
172 protected int length = 128;
174 protected List<DBRefEntry> dbrefs;
176 protected String sequenceString;
178 protected Map<String, CdsData> cds;
185 * @throws IOException
187 public FlatFile(FileParse fp, String sourceId) throws IOException
189 super(false, fp); // don't parse immediately
190 this.sourceDb = sourceId;
191 dbrefs = new ArrayList<>();
194 * using TreeMap gives CDS sequences in alphabetical, so readable, order
196 cds = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
202 * Parses one (GenBank or EMBL format) CDS feature, saves the parsed data, and
203 * returns the next line
207 * @throws IOException
209 protected String parseCDSFeature(String location) throws IOException
214 * parse location, which can be over >1 line e.g. EAW51554
216 CdsData data = new CdsData();
217 StringBuilder sb = new StringBuilder().append(location);
218 line = parseFeatureQualifier(sb, LOCATION);
219 data.cdsLocation = sb.toString();
223 if (!isFeatureContinuationLine(line))
225 // e.g. start of next feature "FT source..."
230 * extract qualifier, e.g. FT /protein_id="CAA37824.1"
231 * - the value may extend over more than one line
232 * - if the value has enclosing quotes, these are removed
233 * - escaped double quotes ("") are reduced to a single character
235 int slashPos = line.indexOf('/');
238 Cache.log.error("Unexpected EMBL line ignored: " + line);
242 int eqPos = line.indexOf('=', slashPos + 1);
245 // can happen, e.g. /ribosomal_slippage
249 String qualifier = line.substring(slashPos + 1, eqPos);
250 String value = line.substring(eqPos + 1);
251 value = removeQuotes(value);
252 sb = new StringBuilder().append(value);
253 line = parseFeatureQualifier(sb, qualifier);
254 String featureValue = sb.toString();
256 if ("protein_id".equals(qualifier))
258 data.proteinId = featureValue;
260 else if ("codon_start".equals(qualifier))
264 data.codonStart = Integer.parseInt(featureValue.trim());
265 } catch (NumberFormatException e)
267 Cache.log.error("Invalid codon_start in XML for " + this.accession
268 + ": " + e.getMessage());
271 else if ("db_xref".equals(qualifier))
273 String[] parts = featureValue.split(":");
274 if (parts.length == 2)
276 String db = parts[0].trim();
277 db = DBRefUtils.getCanonicalName(db);
278 DBRefEntry dbref = new DBRefEntry(db, "0", parts[1].trim());
279 data.xrefs.add(dbref);
282 else if ("product".equals(qualifier))
284 data.proteinName = featureValue;
286 else if ("translation".equals(qualifier))
288 data.translation = featureValue;
290 else if (!"".equals(featureValue))
292 // throw anything else into the additional properties hash
293 data.cdsProps.put(qualifier, featureValue);
297 if (data.proteinId != null)
299 this.cds.put(data.proteinId, data);
303 Cache.log.error("Ignoring CDS feature with no protein_id for "
304 + sourceDb + ":" + accession);
310 protected abstract boolean isFeatureContinuationLine(String line);
313 * Output (print) is not (yet) implemented for flat file format
316 public String print(SequenceI[] seqs, boolean jvsuffix)
322 * Constructs and saves the sequence from parsed components
324 protected void buildSequence()
326 if (this.accession == null || this.sequenceString == null)
328 Cache.log.error("Failed to parse data from EMBL");
332 String name = this.accession;
333 if (this.sourceDb != null)
335 name = this.sourceDb + "|" + name;
337 SequenceI seq = new Sequence(name, this.sequenceString);
338 seq.setDescription(this.description);
341 * add a DBRef to itself
343 DBRefEntry selfRef = new DBRefEntry(sourceDb, version, accession);
344 int[] startEnd = new int[] { 1, seq.getLength() };
345 selfRef.setMap(new Mapping(null, startEnd, startEnd, 1, 1));
346 seq.addDBRef(selfRef);
348 for (DBRefEntry dbref : this.dbrefs)
353 processCDSFeatures(seq);
355 seq.deriveSequence();
361 * Process the CDS features, including generation of cross-references and
362 * mappings to the protein products (translation)
366 protected void processCDSFeatures(SequenceI seq)
369 * record protein products found to avoid duplication i.e. >1 CDS with
370 * the same /protein_id [though not sure I can find an example of this]
372 Map<String, SequenceI> proteins = new HashMap<>();
373 for (CdsData data : cds.values())
375 processCDSFeature(seq, data, proteins);
380 * Processes data for one parsed CDS feature to
382 * <li>create a protein product sequence for the translation</li>
383 * <li>create a cross-reference to protein with mapping from dna</li>
384 * <li>add a CDS feature to the sequence for each CDS start-end range</li>
385 * <li>add any CDS dbrefs to the sequence and to the protein product</li>
391 * map of protein products so far derived from CDS data
393 void processCDSFeature(SequenceI dna, CdsData data,
394 Map<String, SequenceI> proteins)
397 * parse location into a list of [start, end, start, end] positions
399 int[] exons = getCdsRanges(this.accession, data.cdsLocation);
401 MapList maplist = buildMappingToProtein(dna, exons, data);
405 for (int xint = 0; exons != null && xint < exons.length - 1; xint += 2)
407 int exonStart = exons[xint];
408 int exonEnd = exons[xint + 1];
409 int begin = Math.min(exonStart, exonEnd);
410 int end = Math.max(exonStart, exonEnd);
412 String desc = String.format("Exon %d for protein EMBLCDS:%s",
413 exonNumber, data.proteinId);
415 SequenceFeature sf = new SequenceFeature("CDS", desc, begin, end,
417 for (Entry<String, String> val : data.cdsProps.entrySet())
419 sf.setValue(val.getKey(), val.getValue());
422 sf.setEnaLocation(data.cdsLocation);
423 boolean forwardStrand = exonStart <= exonEnd;
424 sf.setStrand(forwardStrand ? "+" : "-");
425 sf.setPhase(String.valueOf(data.codonStart - 1));
426 sf.setValue(FeatureProperties.EXONPOS, exonNumber);
427 sf.setValue(FeatureProperties.EXONPRODUCT, data.proteinName);
429 dna.addSequenceFeature(sf);
432 boolean hasUniprotDbref = false;
433 for (DBRefEntry xref : data.xrefs)
436 if (xref.getSource().equals(DBRefSource.UNIPROT))
439 * construct (or find) the sequence for (data.protein_id, data.translation)
441 SequenceI protein = buildProteinProduct(dna, xref, data, proteins);
442 Mapping map = new Mapping(protein, maplist);
443 map.setMappedFromId(data.proteinId);
447 * add DBRefs with mappings from dna to protein and the inverse
449 DBRefEntry db1 = new DBRefEntry(sourceDb, version, accession);
450 db1.setMap(new Mapping(dna, maplist.getInverse()));
451 protein.addDBRef(db1);
453 hasUniprotDbref = true;
458 * if we have a product (translation) but no explicit Uniprot dbref
459 * (example: EMBL M19487 protein_id AAB02592.1)
460 * then construct mappings to an assumed EMBLCDSPROTEIN accession
462 if (!hasUniprotDbref)
464 SequenceI protein = proteins.get(data.proteinId);
467 protein = new Sequence(data.proteinId, data.translation);
468 protein.setDescription(data.proteinName);
469 proteins.put(data.proteinId, protein);
471 // assuming CDSPROTEIN sequence version = dna version (?!)
472 DBRefEntry db1 = new DBRefEntry(DBRefSource.EMBLCDSProduct,
473 this.version, data.proteinId);
474 protein.addDBRef(db1);
476 DBRefEntry dnaToEmblProteinRef = new DBRefEntry(
477 DBRefSource.EMBLCDSProduct, this.version, data.proteinId);
478 Mapping map = new Mapping(protein, maplist);
479 map.setMappedFromId(data.proteinId);
480 dnaToEmblProteinRef.setMap(map);
481 dna.addDBRef(dnaToEmblProteinRef);
485 * comment brought forward from EmblXmlSource, lines 447-451:
486 * TODO: if retrieved from EMBLCDS, add a DBRef back to the parent EMBL
487 * sequence with the exon map; if given a dataset reference, search
488 * dataset for parent EMBL sequence if it exists and set its map;
489 * make a new feature annotating the coding contig
494 * Computes a mapping from CDS positions in DNA sequence to protein product
495 * positions, with allowance for stop codon or incomplete start codon
502 MapList buildMappingToProtein(final SequenceI dna, final int[] exons,
505 MapList dnaToProteinMapping = null;
506 int peptideLength = data.translation.length();
508 int[] proteinRange = new int[] { 1, peptideLength };
509 if (exons != null && exons.length > 0)
512 * We were able to parse 'location'; do a final
513 * product length truncation check
515 int[] cdsRanges = adjustForProteinLength(peptideLength, exons);
516 dnaToProteinMapping = new MapList(cdsRanges, proteinRange, 3, 1);
521 * workaround until we handle all 'location' formats fully
522 * e.g. X53828.1:60..1058 or <123..>289
524 Cache.log.error(String.format(
525 "Implementation Notice: EMBLCDS location '%s'not properly supported yet"
526 + " - Making up the CDNA region of (%s:%s)... may be incorrect",
527 data.cdsLocation, sourceDb, this.accession));
529 int completeCodonsLength = 1 - data.codonStart + dna.getLength();
530 int mappedDnaEnd = dna.getEnd();
531 if (peptideLength * 3 == completeCodonsLength)
533 // this might occur for CDS sequences where no features are marked
534 Cache.log.warn("Assuming no stop codon at end of cDNA fragment");
535 mappedDnaEnd = dna.getEnd();
537 else if ((peptideLength + 1) * 3 == completeCodonsLength)
539 Cache.log.warn("Assuming stop codon at end of cDNA fragment");
540 mappedDnaEnd = dna.getEnd() - 3;
543 if (mappedDnaEnd != -1)
545 int[] cdsRanges = new int[] {
546 dna.getStart() + (data.codonStart - 1), mappedDnaEnd };
547 dnaToProteinMapping = new MapList(cdsRanges, proteinRange, 3, 1);
551 return dnaToProteinMapping;
555 * Constructs a sequence for the protein product for the CDS data (if there is
556 * one), and dbrefs with mappings from CDS to protein and the reverse
564 SequenceI buildProteinProduct(SequenceI dna, DBRefEntry xref,
565 CdsData data, Map<String, SequenceI> proteins)
568 * check we have some data to work with
570 if (data.proteinId == null || data.translation == null)
576 * Construct the protein sequence (if not already seen)
578 String proteinSeqName = xref.getSource() + "|" + xref.getAccessionId();
579 SequenceI protein = proteins.get(proteinSeqName);
582 protein = new Sequence(proteinSeqName, data.translation, 1,
583 data.translation.length());
584 protein.setDescription(data.proteinName != null ? data.proteinName
585 : "Protein Product from " + sourceDb);
586 proteins.put(proteinSeqName, protein);
593 * Returns the CDS location as a single array of [start, end, start, end...]
594 * positions. If on the reverse strand, these will be in descending order.
600 protected int[] getCdsRanges(String accession, String location)
602 if (location == null)
609 List<int[]> ranges = DnaUtils.parseLocation(location);
610 return MappingUtils.rangeListToArray(ranges);
611 } catch (ParseException e)
614 String.format("Not parsing inexact CDS location %s in ENA %s",
615 location, accession));
621 * Reads the value of a feature (FT) qualifier from one or more lines of the
622 * file, and returns the next line after that. Values are appended to the
623 * string buffer, which should be already primed with the value read from the
624 * first line for the qualifier (with any leading double quote removed).
625 * Enclosing double quotes are removed, and escaped (repeated) double quotes
626 * reduced to one only. For example for
629 * FT /note="gene_id=hCG28070.3
630 * FT ""foobar"" isoform=CRA_b"
631 * the returned value is
632 * gene_id=hCG28070.3 "foobar" isoform=CRA_b
635 * Note the side-effect of this method, to advance data reading to the next
636 * line after the feature qualifier (which could be another qualifier, a
637 * different feature, a non-feature line, or null at end of file).
640 * a string buffer primed with the first line of the value
641 * @param qualifierName
643 * @throws IOException
645 String parseFeatureQualifier(StringBuilder sb, String qualifierName)
649 while ((line = nextLine()) != null)
651 if (!isFeatureContinuationLine(line))
653 break; // reached next feature or other input line
655 String[] tokens = line.split(WHITESPACE);
656 if (tokens.length < 2)
658 Cache.log.error("Ignoring bad EMBL line for " + this.accession
662 if (tokens[1].startsWith("/"))
664 break; // next feature qualifier
668 * heuristic rule: most multi-line value (e.g. /product) are text,
669 * so add a space for word boundary at a new line; not for translation
671 if (!"translation".equals(qualifierName)
672 && !LOCATION.equals(qualifierName))
678 * remove trailing " and unescape doubled ""
680 String data = removeQuotes(tokens[1]);
688 * Reads and saves the sequence, read from the lines following the ORIGIN
689 * (GenBank) or SQ (EMBL) line. Whitespace and position counters are
690 * discarded. Returns the next line following the sequence data (the next line
691 * that doesn't start with whitespace).
693 * @throws IOException
695 protected String parseSequence() throws IOException
697 StringBuilder sb = new StringBuilder(this.length);
698 String line = nextLine();
699 while (line != null && line.startsWith(" "))
702 String[] blocks = line.split(WHITESPACE);
705 * the first or last block on each line might be a position count - omit
707 for (int i = 0; i < blocks.length; i++)
711 Long.parseLong(blocks[i]);
712 // position counter - ignore it
713 } catch (NumberFormatException e)
715 // sequence data - append it
716 sb.append(blocks[i]);
721 this.sequenceString = sb.toString();
727 * Processes a feature line. If it declares a feature type of interest
728 * (currently, only CDS is processed), processes all of the associated lines
729 * (feature qualifiers), and returns the next line after that, otherwise
730 * simply returns the next line.
733 * the first line for the feature (with initial FT omitted for EMBL
736 * @throws IOException
738 protected String parseFeature(String line) throws IOException
740 String[] tokens = line.trim().split(WHITESPACE);
741 if (tokens.length < 2 || !"CDS".equals(tokens[0]))
746 return parseCDSFeature(tokens[1]);
751 * A data bean class to hold values parsed from one CDS Feature
755 String translation; // from /translation qualifier
757 String cdsLocation; // the raw value e.g. join(1..1234,2012..2837)
759 int codonStart = 1; // from /codon_start qualifier
761 String proteinName; // from /product qualifier; used for protein description
763 String proteinId; // from /protein_id qualifier
765 List<DBRefEntry> xrefs = new ArrayList<>(); // from /db_xref qualifiers
767 Map<String, String> cdsProps = new Hashtable<>(); // other qualifiers