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 class that provides selective parsing of the EMBL flatfile format.
30 * The initial implementation is limited to extracting fields used by Jalview
31 * after fetching an EMBL or EMBLCDS entry:
34 * accession, version, sequence, xref
35 * and (for CDS feature) location, protein_id, product, codon_start, translation
38 * For a complete parser, it may be best to adopt that provided in
39 * https://github.com/enasequence/sequencetools/tree/master/src/main/java/uk/ac/ebi/embl/flatfile
40 * (but note this has a dependency on the Apache Commons library)
43 * @see ftp://ftp.ebi.ac.uk/pub/databases/ena/sequence/release/doc/usrman.txt
44 * @see ftp://ftp.ebi.ac.uk/pub/databases/embl/doc/FT_current.html
46 public class EmblFlatFile extends AlignFile // FileParse
48 private static final String QUOTE = "\"";
50 private static final String DOUBLED_QUOTE = QUOTE + QUOTE;
53 * when true, interpret the mol_type 'source' feature attribute
54 * and generate an RNA sequence from the DNA record
56 private boolean produceRna=true;
58 * A data bean class to hold values parsed from one CDS Feature (FT)
62 String translation; // from CDS feature /translation
64 String cdsLocation; // CDS /location raw value
66 int codonStart = 1; // from CDS /codon_start
68 String proteinName; // from CDS /product; used for protein description
70 String proteinId; // from CDS /protein_id
72 List<DBRefEntry> xrefs = new ArrayList<>(); // from CDS /db_xref qualifiers
74 Map<String, String> cdsProps = new Hashtable<>(); // CDS other qualifiers
77 private static final String WHITESPACE = "\\s+";
79 private String sourceDb;
82 * values parsed from the EMBL flatfile record
84 private String accession; // from ID (first token)
86 private String version; // from ID (second token)
88 private String description; // from (first) DE line
90 private int length = 128; // from ID (7th token), with usable default
92 private List<DBRefEntry> dbrefs; // from DR
94 private boolean sequenceStringIsRNA=false;
95 private String sequenceString; // from SQ lines
98 * parsed CDS data fields, keyed by protein_id
100 private Map<String, CdsData> cds;
107 * @throws IOException
109 public EmblFlatFile(FileParse fp, String sourceId) throws IOException
111 super(false, fp); // don't parse immediately
112 this.sourceDb = sourceId;
113 dbrefs = new ArrayList<>();
116 * using TreeMap gives CDS sequences in alphabetical, so readable, order
118 cds = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
122 * Parses the flatfile, and if successful, saves as an annotated sequence
123 * which may be retrieved by calling {@code getSequence()}
125 * @throws IOException
127 public void parse() throws IOException
129 String line = nextLine();
132 if (line.startsWith("ID"))
134 line = parseID(line);
136 else if (line.startsWith("DE"))
138 line = parseDE(line);
140 else if (line.startsWith("DR"))
142 line = parseDR(line);
144 else if (line.startsWith("SQ"))
148 else if (line.startsWith("FT"))
150 line = parseFT(line);
161 * Extracts and saves the primary accession and version (SV value) from an ID
162 * line, or null if not found. Returns the next line after the one processed.
165 * @throws IOException
167 String parseID(String line) throws IOException
169 String[] tokens = line.substring(2).split(";");
172 * first is primary accession
174 String token = tokens[0].trim();
175 if (!token.isEmpty())
177 this.accession = token;
181 * second token is 'SV versionNo'
183 if (tokens.length > 1)
185 token = tokens[1].trim();
186 if (token.startsWith("SV"))
188 String[] bits = token.trim().split(WHITESPACE);
189 this.version = bits[bits.length - 1];
194 * seventh token is 'length BP'
196 if (tokens.length > 6)
198 token = tokens[6].trim();
199 String[] bits = token.trim().split(WHITESPACE);
202 this.length = Integer.valueOf(bits[0]);
203 } catch (NumberFormatException e)
205 Cache.log.error("bad length read in flatfile, line: " + line);
213 * Reads sequence description from the first DE line found. Any trailing
214 * period is discarded. If there are multiple DE lines, only the first (short
215 * description) is read, the rest are ignored.
219 * @throws IOException
221 String parseDE(String line) throws IOException
223 String desc = line.substring(2).trim();
224 if (desc.endsWith("."))
226 desc = desc.substring(0, desc.length() - 1);
228 this.description = desc;
231 * pass over any additional DE lines
233 while ((line = nextLine()) != null)
235 if (!line.startsWith("DE"))
245 * Processes one DR line and saves as a DBRefEntry cross-reference. Returns
246 * the line following the line processed.
249 * @throws IOException
251 String parseDR(String line) throws IOException
253 String[] tokens = line.substring(2).split(";");
254 if (tokens.length > 1)
257 * ensure UniProtKB/Swiss-Prot converted to UNIPROT
259 String db = tokens[0].trim();
260 db = DBRefUtils.getCanonicalName(db);
261 String acc = tokens[1].trim();
262 if (acc.endsWith("."))
264 acc = acc.substring(0, acc.length() - 1);
266 String version = "0";
267 if (tokens.length > 2)
269 String secondaryId = tokens[2].trim();
270 if (!secondaryId.isEmpty())
272 // todo: is this right? secondary id is not a version number
273 // version = secondaryId;
276 this.dbrefs.add(new DBRefEntry(db, version, acc));
283 * Reads and saves the sequence, read from the lines following the SQ line.
284 * Whitespace and position counters are discarded. Returns the next line
285 * following the sequence data (the next line that doesn't start with
288 * @throws IOException
290 String parseSQ() throws IOException
292 StringBuilder sb = new StringBuilder(this.length);
293 String line = nextLine();
294 while (line != null && line.startsWith(" "))
297 String[] blocks = line.split(WHITESPACE);
300 * omit the last block (position counter) on each line
302 for (int i = 0; i < blocks.length - 1; i++)
304 sb.append(blocks[i]);
308 this.sequenceString = sb.toString();
314 * Processes an FT line. If it declares a feature type of interest (currently,
315 * only CDS is processed), processes all of the associated lines (feature
316 * qualifiers), and returns the next line after that, otherwise simply returns
321 * @throws IOException
323 String parseFT(String line) throws IOException
325 String[] tokens = line.split(WHITESPACE);
326 if (tokens.length < 3 || (!"CDS".equals(tokens[1]) && !"source".equals(tokens[1])))
331 if (tokens[1].equals("source"))
333 return parseSourceQualifiers(tokens);
337 * parse location - which may be over more than one line e.g. EAW51554
339 CdsData data = new CdsData();
340 data.cdsLocation = tokens[2];
341 // TODO location can be over >1 line e.g. EAW51554
346 if (!line.startsWith("FT ")) // 4 spaces
348 // e.g. start of next feature "FT source..."
353 * extract qualifier, e.g. FT /protein_id="CAA37824.1"
354 * - the value may extend over more than one line
355 * - if the value has enclosing quotes, these are removed
356 * - escaped double quotes ("") are reduced to a single character
358 int slashPos = line.indexOf('/');
361 Cache.log.error("Unexpected EMBL line ignored: " + line);
365 int eqPos = line.indexOf('=', slashPos + 1);
368 // can happen, e.g. /ribosomal_slippage
369 // Cache.log.error("Unexpected EMBL line ignored: " + line);
373 String qualifier = line.substring(slashPos + 1, eqPos);
374 String value = line.substring(eqPos + 1);
375 value = removeQuotes(value);
376 StringBuilder sb = new StringBuilder().append(value);
377 line = parseFeatureQualifier(sb, qualifier);
378 String featureValue = sb.toString();
380 if ("protein_id".equals(qualifier))
382 data.proteinId = featureValue;
384 else if ("codon_start".equals(qualifier))
388 data.codonStart = Integer.parseInt(featureValue.trim());
389 } catch (NumberFormatException e)
391 Cache.log.error("Invalid codon_start in XML for " + this.accession
392 + ": " + e.getMessage());
395 else if ("db_xref".equals(qualifier))
397 String[] parts = featureValue.split(":");
398 if (parts.length == 2)
400 String db = parts[0].trim();
401 db = DBRefUtils.getCanonicalName(db);
402 DBRefEntry dbref = new DBRefEntry(db, "0", parts[1].trim());
403 data.xrefs.add(dbref);
406 else if ("product".equals(qualifier))
408 data.proteinName = featureValue;
410 else if ("translation".equals(qualifier))
412 data.translation = featureValue;
414 else if (!"".equals(featureValue))
416 // throw anything else into the additional properties hash
417 data.cdsProps.put(qualifier, featureValue);
421 if (data.proteinId != null)
423 this.cds.put(data.proteinId, data);
427 Cache.log.error("Ignoring CDS feature with no protein_id for "
428 + sourceDb + ":" + accession);
435 * process attributes for 'source' until the next FT feature entry
436 * only interested in 'mol_type'
439 * @throws IOException
441 private String parseSourceQualifiers(String[] tokens) throws IOException
443 if (!"source".equals(tokens[1]))
445 throw (new RuntimeException("Not given a source qualifier"));
447 // search for mol_type attribute
449 StringBuilder sb = new StringBuilder().append(tokens[2]); // extent of
452 String line = parseFeatureQualifier(sb, "source");
455 if (!line.startsWith("FT ")) // four spaces, end of this feature table
461 int p = line.indexOf("\\mol_type");
462 int qs = line.indexOf("\"", p);
463 int qe = line.indexOf("\"", qs + 1);
464 String qualifier=line.substring(qs,qe).toLowerCase();
465 if (qualifier.indexOf("rna") > -1)
467 sequenceStringIsRNA = true;
469 if (qualifier.indexOf("dna") > -1)
471 sequenceStringIsRNA = false;
473 line=parseFeatureQualifier(sb, "source");
479 * Removes leading or trailing double quotes (") unless doubled, and changes
480 * any 'escaped' (doubled) double quotes to single characters. As per the
481 * Feature Table specification for Qualifiers, Free Text.
486 static String removeQuotes(String value)
492 if (value.startsWith(QUOTE) && !value.startsWith(DOUBLED_QUOTE))
494 value = value.substring(1);
496 if (value.endsWith(QUOTE) && !value.endsWith(DOUBLED_QUOTE))
498 value = value.substring(0, value.length() - 1);
500 value = value.replace(DOUBLED_QUOTE, QUOTE);
505 * Reads the value of a feature (FT) qualifier from one or more lines of the
506 * file, and returns the next line after that. Values are appended to the
507 * string buffer, which should be already primed with the value read from the
508 * first line for the qualifier (with any leading double quote removed).
509 * Enclosing double quotes are removed, and escaped (repeated) double quotes
510 * reduced to one only. For example for
513 * FT /note="gene_id=hCG28070.3
514 * FT ""foobar"" isoform=CRA_b"
515 * the returned value is
516 * gene_id=hCG28070.3 "foobar" isoform=CRA_b
519 * Note the side-effect of this method, to advance data reading to the next
520 * line after the feature qualifier.
523 * a string buffer primed with the first line of the value
524 * @param qualifierName
526 * @throws IOException
528 String parseFeatureQualifier(StringBuilder sb, String qualifierName)
532 while ((line = nextLine()) != null)
534 if (!line.startsWith("FT "))
536 break; // reached next feature or other input line
538 String[] tokens = line.split(WHITESPACE);
539 if (tokens.length < 2)
541 Cache.log.error("Ignoring bad EMBL line for " + this.accession
545 if (tokens[1].startsWith("/"))
547 break; // next feature qualifier
551 * heuristic rule: most multi-line value (e.g. /product) are text,
552 * so add a space for word boundary at a new line; not for translation
554 if (!"translation".equals(qualifierName))
560 * remove trailing " and unescape doubled ""
562 String data = removeQuotes(tokens[1]);
570 * Constructs and saves the sequence from parsed components
574 if (this.accession == null || this.sequenceString == null)
576 Cache.log.error("Failed to parse data from EMBL");
580 String name = this.accession;
581 if (this.sourceDb != null)
583 name = this.sourceDb + "|" + name;
586 if (produceRna && sequenceStringIsRNA)
588 sequenceString = sequenceString.replace('T', 'U').replace('t', 'u');
591 SequenceI seq = new Sequence(name, this.sequenceString);
592 seq.setDescription(this.description);
595 * add a DBRef to itself
597 DBRefEntry selfRef = new DBRefEntry(sourceDb, version, accession);
598 int[] startEnd = new int[] { 1, seq.getLength() };
599 selfRef.setMap(new Mapping(null, startEnd, startEnd, 1, 1));
600 seq.addDBRef(selfRef);
602 for (DBRefEntry dbref : this.dbrefs)
607 processCDSFeatures(seq);
609 seq.deriveSequence();
615 * Process the CDS features, including generation of cross-references and
616 * mappings to the protein products (translation)
620 protected void processCDSFeatures(SequenceI seq)
623 * record protein products found to avoid duplication i.e. >1 CDS with
624 * the same /protein_id [though not sure I can find an example of this]
626 Map<String, SequenceI> proteins = new HashMap<>();
627 for (CdsData data : cds.values())
629 processCDSFeature(seq, data, proteins);
634 * Processes data for one parsed CDS feature to
636 * <li>create a protein product sequence for the translation</li>
637 * <li>create a cross-reference to protein with mapping from dna</li>
638 * <li>add a CDS feature to the sequence for each CDS start-end range</li>
639 * <li>add any CDS dbrefs to the sequence and to the protein product</li>
645 * map of protein products so far derived from CDS data
647 void processCDSFeature(SequenceI dna, CdsData data,
648 Map<String, SequenceI> proteins)
651 * parse location into a list of [start, end, start, end] positions
653 int[] exons = getCdsRanges(this.accession, data.cdsLocation);
655 MapList maplist = buildMappingToProtein(dna, exons, data);
659 for (int xint = 0; exons != null && xint < exons.length - 1; xint += 2)
661 int exonStart = exons[xint];
662 int exonEnd = exons[xint + 1];
663 int begin = Math.min(exonStart, exonEnd);
664 int end = Math.max(exonStart, exonEnd);
666 String desc = String.format("Exon %d for protein EMBLCDS:%s",
667 exonNumber, data.proteinId);
669 SequenceFeature sf = new SequenceFeature("CDS", desc, begin, end,
671 for (Entry<String, String> val : data.cdsProps.entrySet())
673 sf.setValue(val.getKey(), val.getValue());
676 sf.setEnaLocation(data.cdsLocation);
677 boolean forwardStrand = exonStart <= exonEnd;
678 sf.setStrand(forwardStrand ? "+" : "-");
679 sf.setPhase(String.valueOf(data.codonStart - 1));
680 sf.setValue(FeatureProperties.EXONPOS, exonNumber);
681 sf.setValue(FeatureProperties.EXONPRODUCT, data.proteinName);
683 dna.addSequenceFeature(sf);
686 boolean hasUniprotDbref = false;
687 for (DBRefEntry xref : data.xrefs)
690 if (xref.getSource().equals(DBRefSource.UNIPROT))
693 * construct (or find) the sequence for (data.protein_id, data.translation)
695 SequenceI protein = buildProteinProduct(dna, xref, data, proteins);
696 Mapping map = new Mapping(protein, maplist);
697 map.setMappedFromId(data.proteinId);
701 * add DBRefs with mappings from dna to protein and the inverse
703 DBRefEntry db1 = new DBRefEntry(sourceDb, version, accession);
704 db1.setMap(new Mapping(dna, maplist.getInverse()));
705 protein.addDBRef(db1);
707 hasUniprotDbref = true;
712 * if we have a product (translation) but no explicit Uniprot dbref
713 * (example: EMBL M19487 protein_id AAB02592.1)
714 * then construct mappings to an assumed EMBLCDSPROTEIN accession
716 if (!hasUniprotDbref)
718 SequenceI protein = proteins.get(data.proteinId);
721 protein = new Sequence(data.proteinId, data.translation);
722 protein.setDescription(data.proteinName);
723 proteins.put(data.proteinId, protein);
725 // assuming CDSPROTEIN sequence version = dna version (?!)
726 DBRefEntry db1 = new DBRefEntry(DBRefSource.EMBLCDSProduct,
727 this.version, data.proteinId);
728 protein.addDBRef(db1);
730 DBRefEntry dnaToEmblProteinRef = new DBRefEntry(
731 DBRefSource.EMBLCDSProduct, this.version, data.proteinId);
732 Mapping map = new Mapping(protein, maplist);
733 map.setMappedFromId(data.proteinId);
734 dnaToEmblProteinRef.setMap(map);
735 dna.addDBRef(dnaToEmblProteinRef);
739 * comment brought forward from EmblXmlSource, lines 447-451:
740 * TODO: if retrieved from EMBLCDS, add a DBRef back to the parent EMBL
741 * sequence with the exon map; if given a dataset reference, search
742 * dataset for parent EMBL sequence if it exists and set its map;
743 * make a new feature annotating the coding contig
748 * Computes a mapping from CDS positions in DNA sequence to protein product
749 * positions, with allowance for stop codon or incomplete start codon
756 MapList buildMappingToProtein(final SequenceI dna, final int[] exons,
759 MapList dnaToProteinMapping = null;
760 int peptideLength = data.translation.length();
762 int[] proteinRange = new int[] { 1, peptideLength };
763 if (exons != null && exons.length > 0)
766 * We were able to parse 'location'; do a final
767 * product length truncation check
769 int[] cdsRanges = adjustForProteinLength(peptideLength, exons);
770 dnaToProteinMapping = new MapList(cdsRanges, proteinRange, 3, 1);
775 * workaround until we handle all 'location' formats fully
776 * e.g. X53828.1:60..1058 or <123..>289
778 Cache.log.error(String.format(
779 "Implementation Notice: EMBLCDS location '%s'not properly supported yet"
780 + " - Making up the CDNA region of (%s:%s)... may be incorrect",
781 data.cdsLocation, sourceDb, this.accession));
783 int completeCodonsLength = 1 - data.codonStart + dna.getLength();
784 int mappedDnaEnd = dna.getEnd();
785 if (peptideLength * 3 == completeCodonsLength)
787 // this might occur for CDS sequences where no features are marked
788 Cache.log.warn("Assuming no stop codon at end of cDNA fragment");
789 mappedDnaEnd = dna.getEnd();
791 else if ((peptideLength + 1) * 3 == completeCodonsLength)
793 Cache.log.warn("Assuming stop codon at end of cDNA fragment");
794 mappedDnaEnd = dna.getEnd() - 3;
797 if (mappedDnaEnd != -1)
799 int[] cdsRanges = new int[] {
800 dna.getStart() + (data.codonStart - 1), mappedDnaEnd };
801 dnaToProteinMapping = new MapList(cdsRanges, proteinRange, 3, 1);
805 return dnaToProteinMapping;
809 * Constructs a sequence for the protein product for the CDS data (if there is
810 * one), and dbrefs with mappings from CDS to protein and the reverse
818 SequenceI buildProteinProduct(SequenceI dna, DBRefEntry xref,
819 CdsData data, Map<String, SequenceI> proteins)
822 * check we have some data to work with
824 if (data.proteinId == null || data.translation == null)
830 * Construct the protein sequence (if not already seen)
832 String proteinSeqName = xref.getSource() + "|" + xref.getAccessionId();
833 SequenceI protein = proteins.get(proteinSeqName);
836 protein = new Sequence(proteinSeqName, data.translation, 1,
837 data.translation.length());
838 protein.setDescription(data.proteinName != null ? data.proteinName
839 : "Protein Product from " + sourceDb);
840 proteins.put(proteinSeqName, protein);
847 * Returns the CDS location as a single array of [start, end, start, end...]
848 * positions. If on the reverse strand, these will be in descending order.
854 protected int[] getCdsRanges(String accession, String location)
856 if (location == null)
863 List<int[]> ranges = DnaUtils.parseLocation(location);
864 return MappingUtils.listToArray(ranges);
865 } catch (ParseException e)
868 String.format("Not parsing inexact CDS location %s in ENA %s",
869 location, accession));
875 * Output (print) is not implemented for EMBL flat file format
878 public String print(SequenceI[] seqs, boolean jvsuffix)
884 * Truncates (if necessary) the exon intervals to match 3 times the length of
885 * the protein; also accepts 3 bases longer (for stop codon not included in
888 * @param proteinLength
890 * an array of [start, end, start, end...] intervals
891 * @return the same array (if unchanged) or a truncated copy
893 static int[] adjustForProteinLength(int proteinLength, int[] exon)
895 if (proteinLength <= 0 || exon == null)
899 int expectedCdsLength = proteinLength * 3;
900 int exonLength = MappingUtils.getLength(Arrays.asList(exon));
903 * if exon length matches protein, or is shorter, or longer by the
904 * length of a stop codon (3 bases), then leave it unchanged
906 if (expectedCdsLength >= exonLength
907 || expectedCdsLength == exonLength - 3)
915 origxon = new int[exon.length];
916 System.arraycopy(exon, 0, origxon, 0, exon.length);
918 for (int x = 0; x < exon.length; x += 2)
920 cdspos += Math.abs(exon[x + 1] - exon[x]) + 1;
921 if (expectedCdsLength <= cdspos)
923 // advanced beyond last codon.
925 if (expectedCdsLength != cdspos)
928 // .println("Truncating final exon interval on region by "
929 // + (cdspos - cdslength));
933 * shrink the final exon - reduce end position if forward
934 * strand, increase it if reverse
936 if (exon[x + 1] >= exon[x])
938 endxon = exon[x + 1] - cdspos + expectedCdsLength;
942 endxon = exon[x + 1] + cdspos - expectedCdsLength;
950 // and trim the exon interval set if necessary
951 int[] nxon = new int[sxpos + 2];
952 System.arraycopy(exon, 0, nxon, 0, sxpos + 2);
953 nxon[sxpos + 1] = endxon; // update the end boundary for the new exon