From f469e3798dde327b9f2bbdf7ed21c1f44c18d444 Mon Sep 17 00:00:00 2001 From: Chris Norman Date: Mon, 23 May 2022 08:54:38 -0400 Subject: [PATCH] Require sequence dictionary MD5s when writing CRAM. --- .../samtools/CRAMContainerStreamWriter.java | 1 + .../htsjdk/samtools/SAMFileWriterFactory.java | 41 ++++++- .../samtools/SAMSequenceDictionary.java | 100 ++++++++++++++++++ .../htsjdk/samtools/SAMSequenceRecord.java | 23 +++- .../htsjdk/samtools/CRAMBAIIndexerTest.java | 5 - .../htsjdk/samtools/CRAMCRAIIndexerTest.java | 4 +- .../htsjdk/samtools/CRAMComplianceTest.java | 6 +- .../CRAMContainerStreamWriterTest.java | 19 +++- .../htsjdk/samtools/CRAMFileWriterTest.java | 14 ++- .../samtools/CRAMFileWriterWithIndexTest.java | 8 +- .../samtools/CRAMIndexPermutationsTests.java | 5 +- .../java/htsjdk/samtools/CRAMMergerTest.java | 6 +- .../htsjdk/samtools/CRAMSliceMD5Test.java | 2 +- .../java/htsjdk/samtools/CRAMTestUtils.java | 24 ++++- .../samtools/SAMFileWriterFactoryTest.java | 46 +++++++- .../htsjdk/samtools/SAMRecordUnitTest.java | 2 +- .../samtools/cram/LosslessRoundTripTest.java | 1 + .../structure/CRAMStructureTestHelper.java | 3 +- .../cram/NA12878.20.21.unmapped.orig.bai | Bin 9936 -> 9936 bytes .../cram/NA12878.20.21.unmapped.orig.bam | Bin 2089981 -> 2087566 bytes 20 files changed, 274 insertions(+), 36 deletions(-) diff --git a/src/main/java/htsjdk/samtools/CRAMContainerStreamWriter.java b/src/main/java/htsjdk/samtools/CRAMContainerStreamWriter.java index d8c1dd9568..a786bf4330 100644 --- a/src/main/java/htsjdk/samtools/CRAMContainerStreamWriter.java +++ b/src/main/java/htsjdk/samtools/CRAMContainerStreamWriter.java @@ -89,6 +89,7 @@ public CRAMContainerStreamWriter( this.cramIndexer = indexer; this.outputStreamIdentifier = outputIdentifier; this.containerFactory = new ContainerFactory(samFileHeader, encodingStrategy, referenceSource); + samFileHeader.getSequenceDictionary().requireMD5sOrThrow(outputIdentifier); } /** diff --git a/src/main/java/htsjdk/samtools/SAMFileWriterFactory.java b/src/main/java/htsjdk/samtools/SAMFileWriterFactory.java index b9bd1dcb14..b3dad09a58 100644 --- a/src/main/java/htsjdk/samtools/SAMFileWriterFactory.java +++ b/src/main/java/htsjdk/samtools/SAMFileWriterFactory.java @@ -23,9 +23,10 @@ */ package htsjdk.samtools; +import htsjdk.io.HtsPath; +import htsjdk.io.IOPath; import htsjdk.samtools.cram.ref.CRAMReferenceSource; import htsjdk.samtools.cram.ref.ReferenceSource; -import htsjdk.samtools.cram.structure.CRAMEncodingStrategy; import htsjdk.samtools.util.BlockCompressedOutputStream; import htsjdk.samtools.util.FileExtensions; import htsjdk.samtools.util.IOUtil; @@ -36,9 +37,11 @@ import java.io.File; import java.io.IOException; +import java.io.InputStream; import java.io.OutputStream; import java.nio.file.Files; import java.nio.file.Path; +import java.util.List; import java.util.zip.Deflater; import static htsjdk.samtools.SamReader.Type.*; @@ -634,6 +637,7 @@ private CRAMFileWriter createCRAMWriterWithSettings( final Path referenceFasta) { final CRAMReferenceSource referenceSource; + SAMFileHeader repairedHeader = header; if (referenceFasta == null) { log.info("Reference fasta is not provided when writing CRAM file " + outputFile.toUri().toString()); log.info("Will attempt to use a default reference or download as set by defaults:"); @@ -642,6 +646,10 @@ private CRAMFileWriter createCRAMWriterWithSettings( referenceSource = ReferenceSource.getDefaultCRAMReferenceSource(); } else { + // CRAMs are required to have a SAMHeader with a sequence dictionary that contains MD5 + // checksums for each sequence. Check the proposed dictionary to see if it has MD5s, and + // if not, attempt to update it with MD5s from the reference dictionary, if one exists. + repairedHeader = repairSequenceMD5s(header, new HtsPath(referenceFasta.toUri().toString())); referenceSource = new ReferenceSource(referenceFasta); } OutputStream cramOS = null; @@ -674,7 +682,7 @@ private CRAMFileWriter createCRAMWriterWithSettings( indexOS, presorted, referenceSource, - header, + repairedHeader, outputFile.toUri().toString()); setCRAMWriterDefaults(writer); @@ -687,6 +695,35 @@ private void setCRAMWriterDefaults(final CRAMFileWriter writer) { //writer.setEncodingParams(new CRAMEncodingStrategy()); } + // If any sequence records don't contain MD5s, attempt to repair them by consulting the reference's + // dictionary file, if one exists. + private SAMFileHeader repairSequenceMD5s(final SAMFileHeader header, final IOPath referenceFasta) { + final List missingMD5s = header.getSequenceDictionary().getSequencesWithMissingMD5s(); + if (!missingMD5s.isEmpty()) { + final String missingMD5Message = SAMSequenceDictionary.createFormattedMD5Message("SAM header", missingMD5s); + log.warn(String.format( + "%s Attempting to use the reference dictionary to repair missing MD5s required for CRAM output.", + missingMD5Message)); + final IOPath referenceDictionary = SAMSequenceDictionary.getFastaDictionaryFileName(referenceFasta); + try (final InputStream fastaDictionaryStream = referenceDictionary.getInputStream()) { + final SAMSequenceDictionary newDictionary = + SAMSequenceDictionary.loadSAMSequenceDictionary(fastaDictionaryStream); + newDictionary.requireMD5sOrThrow(referenceFasta.toString()); + final SAMFileHeader repairedHeader = header.clone(); + repairedHeader.setSequenceDictionary(newDictionary); + return repairedHeader; + } catch (final IOException | RuntimeException e) { + throw new RuntimeException( + String.format( + "The attempt to repair the missing sequence MD5s (%s) using the reference dictionary %s failed.", + missingMD5Message, + referenceDictionary), + e); + } + } + return header; + } + @Override public String toString() { return "SAMFileWriterFactory [createIndex=" + createIndex + ", createMd5File=" + createMd5File + ", useAsyncIo=" diff --git a/src/main/java/htsjdk/samtools/SAMSequenceDictionary.java b/src/main/java/htsjdk/samtools/SAMSequenceDictionary.java index cf40fe6532..7243d01499 100644 --- a/src/main/java/htsjdk/samtools/SAMSequenceDictionary.java +++ b/src/main/java/htsjdk/samtools/SAMSequenceDictionary.java @@ -24,8 +24,15 @@ package htsjdk.samtools; import htsjdk.beta.plugin.HtsHeader; +import htsjdk.io.HtsPath; +import htsjdk.io.IOPath; +import htsjdk.samtools.util.BufferedLineReader; +import htsjdk.samtools.util.FileExtensions; import htsjdk.samtools.util.Log; +import htsjdk.samtools.util.RuntimeIOException; +import java.io.IOException; +import java.io.InputStream; import java.io.Serializable; import java.math.BigInteger; import java.security.MessageDigest; @@ -294,6 +301,99 @@ public String md5() { } } + + /** + * Given a fasta filename, return the name of the corresponding dictionary file. + */ + public static IOPath getFastaDictionaryFileName(IOPath fastaFile) { + final String fastaName = fastaFile.getURIString(); + int lastDot = fastaName.lastIndexOf('.'); + return new HtsPath(fastaName.substring(0, lastDot) + FileExtensions.DICT); + } + + /** + * Given a fasta dictionary file, returns its sequence dictionary + * + * @param fastaDictionary fasta dictionary file + * @return the SAMSequenceDictionary from fastaDictionaryFile + */ + public static SAMSequenceDictionary loadSAMSequenceDictionary( final IOPath fastaDictionary ) { + try ( final InputStream fastaDictionaryStream = fastaDictionary.getInputStream() ) { + return loadSAMSequenceDictionary(fastaDictionaryStream); + } + catch ( IOException e ) { + throw new RuntimeIOException("Error loading fasta dictionary file " + fastaDictionary, e); + } + } + + /** + * Given an InputStream connected to a fasta dictionary, returns its sequence dictionary + * + * Note: does not close the InputStream it's passed + * + * @param fastaDictionaryStream InputStream connected to a fasta dictionary + * @return the SAMSequenceDictionary from the fastaDictionaryStream + */ + public static SAMSequenceDictionary loadSAMSequenceDictionary( final InputStream fastaDictionaryStream ) { + // Don't close the reader when we're done, since we don't want to close the client's InputStream for them + final BufferedLineReader reader = new BufferedLineReader(fastaDictionaryStream); + + final SAMTextHeaderCodec codec = new SAMTextHeaderCodec(); + final SAMFileHeader header = codec.decode(reader, fastaDictionaryStream.toString()); + + // Make sure we have a valid sequence dictionary before continuing: + if (header.getSequenceDictionary() == null || header.getSequenceDictionary().isEmpty()) { + throw new RuntimeException ( + "Could not read sequence dictionary from given fasta stream " + + fastaDictionaryStream + ); + } + + return header.getSequenceDictionary(); + } + + final void requireMD5sOrThrow(final String contextMessage) { + final List missingMD5s = getSequencesWithMissingMD5s(); + if (!missingMD5s.isEmpty()) { + throw new RuntimeException(SAMSequenceDictionary.createFormattedMD5Message(contextMessage, missingMD5s)); + } + } + + /** + * @return all sequences in this dictionary that do not have an MD5 attribute + */ + List getSequencesWithMissingMD5s() { + return getSequences().stream().filter( + seqRec -> { + final String MD5 = seqRec.getMd5(); + return MD5 == null || MD5.length() == 0; + }).collect(Collectors.toList()); + } + + /** + * Create a formatted error string message describing which MD5s are missing from the sequence dictionary. + * + * @param contextID a string recognizable to the user describing the input that is the source of the failure + * @param badSequenceRecords the sequence with missing MD5s + * @return a message string suitable for presentation to the user + */ + static String createFormattedMD5Message(final String contextID, List badSequenceRecords) { + final int MAX_ERRORS_REPORTED = 10; + if (badSequenceRecords.size() != 0) { + return String.format( + "The sequence dictionary for %s is missing the required MD5 checksum for some contigs: %s%s.", + contextID, + badSequenceRecords.stream() + .limit(Integer.min(badSequenceRecords.size(), MAX_ERRORS_REPORTED)) + .map(SAMSequenceRecord::getSequenceName) + .collect(Collectors.joining(",")), + badSequenceRecords.size() > MAX_ERRORS_REPORTED ? + " and others": + ""); + } + return null; + } + @Override public int hashCode() { return mSequences.hashCode(); diff --git a/src/main/java/htsjdk/samtools/SAMSequenceRecord.java b/src/main/java/htsjdk/samtools/SAMSequenceRecord.java index 227f704f48..f2517ae009 100644 --- a/src/main/java/htsjdk/samtools/SAMSequenceRecord.java +++ b/src/main/java/htsjdk/samtools/SAMSequenceRecord.java @@ -24,16 +24,16 @@ package htsjdk.samtools; import htsjdk.samtools.util.Locatable; -import htsjdk.samtools.util.StringUtil; +import htsjdk.samtools.util.SequenceUtil; import java.math.BigInteger; -import java.util.ArrayList; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.LinkedHashSet; -import java.util.List; import java.util.Map; import java.util.Set; import java.util.regex.Pattern; @@ -148,6 +148,23 @@ public SAMSequenceRecord setMd5(final String value) { return this; } + /** + * Set the M5 attribute for this sequence using the provided sequenceBases. + * @param sequenceBases sequence bases to use to compute the MD5 for this sequence + * @return the update sequence record + */ + public SAMSequenceRecord setComputedMd5(final byte[] sequenceBases) { + try { + final MessageDigest md5Digester = MessageDigest.getInstance("MD5"); + if (md5Digester != null) { + setMd5(SequenceUtil.md5DigestToString(md5Digester.digest())); + } + } catch (final NoSuchAlgorithmException e) { + throw new RuntimeException("Error getting MD5 algorithm for sequence record MD5 computation", e); + } + return this; + } + public String getDescription() { return getAttribute(DESCRIPTION_TAG); } diff --git a/src/test/java/htsjdk/samtools/CRAMBAIIndexerTest.java b/src/test/java/htsjdk/samtools/CRAMBAIIndexerTest.java index 133c8edd89..1f7a3e860a 100644 --- a/src/test/java/htsjdk/samtools/CRAMBAIIndexerTest.java +++ b/src/test/java/htsjdk/samtools/CRAMBAIIndexerTest.java @@ -140,11 +140,6 @@ private void testMultipleContainerStream() throws IOException { final int refId1 = 0; final int refId2 = 1; - // for each ref, we alternate unmapped-placed with mapped - - final int expectedMapped = 1; - final int expectedUnmappedPlaced = 2; - try (final ByteArrayOutputStream contentStream = new ByteArrayOutputStream(); final ByteArrayOutputStream indexStream = new ByteArrayOutputStream()) { final CRAMContainerStreamWriter cramContainerStreamWriter = new CRAMContainerStreamWriter( diff --git a/src/test/java/htsjdk/samtools/CRAMCRAIIndexerTest.java b/src/test/java/htsjdk/samtools/CRAMCRAIIndexerTest.java index 4ded9ea581..6009b30acf 100644 --- a/src/test/java/htsjdk/samtools/CRAMCRAIIndexerTest.java +++ b/src/test/java/htsjdk/samtools/CRAMCRAIIndexerTest.java @@ -43,13 +43,13 @@ private void testCRAIIndexer(Index index) throws IOException { @Test public void testMultiRefContainer() throws IOException { - final SAMFileHeader samFileHeader = new SAMFileHeader(); + SAMFileHeader samFileHeader = new SAMFileHeader(); samFileHeader.setSortOrder(SAMFileHeader.SortOrder.coordinate); samFileHeader.addSequence(new SAMSequenceRecord("1", 10)); samFileHeader.addSequence(new SAMSequenceRecord("2", 10)); samFileHeader.addSequence(new SAMSequenceRecord("3", 10)); - + samFileHeader = CRAMTestUtils.addFakeSequenceMD5s(samFileHeader); final ReferenceSource source = new ReferenceSource(new FakeReferenceSequenceFile(samFileHeader.getSequenceDictionary().getSequences())); byte[] cramBytes; diff --git a/src/test/java/htsjdk/samtools/CRAMComplianceTest.java b/src/test/java/htsjdk/samtools/CRAMComplianceTest.java index 790a72e5cd..70063227f5 100644 --- a/src/test/java/htsjdk/samtools/CRAMComplianceTest.java +++ b/src/test/java/htsjdk/samtools/CRAMComplianceTest.java @@ -179,7 +179,7 @@ private void doComplianceTest( // write them to a cram stream final ByteArrayOutputStream baos = new ByteArrayOutputStream(); final ReferenceSource source = new ReferenceSource(t.refFile); - final CRAMFileWriter cramFileWriter = new CRAMFileWriter(baos, source, samFileHeader, name); + final CRAMFileWriter cramFileWriter = new CRAMFileWriter(baos, source, CRAMTestUtils.addFakeSequenceMD5s(samFileHeader), name); for (SAMRecord samRecord : samRecords) { cramFileWriter.addAlignment(samRecord); } @@ -331,7 +331,7 @@ public void testBAMThroughCRAMRoundTrip(final String testFileName, final String final File tempCRAMFile = File.createTempFile("testBAMThroughCRAMRoundTrip", FileExtensions.CRAM); tempCRAMFile.deleteOnExit(); SAMFileHeader samHeader = getFileHeader(originalBAMInputFile, referenceFile); - writeRecordsToFile(originalBAMRecords, tempCRAMFile, referenceFile, samHeader); + writeRecordsToFile(originalBAMRecords, tempCRAMFile, referenceFile, CRAMTestUtils.addFakeSequenceMD5s(samHeader)); // read the CRAM records back in and compare to the original BAM records List cramRecords = getSAMRecordsFromFile(tempCRAMFile, referenceFile); @@ -359,7 +359,7 @@ public void testBAMThroughCRAMRoundTripViaPath() throws IOException { try (FileSystem jimfs = Jimfs.newFileSystem(Configuration.unix())) { final Path tempCRAM = jimfs.getPath("testBAMThroughCRAMRoundTrip" + FileExtensions.CRAM); SAMFileHeader samHeader = getFileHeader(originalBAMInputFile, referenceFile); - writeRecordsToPath(originalBAMRecords, tempCRAM, referenceFile, samHeader); + writeRecordsToPath(originalBAMRecords, tempCRAM, referenceFile, CRAMTestUtils.addFakeSequenceMD5s(samHeader)); // read the CRAM records back in and compare to the original BAM records List cramRecords = getSAMRecordsFromPath(tempCRAM, referenceFile); diff --git a/src/test/java/htsjdk/samtools/CRAMContainerStreamWriterTest.java b/src/test/java/htsjdk/samtools/CRAMContainerStreamWriterTest.java index db76a29c04..fe8dd6aaf0 100644 --- a/src/test/java/htsjdk/samtools/CRAMContainerStreamWriterTest.java +++ b/src/test/java/htsjdk/samtools/CRAMContainerStreamWriterTest.java @@ -58,7 +58,7 @@ private ReferenceSource createReferenceSource() { } private void doTest(final List samRecords, final ByteArrayOutputStream outStream, final OutputStream indexStream) { - final SAMFileHeader header = createSAMHeader(SAMFileHeader.SortOrder.coordinate); + final SAMFileHeader header = CRAMTestUtils.addFakeSequenceMD5s(createSAMHeader(SAMFileHeader.SortOrder.coordinate)); final ReferenceSource refSource = createReferenceSource(); final CRAMContainerStreamWriter containerStream = new CRAMContainerStreamWriter(outStream, indexStream, refSource, header, "test"); @@ -102,7 +102,7 @@ public void testCRAMContainerStreamNoIndex() { @Test(description = "Test CRAMContainerStream aggregating multiple partitions") public void testCRAMContainerAggregatePartitions() throws IOException { - final SAMFileHeader header = createSAMHeader(SAMFileHeader.SortOrder.coordinate); + final SAMFileHeader header = CRAMTestUtils.addFakeSequenceMD5s(createSAMHeader(SAMFileHeader.SortOrder.coordinate)); final ReferenceSource refSource = createReferenceSource(); // create a bunch of records and write them out to separate streams in groups @@ -161,7 +161,7 @@ public void testCRAMContainerStreamWithBaiIndex() throws IOException { @Test(description = "Test CRAMContainerStream with crai index") public void testCRAMContainerStreamWithCraiIndex() throws IOException { final List samRecords = createRecords(100); - final SAMFileHeader header = createSAMHeader(SAMFileHeader.SortOrder.coordinate); + final SAMFileHeader header = CRAMTestUtils.addFakeSequenceMD5s(createSAMHeader(SAMFileHeader.SortOrder.coordinate)); try (ByteArrayOutputStream outStream = new ByteArrayOutputStream(); ByteArrayOutputStream indexStream = new ByteArrayOutputStream()) { doTestWithIndexer(samRecords, outStream, header, new CRAMCRAIIndexer(indexStream, header)); @@ -171,6 +171,19 @@ public void testCRAMContainerStreamWithCraiIndex() throws IOException { } } + @Test(expectedExceptions=RuntimeException.class) + public void testRejectDictionaryWithMissingMD5s() { + final SAMFileHeader samHeader = createSAMHeader(SAMFileHeader.SortOrder.coordinate); + final ByteArrayOutputStream outStream = new ByteArrayOutputStream(); + final ReferenceSource refSource = createReferenceSource(); + try { + new CRAMContainerStreamWriter(outStream, null, refSource, samHeader, "test"); + } catch (final RuntimeException e) { + Assert.assertTrue(e.getMessage().contains("missing the required MD5 checksum")); + throw e; + } + } + private void checkCRAMContainerStream(ByteArrayOutputStream outStream, ByteArrayOutputStream indexStream, String indexExtension) throws IOException { // write the file out final File cramTempFile = File.createTempFile("cramContainerStreamTest", ".cram"); diff --git a/src/test/java/htsjdk/samtools/CRAMFileWriterTest.java b/src/test/java/htsjdk/samtools/CRAMFileWriterTest.java index c61f9acd49..a14fdfe277 100644 --- a/src/test/java/htsjdk/samtools/CRAMFileWriterTest.java +++ b/src/test/java/htsjdk/samtools/CRAMFileWriterTest.java @@ -170,7 +170,7 @@ private void doTest(final List samRecords) { final ReferenceSource refSource = createReferenceSource(); final ByteArrayOutputStream os = new ByteArrayOutputStream(); - try (CRAMFileWriter writer = new CRAMFileWriter(os, refSource, header, null)) { + try (CRAMFileWriter writer = new CRAMFileWriter(os, refSource, CRAMTestUtils.addFakeSequenceMD5s(header), null)) { writeRecordsToCRAM(writer, samRecords); } @@ -185,7 +185,7 @@ public void testCRAMWriterWithIndex() { final ByteArrayOutputStream indexStream = new ByteArrayOutputStream(); final List samRecords = createRecords(100); - try (CRAMFileWriter writer = new CRAMFileWriter(outStream, indexStream, refSource, header, null)) { + try (CRAMFileWriter writer = new CRAMFileWriter(outStream, indexStream, refSource, CRAMTestUtils.addFakeSequenceMD5s(header), null)) { writeRecordsToCRAM(writer, samRecords); } @@ -201,7 +201,8 @@ public void testCRAMWriterNotPresorted() { final ByteArrayOutputStream indexStream = new ByteArrayOutputStream(); final List samRecords = createRecords(100); - try (CRAMFileWriter writer = new CRAMFileWriter(outStream, indexStream, false, refSource, header, null)) { + final SAMFileHeader repairedHeader = CRAMTestUtils.addFakeSequenceMD5s(header); + try (CRAMFileWriter writer = new CRAMFileWriter(outStream, indexStream, false, refSource, repairedHeader, null)) { // force records to not be coordinate sorted to ensure we're relying on presorted=false samRecords.sort(new SAMRecordCoordinateComparator().reversed()); @@ -246,7 +247,7 @@ public void test_roundtrip_tlen_preserved() throws IOException { final List records = new ArrayList<>(); try (SamReader reader = SamReaderFactory.make().open(new File(SAM_TOOLS_TEST_DIR, "cram_tlen_reads.sorted.sam")); - CRAMFileWriter writer = new CRAMFileWriter(baos, source, reader.getFileHeader(), "test.cram")) { + CRAMFileWriter writer = new CRAMFileWriter(baos, source, CRAMTestUtils.addFakeSequenceMD5s(reader.getFileHeader()), "test.cram")) { for (final SAMRecord record : reader) { writer.addAlignment(record); records.add(record); @@ -283,7 +284,10 @@ public void test_roundtrip_many_reads() throws IOException { IOUtil.deleteOnExit(ReferenceSequenceFileFactory.getDefaultDictionaryForReferenceSequence(newFasta)); final SAMRecordSetBuilder samRecordSetBuilder = new SAMRecordSetBuilder(); - samRecordSetBuilder.setHeader(SAMRecordSetBuilder.makeDefaultHeader(SAMFileHeader.SortOrder.coordinate, 10_000, true)); + samRecordSetBuilder.setHeader( + CRAMTestUtils.addFakeSequenceMD5s( + SAMRecordSetBuilder.makeDefaultHeader(SAMFileHeader.SortOrder.coordinate, 10_000, true)) + ); samRecordSetBuilder.writeRandomReference(newFasta); final List records = new ArrayList<>(); diff --git a/src/test/java/htsjdk/samtools/CRAMFileWriterWithIndexTest.java b/src/test/java/htsjdk/samtools/CRAMFileWriterWithIndexTest.java index 5f62340014..566f16bf46 100644 --- a/src/test/java/htsjdk/samtools/CRAMFileWriterWithIndexTest.java +++ b/src/test/java/htsjdk/samtools/CRAMFileWriterWithIndexTest.java @@ -199,12 +199,14 @@ public void beforeTest() { private static void addRandomSequence(SAMFileHeader header, int length, InMemoryReferenceSequenceFile rsf) { String name = String.valueOf(header.getSequenceDictionary().size() + 1); - header.addSequence(new SAMSequenceRecord(name, length)); + final SAMSequenceRecord sequenceRecord = new SAMSequenceRecord(name, length); byte[] refBases = new byte[length]; byte[] alphabet = "ACGTN".getBytes(); - for (int i = 0; i < refBases.length; i++) + for (int i = 0; i < refBases.length; i++) { refBases[i] = alphabet[random.nextInt(alphabet.length)]; - + } rsf.add(name, refBases); + sequenceRecord.setComputedMd5(refBases); + header.addSequence(sequenceRecord); } } diff --git a/src/test/java/htsjdk/samtools/CRAMIndexPermutationsTests.java b/src/test/java/htsjdk/samtools/CRAMIndexPermutationsTests.java index 39c9c42874..024b702206 100644 --- a/src/test/java/htsjdk/samtools/CRAMIndexPermutationsTests.java +++ b/src/test/java/htsjdk/samtools/CRAMIndexPermutationsTests.java @@ -24,11 +24,12 @@ public class CRAMIndexPermutationsTests extends HtsjdkTest { private static final File TEST_DATA_DIR = new File("src/test/resources/htsjdk/samtools/cram"); - // BAM test file for comparison with CRAM results. This is asubset of NA12878 + // BAM test file for comparison with CRAM results. This is a subset of NA12878 // (20:10000000-10004000, 21:10000000-10004000, +2000 unmapped). There is no corresponding // reference checked in for this file since its too big, but because we're only using it to validate // index query results, we only care that the right the reads are returned, not what the read bases are, so - // we use a synthetic in-memory fake reference of all 'N's to convert it to CRAM. + // we use a synthetic in-memory fake reference of all 'N's to convert it to CRAM. Note that the MD tags in + // this bam are also synthetic (fake/made up in order to satisfy the MD5 requirement for write). private static final File truthBAM = new File(TEST_DATA_DIR, "NA12878.20.21.unmapped.orig.bam"); //Note that these tests can be REALLY slow because although we don't use the real (huge) reference, we use a fake diff --git a/src/test/java/htsjdk/samtools/CRAMMergerTest.java b/src/test/java/htsjdk/samtools/CRAMMergerTest.java index 34f793cc84..8a5b3858e6 100644 --- a/src/test/java/htsjdk/samtools/CRAMMergerTest.java +++ b/src/test/java/htsjdk/samtools/CRAMMergerTest.java @@ -208,7 +208,11 @@ public void test() throws IOException { // 1. Read an input CRAM and write it out in partitioned form (header, parts, terminator) ReferenceSource referenceSource = new ReferenceSource(CRAM_REF); try (SamReader samReader = SamReaderFactory.makeDefault().referenceSource(referenceSource).open(CRAM_FILE); - PartitionedCRAMFileWriter partitionedCRAMFileWriter = new PartitionedCRAMFileWriter(outputDir, referenceSource, samReader.getFileHeader(), 250)) { + PartitionedCRAMFileWriter partitionedCRAMFileWriter = new PartitionedCRAMFileWriter( + outputDir, + referenceSource, + CRAMTestUtils.addFakeSequenceMD5s(samReader.getFileHeader()), + 250)) { for (SAMRecord samRecord : samReader) { partitionedCRAMFileWriter.addAlignment(samRecord); } diff --git a/src/test/java/htsjdk/samtools/CRAMSliceMD5Test.java b/src/test/java/htsjdk/samtools/CRAMSliceMD5Test.java index a80ea59035..b85fe9575d 100644 --- a/src/test/java/htsjdk/samtools/CRAMSliceMD5Test.java +++ b/src/test/java/htsjdk/samtools/CRAMSliceMD5Test.java @@ -132,7 +132,7 @@ record = new SAMRecord(samFileHeader); // write a valid CRAM with a valid reference source: final ByteArrayOutputStream baos = new ByteArrayOutputStream(); - try (final CRAMFileWriter writer = new CRAMFileWriter(baos, referenceSourceUpperCased, samFileHeader, "test")) { + try (final CRAMFileWriter writer = new CRAMFileWriter(baos, referenceSourceUpperCased, CRAMTestUtils.addFakeSequenceMD5s(samFileHeader), "test")) { writer.addAlignment(record); } cramData = baos.toByteArray(); diff --git a/src/test/java/htsjdk/samtools/CRAMTestUtils.java b/src/test/java/htsjdk/samtools/CRAMTestUtils.java index 2476ce7783..ba1f8a0a01 100644 --- a/src/test/java/htsjdk/samtools/CRAMTestUtils.java +++ b/src/test/java/htsjdk/samtools/CRAMTestUtils.java @@ -10,6 +10,7 @@ import java.nio.file.Files; import java.util.Arrays; import java.util.Collection; +import java.util.List; public final class CRAMTestUtils { @@ -45,7 +46,7 @@ public static long writeToCRAMWithEncodingStrategy( null, true, referenceSource, - reader.getFileHeader(), + CRAMTestUtils.addFakeSequenceMD5s(reader.getFileHeader()), tempOutputCRAM.getName()); final SAMRecordIterator inputIterator = reader.iterator(); while (inputIterator.hasNext()) { @@ -67,7 +68,7 @@ public static CRAMFileReader writeAndReadFromInMemoryCram(Collection refFile.add("chr1", ref); ReferenceSource source = new ReferenceSource(refFile); final SAMFileHeader header = records.iterator().next().getHeader(); - return writeAndReadFromInMemoryCram(records, source, header); + return writeAndReadFromInMemoryCram(records, source, CRAMTestUtils.addFakeSequenceMD5s(header)); } /** @@ -76,7 +77,7 @@ public static CRAMFileReader writeAndReadFromInMemoryCram(Collection * @return a CRAMFileReader reading from an in memory buffer that has had the records written into it, uses a fake reference with all A's */ public static CRAMFileReader writeAndReadFromInMemoryCram(SAMRecordSetBuilder records) throws IOException { - return writeAndReadFromInMemoryCram(records.getRecords(), getFakeReferenceSource(), records.getHeader()); + return writeAndReadFromInMemoryCram(records.getRecords(), getFakeReferenceSource(), CRAMTestUtils.addFakeSequenceMD5s(records.getHeader())); } private static CRAMFileReader writeAndReadFromInMemoryCram(Collection records, CRAMReferenceSource source, SAMFileHeader header) throws IOException { @@ -102,4 +103,21 @@ public static CRAMReferenceSource getFakeReferenceSource() { return bases; }; } + + // Synthesize fake MD5 values for test header sequences for which we don't have actual references + // (only fake in-memory references) or for which we can't re-create the CRAM, in order to satisfy + // the CRAM writer's requirement that all sequence dictionaries have MD5 checksums. + public static SAMFileHeader addFakeSequenceMD5s(final SAMFileHeader header) { + final String FAKE_MD5 = "deadbeefdeadbeefdeadbeefdeadbeef"; + final SAMSequenceDictionary headerDictionary = header.getSequenceDictionary(); + if (!headerDictionary.getSequencesWithMissingMD5s().isEmpty()) { + final SAMFileHeader repairedHeader = header.clone(); + final SAMSequenceDictionary newDictionary = repairedHeader.getSequenceDictionary(); + final List badSequences = newDictionary.getSequencesWithMissingMD5s(); + badSequences.forEach(seq -> seq.setMd5(FAKE_MD5)); + return repairedHeader; + } + return header; + } + } diff --git a/src/test/java/htsjdk/samtools/SAMFileWriterFactoryTest.java b/src/test/java/htsjdk/samtools/SAMFileWriterFactoryTest.java index 7046996167..ed9a71313b 100644 --- a/src/test/java/htsjdk/samtools/SAMFileWriterFactoryTest.java +++ b/src/test/java/htsjdk/samtools/SAMFileWriterFactoryTest.java @@ -26,6 +26,9 @@ import com.google.common.jimfs.Configuration; import com.google.common.jimfs.Jimfs; import htsjdk.HtsjdkTest; +import htsjdk.beta.io.IOPathUtils; +import htsjdk.io.HtsPath; +import htsjdk.io.IOPath; import htsjdk.samtools.cram.ref.ReferenceSource; import htsjdk.samtools.seekablestream.SeekableFileStream; import htsjdk.samtools.util.FileExtensions; @@ -255,7 +258,6 @@ private SAMFileWriterFactory createWriterFactoryWithOptions(final SAMFileHeader factory.setCreateMd5File(true); // index only created if coordinate sorted header.setSortOrder(SAMFileHeader.SortOrder.coordinate); - header.addSequence(new SAMSequenceRecord("chr1", 123)); header.addReadGroup(new SAMReadGroupRecord("1")); return factory; } @@ -439,6 +441,48 @@ public void testMakeCRAMWriterPresortedDefault() throws Exception { verifyWriterOutput(outputFile, new ReferenceSource(referenceFile), nRecs, true); } + @Test(expectedExceptions=RuntimeException.class) + public void testRejectMissingMD5WithNoReferenceDictionary() throws IOException { + final IOPath testCRAM = new HtsPath(TEST_DATA_DIR + "/cram/CEUTrio.HiSeq.WGS.b37.NA12878.20.21.10m-10m100.cram"); + final IOPath originalReference = new HtsPath("src/test/resources/htsjdk/samtools/reference/human_g1k_v37.20.21.fasta.gz"); + + // this reference has a reference dictionary, so copy it so it won't be found in order to test that + // the lack of a dictionary causes a cram with no MD5s to be rejected + final IOPath referenceCopy = IOPathUtils.createTempPath("fastaWithNoDictionary", ".fasta"); + final IOPath outputFile = IOPathUtils.createTempPath("fastaWithNoDictionaryTest", ".cram"); + IOUtil.copyFile(originalReference.toPath().toFile(), referenceCopy.toPath().toFile()); + + try (final SamReader samReader = SamReaderFactory.make().open(testCRAM.toPath().toFile()); + final SAMFileWriter samWriter = new SAMFileWriterFactory().makeWriter( + samReader.getFileHeader(), true, outputFile.toPath().toFile(), originalReference.toPath().toFile())) { + } catch (final RuntimeException e) { + Assert.assertTrue(e.getMessage().contains("The attempt to repair the missing sequence MD5s")); + throw e; + } + } + + @Test + public void testRepairMissingMD5sFromReferenceDictionary() throws IOException { + // use a CRAM that has a header sequence dictionary with no MD5s, and a corresponding reference + // that has a ref .dict + final IOPath testCRAM = new HtsPath(TEST_DATA_DIR + "/cram/cramQueryTest.cram"); + final IOPath reference = new HtsPath(TEST_DATA_DIR + "/cram/human_g1k_v37.20.21.1-100.fasta"); + + final IOPath outputFile = IOPathUtils.createTempPath("fastaWithNoDictionaryTest", ".cram"); + try (final SamReader samReader = SamReaderFactory.make().open(testCRAM.toPath().toFile()); + // no need to transfer the records, only the header + final SAMFileWriter samWriter = new SAMFileWriterFactory().makeWriter( + samReader.getFileHeader(), true, outputFile.toPath().toFile(), reference.toPath().toFile())) { + // make sure the original header has no MD5s, otherwise the test won't prove anything + Assert.assertTrue(!samReader.getFileHeader().getSequenceDictionary().getSequencesWithMissingMD5s().isEmpty()); + } + + //now make sure the newly written file contains md5s + try (final SamReader samReader = SamReaderFactory.make().open(outputFile.toPath().toFile())) { + Assert.assertTrue(samReader.getFileHeader().getSequenceDictionary().getSequencesWithMissingMD5s().isEmpty()); + } + } + @Test public void testAsync() throws IOException { final SAMFileWriterFactory builder = new SAMFileWriterFactory(); diff --git a/src/test/java/htsjdk/samtools/SAMRecordUnitTest.java b/src/test/java/htsjdk/samtools/SAMRecordUnitTest.java index 9b0ebdc6b1..548a5eb935 100644 --- a/src/test/java/htsjdk/samtools/SAMRecordUnitTest.java +++ b/src/test/java/htsjdk/samtools/SAMRecordUnitTest.java @@ -1161,7 +1161,7 @@ public void testWriteSamWithEmptyArray(Object emptyArray, Class arrayClass, S .setCreateMd5File(false) .setCreateIndex(false); final Path reference = IOUtil.getPath("src/test/resources/htsjdk/samtools/one-contig.fasta"); - try (final SAMFileWriter samFileWriter = writerFactory.makeWriter(samRecords.getHeader(), false, tmp, reference)) { + try (final SAMFileWriter samFileWriter = writerFactory.makeWriter(CRAMTestUtils.addFakeSequenceMD5s(samRecords.getHeader()), false, tmp, reference)) { samFileWriter.addAlignment(record); } diff --git a/src/test/java/htsjdk/samtools/cram/LosslessRoundTripTest.java b/src/test/java/htsjdk/samtools/cram/LosslessRoundTripTest.java index 1ae8e142aa..3478e9fdf1 100644 --- a/src/test/java/htsjdk/samtools/cram/LosslessRoundTripTest.java +++ b/src/test/java/htsjdk/samtools/cram/LosslessRoundTripTest.java @@ -24,6 +24,7 @@ public void test_MD_NM() throws IOException { samFileHeader.addSequence(new SAMSequenceRecord("1", 3)); samFileHeader.addReadGroup(new SAMReadGroupRecord("some read group")); + samFileHeader = CRAMTestUtils.addFakeSequenceMD5s(samFileHeader); CRAMFileWriter w = new CRAMFileWriter(baos, source, samFileHeader, null); SAMRecord record = new SAMRecord(samFileHeader); record.setReadName("name"); diff --git a/src/test/java/htsjdk/samtools/cram/structure/CRAMStructureTestHelper.java b/src/test/java/htsjdk/samtools/cram/structure/CRAMStructureTestHelper.java index ad6ca4f31b..ce7adf9ab2 100644 --- a/src/test/java/htsjdk/samtools/cram/structure/CRAMStructureTestHelper.java +++ b/src/test/java/htsjdk/samtools/cram/structure/CRAMStructureTestHelper.java @@ -418,7 +418,8 @@ private static SAMFileHeader createSAMFileHeader() { final List sequenceRecords = new ArrayList<>(); sequenceRecords.add(new SAMSequenceRecord("0", REFERENCE_CONTIG_LENGTH)); sequenceRecords.add(new SAMSequenceRecord("1", REFERENCE_CONTIG_LENGTH)); - final SAMFileHeader header = new SAMFileHeader(new SAMSequenceDictionary(sequenceRecords)); + final SAMFileHeader header = CRAMTestUtils.addFakeSequenceMD5s( + new SAMFileHeader(new SAMSequenceDictionary(sequenceRecords))); header.setSortOrder(SAMFileHeader.SortOrder.coordinate); return header; } diff --git a/src/test/resources/htsjdk/samtools/cram/NA12878.20.21.unmapped.orig.bai b/src/test/resources/htsjdk/samtools/cram/NA12878.20.21.unmapped.orig.bai index 2106f10cc7feae5933d4bfcf593f1963410e0c1c..7bace0354c4aaff80c90aacb1519d970793e237c 100644 GIT binary patch literal 9936 zcmeI&u?>ST5QX9AG*KF=REUBqQWl^?C=m@=gOWNKBx_`uEWj@KxHtkvK!1S_Bje&> zf5nOWtqTW9c0Jx+rQsNddR|}sDb_NxkyLxp=PZT!em$?ZMSky6JsWZ$2R}CCKo0nb zhd6)(IDi8s(KYukC&UrW!Z-kT%` naIp+t9U? literal 9936 zcmeI$u?Ye}5CzaVlf%SF5X8VpumQo~5XD5>FgCOi+prnCunp&27UWvEH?TV}yDaPr z|LJ&XCK1uE*F2)h-Y#;>dTjUiY4+>0oDb*g+3#9(rNym(L=hlBfB*pk1PBlyK!5-N z0t5&U7?D8D%>MJWnyc+L@mZxs&DVzW^Rhr4`cN)QzE&LU1PBlyK!5-N0t5&UAV7cs P0RrO@sF~TjEr0$3yT%Yx diff --git a/src/test/resources/htsjdk/samtools/cram/NA12878.20.21.unmapped.orig.bam b/src/test/resources/htsjdk/samtools/cram/NA12878.20.21.unmapped.orig.bam index 01ad319f8ba109d8d1d52faaa2acadc1b2880b86..aa10eeaa212d4ff1e791fb99d91df764bed48cc6 100644 GIT binary patch delta 16694 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