Skip to content

Commit 95f1876

Browse files
Anders LeungAnders Leung
authored andcommitted
Add VCF 4.3 writing
1 parent 3b719a8 commit 95f1876

26 files changed

Lines changed: 863 additions & 143 deletions

src/main/java/htsjdk/samtools/Defaults.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package htsjdk.samtools;
22

33
import htsjdk.samtools.util.Log;
4+
import htsjdk.variant.variantcontext.writer.VCF42To43VersionTransitionPolicy;
45

56
import java.io.File;
67
import java.util.Collections;
@@ -110,6 +111,8 @@ public class Defaults {
110111
*/
111112
public static final boolean DISABLE_SNAPPY_COMPRESSOR;
112113

114+
public static final VCF42To43VersionTransitionPolicy VCF_VERSION_TRANSITION_POLICY;
115+
113116

114117
public static final String SAMJDK_PREFIX = "samjdk.";
115118
static {
@@ -134,6 +137,10 @@ public class Defaults {
134137
SAM_FLAG_FIELD_FORMAT = SamFlagField.valueOf(getStringProperty("sam_flag_field_format", SamFlagField.DECIMAL.name()));
135138
SRA_LIBRARIES_DOWNLOAD = getBooleanProperty("sra_libraries_download", false);
136139
DISABLE_SNAPPY_COMPRESSOR = getBooleanProperty(DISABLE_SNAPPY_PROPERTY_NAME, false);
140+
VCF_VERSION_TRANSITION_POLICY = VCF42To43VersionTransitionPolicy.valueOf(getStringProperty(
141+
"vcf_version_transition_policy",
142+
VCF42To43VersionTransitionPolicy.DO_NOT_TRANSITION.name()
143+
));
137144
}
138145

139146
/**
@@ -157,6 +164,7 @@ public static SortedMap<String, Object> allDefaults(){
157164
result.put("CUSTOM_READER_FACTORY", CUSTOM_READER_FACTORY);
158165
result.put("SAM_FLAG_FIELD_FORMAT", SAM_FLAG_FIELD_FORMAT);
159166
result.put("DISABLE_SNAPPY_COMPRESSOR", DISABLE_SNAPPY_COMPRESSOR);
167+
result.put("VCF_VERSION_TRANSITION_POLICY", VCF_VERSION_TRANSITION_POLICY);
160168
return Collections.unmodifiableSortedMap(result);
161169
}
162170

src/main/java/htsjdk/samtools/cram/structure/CRAMCompressionRecord.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -364,7 +364,7 @@ public void assignReadName() {
364364
* that the record's read bases, quality scores, and mate graph are not stored directly as part of the
365365
* record. Normalization is the process of resolving these values, and is performed at Slice granularity,
366366
* across all records in a Slice.
367-
* (see {@link Slice#normalizeCRAMRecords(List, CRAMReferenceRegion, SubstitutionMatrix)}).
367+
* (see {@link Slice#normalizeCRAMRecords(List, CRAMReferenceRegion)}).
368368
*/
369369
void setIsNormalized() { isNormalized = true; }
370370

@@ -373,7 +373,7 @@ public void assignReadName() {
373373
* scores, and mate graph are not stored directly as part of the record. These values must be resolved
374374
* through the separate process of normalization, which is performed at Slice granularity (all records in a
375375
* Slice are normalized at the same time).
376-
* (see {@link Slice#normalizeCRAMRecords(List, CRAMReferenceRegion, SubstitutionMatrix)}).
376+
* (see {@link Slice#normalizeCRAMRecords(List, CRAMReferenceRegion)}).
377377
* @return true if this record is normalized
378378
*/
379379
public boolean isNormalized() { return isNormalized; }

src/main/java/htsjdk/variant/variantcontext/VariantContext.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -399,6 +399,8 @@ private static void validateFilters(final VariantContext variantContext) {
399399
if (!VALID_FILTER.matcher(filter).matches()) {
400400
throw new IllegalStateException("Filter '" + filter +
401401
"' contains an illegal character. It must conform to the regex ;'" + VALID_FILTER);
402+
} else if (filter.equals("0")) {
403+
throw new IllegalStateException("Filter cannot use reserved string '0'");
402404
}
403405
}
404406
}
@@ -451,7 +453,7 @@ protected VariantContext(final String source,
451453
final Map<String, Object> attributes,
452454
final boolean fullyDecoded,
453455
final EnumSet<Validation> validationToPerform ) {
454-
if ( contig == null ) { throw new IllegalArgumentException("Contig cannot be null"); }
456+
if ( contig == null || contig.isEmpty() ) { throw new IllegalArgumentException("Contig cannot be null or the empty string"); }
455457
this.contig = contig;
456458
this.start = start;
457459
this.stop = stop;

src/main/java/htsjdk/variant/variantcontext/writer/BCF2Writer.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
import htsjdk.variant.vcf.VCFConstants;
4444
import htsjdk.variant.vcf.VCFContigHeaderLine;
4545
import htsjdk.variant.vcf.VCFHeader;
46+
import htsjdk.variant.vcf.VCFHeaderVersion;
4647
import htsjdk.variant.vcf.VCFUtils;
4748

4849
import java.io.ByteArrayOutputStream;
@@ -169,7 +170,8 @@ public void writeHeader(VCFHeader header) {
169170
// write out the header into a byte stream, get its length, and write everything to the file
170171
final ByteArrayOutputStream capture = new ByteArrayOutputStream();
171172
final OutputStreamWriter writer = new OutputStreamWriter(capture);
172-
this.header = VCFWriter.writeHeader(this.header, writer, VCFWriter.getVersionLine(), "BCF2 stream");
173+
// TODO implement logic to select VCF version based on BCF version
174+
this.header = VCFWriter.writeHeader(this.header, writer, VCFHeaderVersion.VCF4_2, "BCF2 stream");
173175
writer.append('\0'); // the header is null terminated by a byte
174176
writer.close();
175177

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package htsjdk.variant.variantcontext.writer;
2+
3+
/**
4+
* Defines the policy VCFWriter will use when writing a VCF file from a version before 4.3.
5+
*
6+
* This is necessary as VCF 4.3 is stricter than 4.2, meaning that some valid 4.2 files are invalid 4.3
7+
* and automatically transitioning from pre 4.3 to 4.3+ is not always possible.
8+
* This class is a temporary workaround to allow opt-in 4.3 writing support in a way that does not break
9+
* workflows that may process 4.2 files that are invalid 4.3, but should be removed once proper versioning
10+
* support for VCF is incorporated into htsjdk
11+
*/
12+
public enum VCF42To43VersionTransitionPolicy {
13+
/**
14+
* Write pre 4.3 files as 4.2, to which automatic transitioning should always be possible, and
15+
* write 4.3+ files as 4.3.
16+
*/
17+
DO_NOT_TRANSITION,
18+
19+
/**
20+
* Inspect the headers of pre 4.3 files to determine if they can be automatically transitioned to 4.3,
21+
* and if automatic transition is possible write them as 4.3, or else write them as 4.2.
22+
*/
23+
TRANSITION_IF_POSSIBLE,
24+
25+
/**
26+
* Inspect the headers of pre 4.3 files to determine if they can be automatically transitioned to 4.3,
27+
* and abort with an error if automatic transition is not possible
28+
*/
29+
FAIL_IF_CANNOT_TRANSITION,
30+
}

src/main/java/htsjdk/variant/variantcontext/writer/VCFWriter.java

Lines changed: 37 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525

2626
package htsjdk.variant.variantcontext.writer;
2727

28+
import htsjdk.samtools.Defaults;
2829
import htsjdk.samtools.SAMSequenceDictionary;
2930
import htsjdk.samtools.util.IOUtil;
3031
import htsjdk.samtools.util.Log;
@@ -57,8 +58,6 @@
5758
class VCFWriter extends IndexingVariantContextWriter {
5859
protected final static Log logger = Log.getInstance(VCFWriter.class);
5960

60-
private static final String DEFAULT_VERSION_LINE = VCFHeader.DEFAULT_VCF_VERSION.toHeaderVersionLine();
61-
6261
// Initialized when the header is written to the output stream
6362
private VCFEncoder vcfEncoder = null;
6463

@@ -76,6 +75,8 @@ class VCFWriter extends IndexingVariantContextWriter {
7675
// is the header or body written to the output stream?
7776
private boolean outputHasBeenWritten;
7877

78+
private VCF42To43VersionTransitionPolicy transitionPolicy = Defaults.VCF_VERSION_TRANSITION_POLICY;
79+
7980
/*
8081
* The VCF writer uses an internal Writer, based by the ByteArrayOutputStream lineBuffer,
8182
* to temp. buffer the header and per-site output before flushing the per line output
@@ -155,37 +156,55 @@ private void writeAndResetBuffer() throws IOException {
155156

156157
@Override
157158
public void writeHeader(final VCFHeader header) {
158-
159159
// note we need to update the mHeader object after this call because they header
160160
// may have genotypes trimmed out of it, if doNotWriteGenotypes is true
161161
setHeader(header);
162162
try {
163-
writeHeader(this.mHeader, writer, getVersionLine(), getStreamName());
163+
writeHeader(this.mHeader, writer, getOutputVersion(header), getStreamName());
164164
writeAndResetBuffer();
165165
outputHasBeenWritten = true;
166166
} catch ( IOException e ) {
167167
throw new RuntimeIOException("Couldn't write file " + getStreamName(), e);
168168
}
169169
}
170170

171-
public static String getVersionLine() {
172-
return DEFAULT_VERSION_LINE;
171+
private VCFHeaderVersion getOutputVersion(final VCFHeader header) {
172+
if (transitionPolicy == VCF42To43VersionTransitionPolicy.DO_NOT_TRANSITION) {
173+
// Write pre 4.3 files as 4.2, and 4.3+ files as 4.3
174+
return header.getVCFHeaderVersion() != null && header.getVCFHeaderVersion().isAtLeastAsRecentAs(VCFHeaderVersion.VCF4_3)
175+
? header.getVCFHeaderVersion()
176+
: VCFHeaderVersion.VCF4_2;
177+
} else {
178+
// Try to promote to 4.3+
179+
return VCFHeader.DEFAULT_VCF_VERSION;
180+
}
173181
}
174182

175-
public static VCFHeader writeHeader(VCFHeader header,
183+
public static VCFHeader writeHeader(final VCFHeader header,
176184
final Writer writer,
177-
final String versionLine,
185+
final VCFHeaderVersion version,
178186
final String streamNameForError) {
179-
180187
try {
181-
rejectVCFV43Headers(header);
182-
183-
// Validate that the file version we're writing is version-compatible this header's version.
184-
validateHeaderVersion(header, versionLine);
188+
VCFHeaderVersion versionToWrite = version;
189+
// Validate that the file version we're writing is version-compatible with this header's version.
190+
for (final VCFHeaderLine line : header.getMetaDataInSortedOrder() ) {
191+
// Skip the fileformat line, because we may be trying to transition versions
192+
if ( VCFHeaderVersion.isFormatString(line.getKey()) ) {
193+
continue;
194+
}
195+
try {
196+
line.validateForVersion(versionToWrite);
197+
} catch (final TribbleException e) {
198+
// If 4.3 validation fails for any line, use 4.2
199+
if (versionToWrite == VCFHeader.DEFAULT_VCF_VERSION) {
200+
versionToWrite = VCFHeaderVersion.VCF4_2;
201+
}
202+
}
203+
}
185204

186205
// The file format field needs to be written first; below any file format lines
187206
// embedded in the header will be removed
188-
writer.write(versionLine + "\n");
207+
writer.write(versionToWrite.toHeaderVersionLine() + "\n");
189208

190209
for (final VCFHeaderLine line : header.getMetaDataInSortedOrder() ) {
191210
// Remove the fileformat header lines
@@ -201,8 +220,8 @@ public static VCFHeader writeHeader(VCFHeader header,
201220
// write out the column line
202221
writer.write(VCFHeader.HEADER_INDICATOR);
203222
writer.write(header.getHeaderFields().stream()
204-
.map(f -> f.name())
205-
.collect(Collectors.joining(VCFConstants.FIELD_SEPARATOR)).toString());
223+
.map(Enum::name)
224+
.collect(Collectors.joining(VCFConstants.FIELD_SEPARATOR)));
206225

207226
if ( header.hasGenotypingData() ) {
208227
writer.write(VCFConstants.FIELD_SEPARATOR);
@@ -291,21 +310,14 @@ public void add(final VariantContext context) {
291310

292311
@Override
293312
public void setHeader(final VCFHeader header) {
294-
rejectVCFV43Headers(header);
295-
296313
if (outputHasBeenWritten) {
297314
throw new IllegalStateException("The header cannot be modified after the header or variants have been written to the output stream.");
298315
}
299316
this.mHeader = doNotWriteGenotypes ? new VCFHeader(header.getMetaDataInSortedOrder()) : header;
300317
this.vcfEncoder = new VCFEncoder(this.mHeader, this.allowMissingFieldsInHeader, this.writeFullFormatField);
301318
}
302319

303-
// writing vcf v4.3 is not implemented
304-
private static void rejectVCFV43Headers(final VCFHeader targetHeader) {
305-
if (targetHeader.getVCFHeaderVersion() != null && targetHeader.getVCFHeaderVersion().isAtLeastAsRecentAs(VCFHeaderVersion.VCF4_3)) {
306-
throw new IllegalArgumentException(String.format("Writing VCF version %s is not implemented", targetHeader.getVCFHeaderVersion()));
307-
}
308-
309-
320+
public void setTransitionPolicy(final VCF42To43VersionTransitionPolicy transitionPolicy) {
321+
this.transitionPolicy = transitionPolicy;
310322
}
311323
}

src/main/java/htsjdk/variant/variantcontext/writer/VariantContextWriterBuilder.java

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,8 @@ public enum OutputType {
132132
private boolean createMD5 = Defaults.CREATE_MD5;
133133
protected EnumSet<Options> options = DEFAULT_OPTIONS.clone();
134134

135+
private VCF42To43VersionTransitionPolicy vcf42To43VersionTransitionPolicy;
136+
135137
/**
136138
* Default constructor. Adds <code>USE_ASYNC_IO</code> to the Options if it is present in Defaults.
137139
*/
@@ -329,6 +331,11 @@ public VariantContextWriterBuilder setOptions(final EnumSet<Options> options) {
329331
return this;
330332
}
331333

334+
public VariantContextWriterBuilder setVCF42to43TransitionPolicy(final VCF42To43VersionTransitionPolicy policy) {
335+
this.vcf42To43VersionTransitionPolicy = policy;
336+
return this;
337+
}
338+
332339
/**
333340
* Add one option to the set of <code>Options</code> for the <code>VariantContextWriterBuilder</code>, if it's not already present.
334341
*
@@ -569,20 +576,23 @@ private static boolean isCompressedVCF(final Path outPath) {
569576
}
570577

571578
private VariantContextWriter createVCFWriter(final Path writerPath, final OutputStream writerStream) {
579+
final VCFWriter writer;
572580
if (idxCreator == null) {
573-
return new VCFWriter(writerPath, writerStream, refDict,
581+
writer = new VCFWriter(writerPath, writerStream, refDict,
574582
options.contains(Options.INDEX_ON_THE_FLY),
575583
options.contains(Options.DO_NOT_WRITE_GENOTYPES),
576584
options.contains(Options.ALLOW_MISSING_FIELDS_IN_HEADER),
577585
options.contains(Options.WRITE_FULL_FORMAT_FIELD));
578586
}
579587
else {
580-
return new VCFWriter(writerPath, writerStream, refDict, idxCreator,
588+
writer = new VCFWriter(writerPath, writerStream, refDict, idxCreator,
581589
options.contains(Options.INDEX_ON_THE_FLY),
582590
options.contains(Options.DO_NOT_WRITE_GENOTYPES),
583591
options.contains(Options.ALLOW_MISSING_FIELDS_IN_HEADER),
584592
options.contains(Options.WRITE_FULL_FORMAT_FIELD));
585593
}
594+
writer.setTransitionPolicy(vcf42To43VersionTransitionPolicy);
595+
return writer;
586596
}
587597

588598
private VariantContextWriter createBCFWriter(final Path writerPath, final OutputStream writerStream) {

src/main/java/htsjdk/variant/vcf/VCFCompoundHeaderLine.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,8 +166,13 @@ public String getVersion() {
166166
*
167167
* @param vcfTargetVersion the version agains which to validate
168168
*/
169+
@Override
169170
public void validateForVersion(final VCFHeaderVersion vcfTargetVersion) {
170171
super.validateForVersion(vcfTargetVersion);
172+
// Let the 1000 Genomes line through, but only for INFO lines
173+
if (this instanceof VCFInfoHeaderLine && getID().equals(VCFConstants.THOUSAND_GENOMES_KEY)) {
174+
return;
175+
}
171176
if (vcfTargetVersion.isAtLeastAsRecentAs(VCFHeaderVersion.VCF4_3)) {
172177
getGenericFields().forEach((k, v) -> {
173178
if (!VALID_HEADER_ID_PATTERN.matcher(k).matches()) {

src/main/java/htsjdk/variant/vcf/VCFEncoder.java

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,7 @@
2626
*/
2727
public class VCFEncoder {
2828

29-
/**
30-
* The encoding used for VCF files: ISO-8859-1. When writing VCF4.3 is implemented, this should change to UTF-8.
31-
*/
32-
public static final Charset VCF_CHARSET = StandardCharsets.ISO_8859_1;
29+
public static final Charset VCF_CHARSET = StandardCharsets.UTF_8;
3330
private static final String QUAL_FORMAT_STRING = "%.2f";
3431
private static final String QUAL_FORMAT_EXTENSION_TO_TRIM = ".00";
3532

@@ -41,6 +38,8 @@ public class VCFEncoder {
4138

4239
private boolean outputTrailingFormatFields = false;
4340

41+
private VCFTextTransformer vcfTextTransformer;
42+
4443
/**
4544
* Prepare a VCFEncoder that will encode records appropriate to the given VCF header, optionally
4645
* allowing missing fields in the header.
@@ -52,6 +51,9 @@ public VCFEncoder(final VCFHeader header, final boolean allowMissingFieldsInHead
5251
this.header = header;
5352
this.allowMissingFieldsInHeader = allowMissingFieldsInHeader;
5453
this.outputTrailingFormatFields = outputTrailingFormatFields;
54+
this.vcfTextTransformer = header.getVCFHeaderVersion() != null && header.getVCFHeaderVersion().isAtLeastAsRecentAs(VCFHeaderVersion.VCF4_3)
55+
? new VCFPercentEncodedTextTransformer()
56+
: new VCFPassThruTextTransformer();
5557
}
5658

5759
/**
@@ -242,7 +244,7 @@ String formatVCFField(final Object val) {
242244
}
243245
result = sb.toString();
244246
} else {
245-
result = val.toString();
247+
result = vcfTextTransformer.encodeText(val.toString());
246248
}
247249

248250
return result;
@@ -310,7 +312,8 @@ public void addGenotypeData(final VariantContext vc, final Map<Allele, String> a
310312
* @param vcfoutput VCF output
311313
* @throws IOException
312314
*/
313-
private void appendGenotypeData(final VariantContext vc, final Map<Allele, String> alleleMap, final List<String> genotypeFormatKeys, final Appendable vcfoutput) throws IOException {final int ploidy = vc.getMaxPloidy(2);
315+
private void appendGenotypeData(final VariantContext vc, final Map<Allele, String> alleleMap, final List<String> genotypeFormatKeys, final Appendable vcfoutput) throws IOException {
316+
final int ploidy = vc.getMaxPloidy(2);
314317

315318
for (final String sample : this.header.getGenotypeSamples()) {
316319
vcfoutput.append(VCFConstants.FIELD_SEPARATOR);

src/main/java/htsjdk/variant/vcf/VCFHeader.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@
5757
public class VCFHeader implements Serializable {
5858
public static final long serialVersionUID = 1L;
5959
protected static final Log logger = Log.getInstance(VCFHeader.class);
60-
public static final VCFHeaderVersion DEFAULT_VCF_VERSION = VCFHeaderVersion.VCF4_2;
60+
public static final VCFHeaderVersion DEFAULT_VCF_VERSION = VCFHeaderVersion.VCF4_3;
6161

6262
// the mandatory header fields
6363
public enum HEADER_FIELDS {

0 commit comments

Comments
 (0)