X-Git-Url: http://source.jalview.org/gitweb/?a=blobdiff_plain;ds=sidebyside;f=src%2Fjalview%2Fio%2Fvcf%2FVCFLoader.java;h=ac707d8f01cea046948f2809c07256b0739c0ae2;hb=456e5c765ac1e85336fd9f9b1a35453069bb2298;hp=9d98b7e6c675c2967424790c4f22e77c86161df1;hpb=b87ae5ac68939a1b964682046e8b07958fae219a;p=jalview.git diff --git a/src/jalview/io/vcf/VCFLoader.java b/src/jalview/io/vcf/VCFLoader.java index 9d98b7e..ac707d8 100644 --- a/src/jalview/io/vcf/VCFLoader.java +++ b/src/jalview/io/vcf/VCFLoader.java @@ -1,10 +1,8 @@ package jalview.io.vcf; -import jalview.analysis.AlignmentUtils; import jalview.analysis.Dna; import jalview.api.AlignViewControllerGuiI; import jalview.bin.Cache; -import jalview.datamodel.AlignmentI; import jalview.datamodel.DBRefEntry; import jalview.datamodel.GeneLociI; import jalview.datamodel.Mapping; @@ -14,6 +12,7 @@ import jalview.datamodel.features.FeatureAttributeType; import jalview.datamodel.features.FeatureSource; import jalview.datamodel.features.FeatureSources; import jalview.ext.ensembl.EnsemblMap; +import jalview.ext.htsjdk.HtsContigDb; import jalview.ext.htsjdk.VCFReader; import jalview.io.gff.Gff3Helper; import jalview.io.gff.SequenceOntologyI; @@ -21,12 +20,18 @@ import jalview.util.MapList; import jalview.util.MappingUtils; import jalview.util.MessageManager; +import java.io.File; import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.net.URLDecoder; import java.util.ArrayList; import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Map.Entry; +import java.util.Set; import java.util.regex.Pattern; import java.util.regex.PatternSyntaxException; @@ -34,8 +39,10 @@ import htsjdk.samtools.SAMException; import htsjdk.samtools.SAMSequenceDictionary; import htsjdk.samtools.SAMSequenceRecord; import htsjdk.samtools.util.CloseableIterator; +import htsjdk.tribble.TribbleException; import htsjdk.variant.variantcontext.Allele; import htsjdk.variant.variantcontext.VariantContext; +import htsjdk.variant.vcf.VCFConstants; import htsjdk.variant.vcf.VCFHeader; import htsjdk.variant.vcf.VCFHeaderLine; import htsjdk.variant.vcf.VCFHeaderLineCount; @@ -50,6 +57,33 @@ import htsjdk.variant.vcf.VCFInfoHeaderLine; */ public class VCFLoader { + private static final String ENCODED_COMMA = "%2C"; + + private static final String ENCODED_PERCENT = "%25"; + + private static final String ENCODED_EQUALS = "%3D"; + + private static final String ENCODED_SEMICOLON = "%3B"; + + private static final String ENCODED_COLON = "%3A"; + + private static final String UTF_8 = "UTF-8"; + + /* + * Jalview feature attributes for VCF fixed column data + */ + private static final String VCF_POS = "POS"; + + private static final String VCF_ID = "ID"; + + private static final String VCF_QUAL = "QUAL"; + + private static final String VCF_FILTER = "FILTER"; + + private static final String NO_VALUE = VCFConstants.MISSING_VALUE_v4; // '.' + + private static final String DEFAULT_SPECIES = "homo_sapiens"; + /** * A class to model the mapping from sequence to VCF coordinates. Cases include * * - * @param seq - * @param variant - * @param altAlleleIndex * @param consequence * @return * @see http://www.sequenceontology.org/browser/current_svn/term/SO:0001060 */ - String getOntologyTerm(SequenceI seq, VariantContext variant, - int altAlleleIndex, String consequence) + String getOntologyTerm(String consequence) { String type = SequenceOntologyI.SEQUENCE_VARIANT; + /* + * could we associate Consequence data with this allele and feature (transcript)? + * if so, prefer the consequence term from that data + */ if (csqAlleleFieldIndex == -1) // && snpEffAlleleFieldIndex == -1 { /* @@ -987,10 +1133,6 @@ public class VCFLoader return type; } - /* - * can we associate Consequence data with this allele and feature (transcript)? - * if so, prefer the consequence term from that data - */ if (consequence != null) { String[] csqFields = consequence.split(PIPE_REGEX); @@ -1121,7 +1263,6 @@ public class VCFLoader * Add any allele-specific VCF key-value data to the sequence feature * * @param variant - * @param seq * @param sf * @param altAlelleIndex * (0, 1..) @@ -1129,7 +1270,7 @@ public class VCFLoader * if not null, the consequence specific to this sequence (transcript * feature) and allele */ - protected void addAlleleProperties(VariantContext variant, SequenceI seq, + protected void addAlleleProperties(VariantContext variant, SequenceFeature sf, final int altAlelleIndex, String consequence) { Map atts = variant.getAttributes(); @@ -1144,7 +1285,7 @@ public class VCFLoader */ if (CSQ_FIELD.equals(key)) { - addConsequences(variant, seq, sf, consequence); + addConsequences(variant, sf, consequence); continue; } @@ -1193,14 +1334,110 @@ public class VCFLoader * take the index'th value */ String value = getAttributeValue(variant, key, index); - if (value != null) + if (value != null && isValid(variant, key, value)) { - sf.setValue(key, value); + value = decodeSpecialCharacters(value); + addFeatureAttribute(sf, key, value); } } } /** + * Decodes colon, semicolon, equals sign, percent sign, comma to their decoded + * form. The VCF specification (para 1.2) requires these to be encoded where not + * used with their special meaning in the VCF syntax. Note that general URL + * decoding should not be applied, since this would incorrectly decode (for + * example) a '+' sign. + * + * @param value + * @return + */ + protected static String decodeSpecialCharacters(String value) + { + /* + * avoid regex compilation if it is not needed! + */ + if (!value.contains(ENCODED_COLON) && !value.contains(ENCODED_SEMICOLON) + && !value.contains(ENCODED_EQUALS) + && !value.contains(ENCODED_PERCENT) + && !value.contains(ENCODED_COMMA)) + { + return value; + } + + value = value.replace(ENCODED_COLON, ":") + .replace(ENCODED_SEMICOLON, ";").replace(ENCODED_EQUALS, "=") + .replace(ENCODED_PERCENT, "%").replace(ENCODED_COMMA, ","); + return value; + } + + /** + * Answers true for '.', null, or an empty value, or if the INFO type is String. + * If the INFO type is Integer or Float, answers false if the value is not in + * valid format. + * + * @param variant + * @param infoId + * @param value + * @return + */ + protected boolean isValid(VariantContext variant, String infoId, + String value) + { + if (value == null || value.isEmpty() || NO_VALUE.equals(value)) + { + return true; + } + VCFInfoHeaderLine infoHeader = header.getInfoHeaderLine(infoId); + if (infoHeader == null) + { + Cache.log.error("Field " + infoId + " has no INFO header"); + return false; + } + VCFHeaderLineType infoType = infoHeader.getType(); + try + { + if (infoType == VCFHeaderLineType.Integer) + { + Integer.parseInt(value); + } + else if (infoType == VCFHeaderLineType.Float) + { + Float.parseFloat(value); + } + } catch (NumberFormatException e) + { + logInvalidValue(variant, infoId, value); + return false; + } + return true; + } + + /** + * Logs an error message for malformed data; duplicate messages (same id and + * value) are not logged + * + * @param variant + * @param infoId + * @param value + */ + private void logInvalidValue(VariantContext variant, String infoId, + String value) + { + if (badData == null) + { + badData = new HashSet<>(); + } + String token = infoId + ":" + value; + if (!badData.contains(token)) + { + badData.add(token); + Cache.log.error(String.format("Invalid VCF data at %s:%d %s=%s", + variant.getContig(), variant.getStart(), infoId, value)); + } + } + + /** * Inspects CSQ data blocks (consequences) and adds attributes on the sequence * feature. *

@@ -1209,15 +1446,13 @@ public class VCFLoader * transcript (sequence) being processed) * * @param variant - * @param seq * @param sf * @param myConsequence */ - protected void addConsequences(VariantContext variant, SequenceI seq, - SequenceFeature sf, String myConsequence) + protected void addConsequences(VariantContext variant, SequenceFeature sf, + String myConsequence) { Object value = variant.getAttribute(CSQ_FIELD); - // TODO if CSQ not present, try ANN (for SnpEff consequence data)? if (value == null || !(value instanceof List)) { @@ -1250,6 +1485,16 @@ public class VCFLoader String id = vepFieldsOfInterest.get(i); if (id != null) { + /* + * VCF spec requires encoding of special characters e.g. '=' + * so decode them here before storing + */ + try + { + field = URLDecoder.decode(field, UTF_8); + } catch (UnsupportedEncodingException e) + { + } csqValues.put(id, field); } }