From 0f4f2d5e48f514f42e20657417ade2f7e3dacc78 Mon Sep 17 00:00:00 2001 From: Takuto Sato Date: Tue, 18 Apr 2023 18:27:46 -0400 Subject: [PATCH 01/14] first draft --- .../java/htsjdk/samtools/BamFileIoUtils.java | 56 +++++++++++++++++-- src/main/java/htsjdk/samtools/SAMUtils.java | 21 +++++-- 2 files changed, 66 insertions(+), 11 deletions(-) diff --git a/src/main/java/htsjdk/samtools/BamFileIoUtils.java b/src/main/java/htsjdk/samtools/BamFileIoUtils.java index 868d3f80ba..04c3c0087a 100644 --- a/src/main/java/htsjdk/samtools/BamFileIoUtils.java +++ b/src/main/java/htsjdk/samtools/BamFileIoUtils.java @@ -1,5 +1,6 @@ package htsjdk.samtools; +import htsjdk.samtools.cram.io.CountingInputStream; import htsjdk.samtools.util.BlockCompressedFilePointerUtil; import htsjdk.samtools.util.BlockCompressedInputStream; import htsjdk.samtools.util.BlockCompressedOutputStream; @@ -11,11 +12,8 @@ import htsjdk.samtools.util.Md5CalculatingOutputStream; import htsjdk.samtools.util.RuntimeIOException; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.OutputStream; +import java.io.*; +import java.nio.file.Files; import java.nio.file.Path; import java.util.List; @@ -65,6 +63,54 @@ public static void reheaderBamFile(final SAMFileHeader samFileHeader, final File } } + // tsato: let's do it. + public static void blockCopyBamFile(final Path inputFile, final OutputStream outputStream, final boolean skipHeader, final boolean skipTerminator) { + // FileInputStream in = null; + try (final CountingInputStream in = new CountingInputStream(Files.newInputStream(inputFile))){ + // in = new FileInputStream(inputFile); + + // check that a) the end of the file is valid and b) if there's a terminator block and not copy it if skipTerminator is true + final BlockCompressedInputStream.FileTermination term = BlockCompressedInputStream.checkTermination(inputFile); + if (term == BlockCompressedInputStream.FileTermination.DEFECTIVE) + throw new SAMException(inputFile + " does not have a valid GZIP block at the end of the file."); + + if (skipHeader) { + final long vOffsetOfFirstRecord = SAMUtils.findVirtualOffsetOfFirstRecordInBam(inputFile); // tsato: need to convert + final BlockCompressedInputStream blockIn = new BlockCompressedInputStream(IOUtil.openFileForReading(inputFile)); // tsato hmmm... + blockIn.seek(vOffsetOfFirstRecord); + final long remainingInBlock = blockIn.available(); + + // If we found the end of the header then write the remainder of this block out as a + // new gzip block and then break out of the while loop + if (remainingInBlock >= 0) { + final BlockCompressedOutputStream blockOut = new BlockCompressedOutputStream(outputStream, (Path)null); + IOUtil.transferByStream(blockIn, blockOut, remainingInBlock); + blockOut.flush(); + // Don't close blockOut because closing underlying stream would break everything + } + + long pos = BlockCompressedFilePointerUtil.getBlockAddress(blockIn.getFilePointer()); + blockIn.close(); + while (pos > 0) { + pos -= in.skip(pos); + } + } + + // Copy remainder of input stream into output stream + final long currentPos = in.getCount(); + // final long length = inputPath.toFile().length(); // tsato: this right? length of the file in bytes..vs size? -- see below + final long length = Files.size(inputFile); // tsato: rename to size + final long skipLast = ((term == BlockCompressedInputStream.FileTermination.HAS_TERMINATOR_BLOCK) && skipTerminator) ? + BlockCompressedStreamConstants.EMPTY_GZIP_BLOCK.length : 0; + final long bytesToWrite = length - skipLast - currentPos; + + IOUtil.transferByStream(in, outputStream, bytesToWrite); + } catch (final IOException ioe) { + throw new RuntimeIOException(ioe); + } + } + + /** * Copy data from a BAM file to an OutputStream by directly copying the gzip blocks * diff --git a/src/main/java/htsjdk/samtools/SAMUtils.java b/src/main/java/htsjdk/samtools/SAMUtils.java index f0535a7571..e06e111b08 100644 --- a/src/main/java/htsjdk/samtools/SAMUtils.java +++ b/src/main/java/htsjdk/samtools/SAMUtils.java @@ -23,19 +23,17 @@ */ package htsjdk.samtools; +import htsjdk.samtools.seekablestream.SeekablePathStream; import htsjdk.samtools.seekablestream.SeekableStream; -import htsjdk.samtools.util.BinaryCodec; -import htsjdk.samtools.util.CigarUtil; -import htsjdk.samtools.util.CloserUtil; -import htsjdk.samtools.util.CoordMath; -import htsjdk.samtools.util.RuntimeEOFException; -import htsjdk.samtools.util.StringUtil; +import htsjdk.samtools.util.*; import htsjdk.tribble.annotation.Strand; import java.io.File; import java.io.IOException; +import java.io.InputStream; import java.io.UnsupportedEncodingException; import java.math.BigInteger; +import java.nio.file.Path; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.ArrayList; @@ -677,6 +675,17 @@ public static int combineMapqs(int m1, int m2) { } + // tsatof: + public static long findVirtualOffsetOfFirstRecordInBam(final Path bamFile) { + try { + InputStream yo = IOUtil.openFileForReading(bamFile); + SeekableStream ss = new SeekablePathStream(bamFile); // tsato: best way? buffering needed? + return BAMFileReader.findVirtualOffsetOfFirstRecord(ss); + } catch (final IOException ioe) { + throw new RuntimeEOFException(ioe); + } + } + /** * Returns the virtual file offset of the first record in a BAM file - i.e. the virtual file * offset after skipping over the text header and the sequence records. From 72c8b17ca88f93f91ae8d0e7a5e950638b8602c4 Mon Sep 17 00:00:00 2001 From: Takuto Sato Date: Fri, 21 Apr 2023 18:19:12 -0400 Subject: [PATCH 02/14] fix compile errors --- .../java/htsjdk/samtools/BamFileIoUtils.java | 29 +++++++++++++++---- .../util/BlockCompressedInputStream.java | 7 +++++ 2 files changed, 31 insertions(+), 5 deletions(-) diff --git a/src/main/java/htsjdk/samtools/BamFileIoUtils.java b/src/main/java/htsjdk/samtools/BamFileIoUtils.java index 04c3c0087a..17d29e1954 100644 --- a/src/main/java/htsjdk/samtools/BamFileIoUtils.java +++ b/src/main/java/htsjdk/samtools/BamFileIoUtils.java @@ -15,6 +15,7 @@ import java.io.*; import java.nio.file.Files; import java.nio.file.Path; +import java.nio.file.Paths; import java.util.List; public class BamFileIoUtils { @@ -30,7 +31,13 @@ public static boolean isBamFile(final File file) { return ((file != null) && SamReader.Type.BAM_TYPE.hasValidFileExtension(file.getName())); } - public static void reheaderBamFile(final SAMFileHeader samFileHeader, final File inputFile, final File outputFile) { + // tsato: no longer needed +// public static void reheaderBamFile(final SAMFileHeader samFileHeader, final File inputFile, final File outputFile) { +// reheaderBamFile(samFileHeader, inputFile, outputFile, true, true); +// } + + // tsato + public static void reheaderBamFile(final SAMFileHeader samFileHeader, final Path inputFile, final Path outputFile) { reheaderBamFile(samFileHeader, inputFile, outputFile, true, true); } @@ -43,12 +50,12 @@ public static void reheaderBamFile(final SAMFileHeader samFileHeader, final File * @param createMd5 Whether or not to create an MD5 file for the new BAM * @param createIndex Whether or not to create an index file for the new BAM */ - public static void reheaderBamFile(final SAMFileHeader samFileHeader, final File inputFile, final File outputFile, final boolean createMd5, final boolean createIndex) { + public static void reheaderBamFile(final SAMFileHeader samFileHeader, final Path inputFile, final Path outputFile, final boolean createMd5, final boolean createIndex) { IOUtil.assertFileIsReadable(inputFile); - IOUtil.assertFileIsWritable(outputFile); + // IOUtil.assertFileIsWritable(outputFile); // tsato: what do I do with this... try { - BlockCompressedInputStream.assertNonDefectiveFile(inputFile); + BlockCompressedInputStream.assertNonDefectivePath(inputFile); assertSortOrdersAreEqual(samFileHeader, inputFile); final OutputStream outputStream = buildOutputStream(outputFile, createMd5, createIndex); @@ -207,6 +214,18 @@ public static void gatherWithBlockCopying(final List bams, final File outp } } + // tsato: consolidate as needed.... + private static OutputStream buildOutputStream(final Path outputFile, final boolean createMd5, final boolean createIndex) throws IOException { + OutputStream outputStream = Files.newOutputStream(outputFile); + if (createMd5) { + outputStream = new Md5CalculatingOutputStream(outputStream, Paths.get(outputFile + ".md")); // tsato: is this right? + } + if (createIndex) { + outputStream = new StreamInflatingIndexingOutputStream(outputStream, Paths.get(IOUtil.basename(outputFile.toFile()) + FileExtensions.BAI_INDEX)); // tsato: what happens when I run toFile on a cloud file... + } + return outputStream; + } + private static OutputStream buildOutputStream(final File outputFile, final boolean createMd5, final boolean createIndex) throws IOException { OutputStream outputStream = new FileOutputStream(outputFile); if (createMd5) { @@ -218,7 +237,7 @@ private static OutputStream buildOutputStream(final File outputFile, final boole return outputStream; } - private static void assertSortOrdersAreEqual(final SAMFileHeader newHeader, final File inputFile) throws IOException { + private static void assertSortOrdersAreEqual(final SAMFileHeader newHeader, final Path inputFile) throws IOException { final SamReader reader = SamReaderFactory.makeDefault().open(inputFile); final SAMFileHeader origHeader = reader.getFileHeader(); final SAMFileHeader.SortOrder newSortOrder = newHeader.getSortOrder(); diff --git a/src/main/java/htsjdk/samtools/util/BlockCompressedInputStream.java b/src/main/java/htsjdk/samtools/util/BlockCompressedInputStream.java index 01ddf67e77..9f02619b79 100755 --- a/src/main/java/htsjdk/samtools/util/BlockCompressedInputStream.java +++ b/src/main/java/htsjdk/samtools/util/BlockCompressedInputStream.java @@ -710,6 +710,13 @@ public static void assertNonDefectiveFile(final File file) throws IOException { } } + // tsato: can I consolidate with above? + public static void assertNonDefectivePath(final Path file) throws IOException { + if (checkTermination(file) == FileTermination.DEFECTIVE) { + throw new SAMException(file.toUri() + " does not have a valid GZIP block at the end of the file."); + } + } + private static boolean preambleEqual(final byte[] preamble, final byte[] buf, final int startOffset, final int length) { for (int i = 0; i < length; ++i) { if (preamble[i] != buf[i + startOffset]) { From 9b7a03171cb27a641b46e3b652cae45a19a26a4e Mon Sep 17 00:00:00 2001 From: Takuto Sato Date: Mon, 19 Jun 2023 13:31:45 +0900 Subject: [PATCH 03/14] file to path --- src/main/java/htsjdk/samtools/BamFileIoUtils.java | 15 ++++++++++----- .../samtools/util/BlockCompressedInputStream.java | 4 ++-- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/src/main/java/htsjdk/samtools/BamFileIoUtils.java b/src/main/java/htsjdk/samtools/BamFileIoUtils.java index 17d29e1954..8b73e8f847 100644 --- a/src/main/java/htsjdk/samtools/BamFileIoUtils.java +++ b/src/main/java/htsjdk/samtools/BamFileIoUtils.java @@ -1,6 +1,7 @@ package htsjdk.samtools; import htsjdk.samtools.cram.io.CountingInputStream; +import htsjdk.samtools.seekablestream.SeekablePathStream; import htsjdk.samtools.util.BlockCompressedFilePointerUtil; import htsjdk.samtools.util.BlockCompressedInputStream; import htsjdk.samtools.util.BlockCompressedOutputStream; @@ -70,7 +71,7 @@ public static void reheaderBamFile(final SAMFileHeader samFileHeader, final Path } } - // tsato: let's do it. + // tsato: let's do it....this is the path version of blockCopyBamFile. Keeping the File version below, to be deleted. public static void blockCopyBamFile(final Path inputFile, final OutputStream outputStream, final boolean skipHeader, final boolean skipTerminator) { // FileInputStream in = null; try (final CountingInputStream in = new CountingInputStream(Files.newInputStream(inputFile))){ @@ -83,8 +84,12 @@ public static void blockCopyBamFile(final Path inputFile, final OutputStream out if (skipHeader) { final long vOffsetOfFirstRecord = SAMUtils.findVirtualOffsetOfFirstRecordInBam(inputFile); // tsato: need to convert - final BlockCompressedInputStream blockIn = new BlockCompressedInputStream(IOUtil.openFileForReading(inputFile)); // tsato hmmm... - blockIn.seek(vOffsetOfFirstRecord); + // tsato: passing an inputStream directly to BlockCompressed... won't make it seekable (which we need, I think, buy why for block copying...) + // can we just construct a seekable input stream? Is it buffered? Buffering probably not needed. See transferByStream + final SeekablePathStream seekablePathStream = new SeekablePathStream(inputFile); + final BlockCompressedInputStream blockIn = new BlockCompressedInputStream(seekablePathStream); // tsato hmmm... + // final BlockCompressedInputStream blockIn = new BlockCompressedInputStream(IOUtil.openFileForReading(inputFile)); // tsato hmmm... + blockIn.seek(vOffsetOfFirstRecord); // tsato: if seekablePathStream is not used mFile is null so this throws an error final long remainingInBlock = blockIn.available(); // If we found the end of the header then write the remainder of this block out as a @@ -93,7 +98,7 @@ public static void blockCopyBamFile(final Path inputFile, final OutputStream out final BlockCompressedOutputStream blockOut = new BlockCompressedOutputStream(outputStream, (Path)null); IOUtil.transferByStream(blockIn, blockOut, remainingInBlock); blockOut.flush(); - // Don't close blockOut because closing underlying stream would break everything + // Don't close blockOut because closing underlying stream would break everything (tsato: why?) } long pos = BlockCompressedFilePointerUtil.getBlockAddress(blockIn.getFilePointer()); @@ -103,7 +108,7 @@ public static void blockCopyBamFile(final Path inputFile, final OutputStream out } } - // Copy remainder of input stream into output stream + // Copy remainder of input stream into output stream (tsato: why would there be anything left? Didn't we close the input stream already?) final long currentPos = in.getCount(); // final long length = inputPath.toFile().length(); // tsato: this right? length of the file in bytes..vs size? -- see below final long length = Files.size(inputFile); // tsato: rename to size diff --git a/src/main/java/htsjdk/samtools/util/BlockCompressedInputStream.java b/src/main/java/htsjdk/samtools/util/BlockCompressedInputStream.java index 9f02619b79..681d32475e 100755 --- a/src/main/java/htsjdk/samtools/util/BlockCompressedInputStream.java +++ b/src/main/java/htsjdk/samtools/util/BlockCompressedInputStream.java @@ -63,7 +63,7 @@ public class BlockCompressedInputStream extends InputStream implements LocationA private InputStream mStream = null; private boolean mIsClosed = false; - private SeekableStream mFile = null; + private SeekableStream mFile = null; // tsato: change name to mPath? private byte[] mFileBuffer = null; private DecompressedBlock mCurrentBlock = null; private int mCurrentOffset = 0; @@ -359,7 +359,7 @@ public void seek(final long pos) throws IOException { } // Cannot seek on streams that are not file based - if (mFile == null) { + if (mFile == null) { // tsato: mFile is a seekable stream--- throw new IOException(CANNOT_SEEK_STREAM_MSG); } From f10b32f63953a6b75b286eb69ffffc68a2c6c607 Mon Sep 17 00:00:00 2001 From: Takuto Sato Date: Sat, 29 Jul 2023 15:13:44 -0400 Subject: [PATCH 04/14] just make a commit --- .../java/htsjdk/samtools/BamFileIoUtils.java | 124 +++++++++--------- .../htsjdk/samtools/SAMRecordIterator.java | 2 +- src/main/java/htsjdk/samtools/SAMUtils.java | 1 - .../htsjdk/samtools/SamFileValidator.java | 11 +- src/main/java/htsjdk/samtools/SamReader.java | 2 +- .../samtools/filter/FilteringSamIterator.java | 10 +- .../samtools/util/SortingCollection.java | 2 +- .../samtools/BamFileIoUtilsUnitTest.java | 25 ++++ 8 files changed, 102 insertions(+), 75 deletions(-) create mode 100644 src/test/java/htsjdk/samtools/BamFileIoUtilsUnitTest.java diff --git a/src/main/java/htsjdk/samtools/BamFileIoUtils.java b/src/main/java/htsjdk/samtools/BamFileIoUtils.java index 8b73e8f847..aeab178724 100644 --- a/src/main/java/htsjdk/samtools/BamFileIoUtils.java +++ b/src/main/java/htsjdk/samtools/BamFileIoUtils.java @@ -32,12 +32,10 @@ public static boolean isBamFile(final File file) { return ((file != null) && SamReader.Type.BAM_TYPE.hasValidFileExtension(file.getName())); } - // tsato: no longer needed -// public static void reheaderBamFile(final SAMFileHeader samFileHeader, final File inputFile, final File outputFile) { -// reheaderBamFile(samFileHeader, inputFile, outputFile, true, true); -// } + public static void reheaderBamFile(final SAMFileHeader samFileHeader, final File inputFile, final File outputFile) { + reheaderBamFile(samFileHeader, inputFile.toPath(), outputFile.toPath(), true, true); + } - // tsato public static void reheaderBamFile(final SAMFileHeader samFileHeader, final Path inputFile, final Path outputFile) { reheaderBamFile(samFileHeader, inputFile, outputFile, true, true); } @@ -83,7 +81,7 @@ public static void blockCopyBamFile(final Path inputFile, final OutputStream out throw new SAMException(inputFile + " does not have a valid GZIP block at the end of the file."); if (skipHeader) { - final long vOffsetOfFirstRecord = SAMUtils.findVirtualOffsetOfFirstRecordInBam(inputFile); // tsato: need to convert + final long vOffsetOfFirstRecord = SAMUtils.findVirtualOffsetOfFirstRecordInBam(inputFile); // tsato: this is where we "seek" // tsato: passing an inputStream directly to BlockCompressed... won't make it seekable (which we need, I think, buy why for block copying...) // can we just construct a seekable input stream? Is it buffered? Buffering probably not needed. See transferByStream final SeekablePathStream seekablePathStream = new SeekablePathStream(inputFile); @@ -131,52 +129,52 @@ public static void blockCopyBamFile(final Path inputFile, final OutputStream out * @param skipHeader If true, the header of the input file will not be copied to the output stream * @param skipTerminator If true, the terminator block of the input file will not be written to the output stream */ - public static void blockCopyBamFile(final File inputFile, final OutputStream outputStream, final boolean skipHeader, final boolean skipTerminator) { - FileInputStream in = null; - try { - in = new FileInputStream(inputFile); - - // a) It's good to check that the end of the file is valid and b) we need to know if there's a terminator block and not copy it if skipTerminator is true - final BlockCompressedInputStream.FileTermination term = BlockCompressedInputStream.checkTermination(inputFile); - if (term == BlockCompressedInputStream.FileTermination.DEFECTIVE) - throw new SAMException(inputFile.getAbsolutePath() + " does not have a valid GZIP block at the end of the file."); - - if (skipHeader) { - final long vOffsetOfFirstRecord = SAMUtils.findVirtualOffsetOfFirstRecordInBam(inputFile); - final BlockCompressedInputStream blockIn = new BlockCompressedInputStream(inputFile); - blockIn.seek(vOffsetOfFirstRecord); - final long remainingInBlock = blockIn.available(); - - // If we found the end of the header then write the remainder of this block out as a - // new gzip block and then break out of the while loop - if (remainingInBlock >= 0) { - final BlockCompressedOutputStream blockOut = new BlockCompressedOutputStream(outputStream, (Path)null); - IOUtil.transferByStream(blockIn, blockOut, remainingInBlock); - blockOut.flush(); - // Don't close blockOut because closing underlying stream would break everything - } - - long pos = BlockCompressedFilePointerUtil.getBlockAddress(blockIn.getFilePointer()); - blockIn.close(); - while (pos > 0) { - pos -= in.skip(pos); - } - } - - // Copy remainder of input stream into output stream - final long currentPos = in.getChannel().position(); - final long length = inputFile.length(); - final long skipLast = ((term == BlockCompressedInputStream.FileTermination.HAS_TERMINATOR_BLOCK) && skipTerminator) ? - BlockCompressedStreamConstants.EMPTY_GZIP_BLOCK.length : 0; - final long bytesToWrite = length - skipLast - currentPos; - - IOUtil.transferByStream(in, outputStream, bytesToWrite); - } catch (final IOException ioe) { - throw new RuntimeIOException(ioe); - } finally { - CloserUtil.close(in); - } - } +// public static void blockCopyBamFile(final File inputFile, final OutputStream outputStream, final boolean skipHeader, final boolean skipTerminator) { +// FileInputStream in = null; +// try { +// in = new FileInputStream(inputFile); +// +// // a) It's good to check that the end of the file is valid and b) we need to know if there's a terminator block and not copy it if skipTerminator is true +// final BlockCompressedInputStream.FileTermination term = BlockCompressedInputStream.checkTermination(inputFile); +// if (term == BlockCompressedInputStream.FileTermination.DEFECTIVE) +// throw new SAMException(inputFile.getAbsolutePath() + " does not have a valid GZIP block at the end of the file."); +// +// if (skipHeader) { +// final long vOffsetOfFirstRecord = SAMUtils.findVirtualOffsetOfFirstRecordInBam(inputFile); +// final BlockCompressedInputStream blockIn = new BlockCompressedInputStream(inputFile); +// blockIn.seek(vOffsetOfFirstRecord); +// final long remainingInBlock = blockIn.available(); +// +// // If we found the end of the header then write the remainder of this block out as a +// // new gzip block and then break out of the while loop +// if (remainingInBlock >= 0) { +// final BlockCompressedOutputStream blockOut = new BlockCompressedOutputStream(outputStream, (Path)null); +// IOUtil.transferByStream(blockIn, blockOut, remainingInBlock); +// blockOut.flush(); +// // Don't close blockOut because closing underlying stream would break everything +// } +// +// long pos = BlockCompressedFilePointerUtil.getBlockAddress(blockIn.getFilePointer()); +// blockIn.close(); +// while (pos > 0) { +// pos -= in.skip(pos); +// } +// } +// +// // Copy remainder of input stream into output stream +// final long currentPos = in.getChannel().position(); +// final long length = inputFile.length(); +// final long skipLast = ((term == BlockCompressedInputStream.FileTermination.HAS_TERMINATOR_BLOCK) && skipTerminator) ? +// BlockCompressedStreamConstants.EMPTY_GZIP_BLOCK.length : 0; +// final long bytesToWrite = length - skipLast - currentPos; +// +// IOUtil.transferByStream(in, outputStream, bytesToWrite); +// } catch (final IOException ioe) { +// throw new RuntimeIOException(ioe); +// } finally { +// CloserUtil.close(in); +// } +// } /** * Assumes that all inputs and outputs are block compressed VCF files and copies them without decompressing and parsing @@ -198,7 +196,7 @@ public static void gatherWithBlockCopying(final List bams, final File outp for (final File f : bams) { LOG.info(String.format("Block copying %s ...", f.getAbsolutePath())); - blockCopyBamFile(f, out, !isFirstFile, true); + blockCopyBamFile(f.toPath(), out, !isFirstFile, true); isFirstFile = false; } @@ -231,16 +229,16 @@ private static OutputStream buildOutputStream(final Path outputFile, final boole return outputStream; } - private static OutputStream buildOutputStream(final File outputFile, final boolean createMd5, final boolean createIndex) throws IOException { - OutputStream outputStream = new FileOutputStream(outputFile); - if (createMd5) { - outputStream = new Md5CalculatingOutputStream(outputStream, new File(outputFile.getAbsolutePath() + ".md5")); - } - if (createIndex) { - outputStream = new StreamInflatingIndexingOutputStream(outputStream, new File(outputFile.getParentFile(), IOUtil.basename(outputFile) + FileExtensions.BAI_INDEX)); - } - return outputStream; - } +// private static OutputStream buildOutputStream(final File outputFile, final boolean createMd5, final boolean createIndex) throws IOException { +// OutputStream outputStream = new FileOutputStream(outputFile); +// if (createMd5) { +// outputStream = new Md5CalculatingOutputStream(outputStream, new File(outputFile.getAbsolutePath() + ".md5")); +// } +// if (createIndex) { +// outputStream = new StreamInflatingIndexingOutputStream(outputStream, new File(outputFile.getParentFile(), IOUtil.basename(outputFile) + FileExtensions.BAI_INDEX)); +// } +// return outputStream; +// } private static void assertSortOrdersAreEqual(final SAMFileHeader newHeader, final Path inputFile) throws IOException { final SamReader reader = SamReaderFactory.makeDefault().open(inputFile); diff --git a/src/main/java/htsjdk/samtools/SAMRecordIterator.java b/src/main/java/htsjdk/samtools/SAMRecordIterator.java index 4ba580767e..6955043bfd 100755 --- a/src/main/java/htsjdk/samtools/SAMRecordIterator.java +++ b/src/main/java/htsjdk/samtools/SAMRecordIterator.java @@ -28,7 +28,7 @@ /** * A general interface that adds functionality to a CloseableIterator of * SAMRecords. Currently, this interface is implemented by iterators that - * want to validate as they are iterating that that the records in the + * want to validate as they are iterating that the records in the * underlying SAM/BAM file are in a particular order. */ public interface SAMRecordIterator extends CloseableIterator { diff --git a/src/main/java/htsjdk/samtools/SAMUtils.java b/src/main/java/htsjdk/samtools/SAMUtils.java index e06e111b08..6941bbf657 100644 --- a/src/main/java/htsjdk/samtools/SAMUtils.java +++ b/src/main/java/htsjdk/samtools/SAMUtils.java @@ -678,7 +678,6 @@ public static int combineMapqs(int m1, int m2) { // tsatof: public static long findVirtualOffsetOfFirstRecordInBam(final Path bamFile) { try { - InputStream yo = IOUtil.openFileForReading(bamFile); SeekableStream ss = new SeekablePathStream(bamFile); // tsato: best way? buffering needed? return BAMFileReader.findVirtualOffsetOfFirstRecord(ss); } catch (final IOException ioe) { diff --git a/src/main/java/htsjdk/samtools/SamFileValidator.java b/src/main/java/htsjdk/samtools/SamFileValidator.java index 42e66cbf59..50f289e3ab 100644 --- a/src/main/java/htsjdk/samtools/SamFileValidator.java +++ b/src/main/java/htsjdk/samtools/SamFileValidator.java @@ -52,6 +52,7 @@ import java.io.InputStream; import java.io.OutputStream; import java.io.PrintWriter; +import java.nio.file.Path; import java.util.AbstractMap; import java.util.ArrayList; import java.util.Collection; @@ -209,19 +210,23 @@ public boolean validateSamFileVerbose(final SamReader samReader, final Reference } public void validateBamFileTermination(final File inputFile) { + validateBamFileTermination(inputFile.toPath()); + } + + public void validateBamFileTermination(final Path inputFile) { try { - if (!IOUtil.isBlockCompressed(inputFile.toPath())) { + if (!IOUtil.isBlockCompressed(inputFile)) { return; } final BlockCompressedInputStream.FileTermination terminationState = BlockCompressedInputStream.checkTermination(inputFile); if (terminationState.equals(BlockCompressedInputStream.FileTermination.DEFECTIVE)) { addError(new SAMValidationError(Type.TRUNCATED_FILE, "BAM file has defective last gzip block", - inputFile.getPath())); + inputFile.toAbsolutePath().toString())); // tsato: confirm toAbsolutePath() is the right thing to do here } else if (terminationState.equals(BlockCompressedInputStream.FileTermination.HAS_HEALTHY_LAST_BLOCK)) { addError(new SAMValidationError(Type.BAM_FILE_MISSING_TERMINATOR_BLOCK, "Older BAM file -- does not have terminator block", - inputFile.getPath())); + inputFile.toAbsolutePath().toString())); } } catch (IOException e) { diff --git a/src/main/java/htsjdk/samtools/SamReader.java b/src/main/java/htsjdk/samtools/SamReader.java index 74e4016f68..b427482c3c 100644 --- a/src/main/java/htsjdk/samtools/SamReader.java +++ b/src/main/java/htsjdk/samtools/SamReader.java @@ -593,7 +593,7 @@ public SAMRecord next() { final SAMRecord previous = checker.getPreviousRecord(); if (!checker.isSorted(result)) { throw new IllegalStateException(String.format( - "Record %s should come after %s when sorting with %s ordering.", + "Record %s should come after %s when sorting with %s ordering.", // tsato: why doesn't this code get triggered when you have a bad sort order? previous.getSAMString().trim(), result.getSAMString().trim(), checker.getSortOrder())); } diff --git a/src/main/java/htsjdk/samtools/filter/FilteringSamIterator.java b/src/main/java/htsjdk/samtools/filter/FilteringSamIterator.java index a70156ad6e..9b2e2a9b60 100644 --- a/src/main/java/htsjdk/samtools/filter/FilteringSamIterator.java +++ b/src/main/java/htsjdk/samtools/filter/FilteringSamIterator.java @@ -60,9 +60,9 @@ public class FilteringSamIterator implements CloseableIterator { public FilteringSamIterator(final Iterator iterator, final SamRecordFilter filter, final boolean filterByPair) { - if (filterByPair && iterator instanceof SAMRecordIterator) { - ((SAMRecordIterator)iterator).assertSorted(SAMFileHeader.SortOrder.queryname); - } + if (filterByPair && iterator instanceof SAMRecordIterator) { // tsato: this whole thing has code smell + ((SAMRecordIterator)iterator).assertSorted(SAMFileHeader.SortOrder.queryname); // tsato: wtf is this? + } // tsato: this does not throw an error here if the sort order is mismatched...bizarre. this.iterator = new PeekableIterator(iterator); this.filter = filter; @@ -137,7 +137,7 @@ private SAMRecord getNextRecord() { if (filterReadPairs && record.getReadPairedFlag() && record.getFirstOfPairFlag() && iterator.hasNext()) { - SamPairUtil.assertMate(record, iterator.peek()); + SamPairUtil.assertMate(record, iterator.peek()); // tsato: why is the code complaining? if (filter.filterOut(record, iterator.peek())) { // skip second read @@ -148,7 +148,7 @@ private SAMRecord getNextRecord() { } else if (filterReadPairs && record.getReadPairedFlag() && record.getSecondOfPairFlag()) { // assume that we did a pass(first, second) and it passed the filter - return record; + return record; // tsato: doesn't the first of pair always appear first if queryname sorted? } else if (!filter.filterOut(record)) { return record; } diff --git a/src/main/java/htsjdk/samtools/util/SortingCollection.java b/src/main/java/htsjdk/samtools/util/SortingCollection.java index a983a26e85..90a2a3ca15 100644 --- a/src/main/java/htsjdk/samtools/util/SortingCollection.java +++ b/src/main/java/htsjdk/samtools/util/SortingCollection.java @@ -55,7 +55,7 @@ * If Snappy DLL is available and snappy.disable system property is not set to true, then Snappy is used * to compress temporary files. */ -public class SortingCollection implements Iterable { +public class SortingCollection implements Iterable { private static final Log log = Log.getInstance(SortingCollection.class); /** diff --git a/src/test/java/htsjdk/samtools/BamFileIoUtilsUnitTest.java b/src/test/java/htsjdk/samtools/BamFileIoUtilsUnitTest.java new file mode 100644 index 0000000000..431047f22b --- /dev/null +++ b/src/test/java/htsjdk/samtools/BamFileIoUtilsUnitTest.java @@ -0,0 +1,25 @@ +package htsjdk.samtools; + +import htsjdk.HtsjdkTest; +import org.testng.annotations.Test; + +import java.io.File; +import java.nio.file.Paths; + +import static org.testng.Assert.*; + +public class BamFileIoUtilsUnitTest extends HtsjdkTest { + private String inputBam = "gs://broad-dsde-methods-takuto/gatk/test/K-562_test.bam"; + private String bamWithNewHeader = "gs://broad-dsde-methods-takuto/gatk/test/K-562_test_new_header.bam"; + + // Or, should we just write this in Picard? + // But this functionality is in htsjdk, so definitely should be tested here + // Does Paths.get() take a gs:// file and figure it out though... + + @Test + public void testReheaderBamFile(){ + SAMFileHeader header = null; // tsato: to implement + BamFileIoUtils.reheaderBamFile(header, Paths.get(inputBam), Paths.get(bamWithNewHeader)); // tsato: is this going to cut it? + // Creating a temp file....needed in htsjdk too... + } +} \ No newline at end of file From a46c9d8120a3b5436e0855fbaa3358148d5356be Mon Sep 17 00:00:00 2001 From: Takuto Sato Date: Tue, 1 Aug 2023 12:30:47 -0400 Subject: [PATCH 05/14] another --- src/main/java/htsjdk/samtools/BamFileIoUtils.java | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/main/java/htsjdk/samtools/BamFileIoUtils.java b/src/main/java/htsjdk/samtools/BamFileIoUtils.java index aeab178724..77352601f0 100644 --- a/src/main/java/htsjdk/samtools/BamFileIoUtils.java +++ b/src/main/java/htsjdk/samtools/BamFileIoUtils.java @@ -32,14 +32,19 @@ public static boolean isBamFile(final File file) { return ((file != null) && SamReader.Type.BAM_TYPE.hasValidFileExtension(file.getName())); } - public static void reheaderBamFile(final SAMFileHeader samFileHeader, final File inputFile, final File outputFile) { - reheaderBamFile(samFileHeader, inputFile.toPath(), outputFile.toPath(), true, true); - } - public static void reheaderBamFile(final SAMFileHeader samFileHeader, final Path inputFile, final Path outputFile) { reheaderBamFile(samFileHeader, inputFile, outputFile, true, true); } + + /** + * Kept for backward compatibility. Use the same method with Path inputs below. + */ + @Deprecated + public static void reheaderBamFile(final SAMFileHeader samFileHeader, final File inputFile, final File outputFile, final boolean createMd5, final boolean createIndex) { + reheaderBamFile(samFileHeader, inputFile.toPath(), outputFile.toPath(), createMd5, createIndex); + } + /** * Copy a BAM file but replacing the header * From 3ad8a4b5f43453facc5e516fe4ea902d71c43ac0 Mon Sep 17 00:00:00 2001 From: Takuto Sato Date: Tue, 1 Aug 2023 16:30:30 -0400 Subject: [PATCH 06/14] for better versioning --- .../java/htsjdk/samtools/BamFileIoUtils.java | 75 ++++--------------- 1 file changed, 16 insertions(+), 59 deletions(-) diff --git a/src/main/java/htsjdk/samtools/BamFileIoUtils.java b/src/main/java/htsjdk/samtools/BamFileIoUtils.java index 77352601f0..b230c96f24 100644 --- a/src/main/java/htsjdk/samtools/BamFileIoUtils.java +++ b/src/main/java/htsjdk/samtools/BamFileIoUtils.java @@ -38,7 +38,7 @@ public static void reheaderBamFile(final SAMFileHeader samFileHeader, final Path /** - * Kept for backward compatibility. Use the same method with Path inputs below. + * Support File input types for backward compatibility. Use the same method with Path inputs below. */ @Deprecated public static void reheaderBamFile(final SAMFileHeader samFileHeader, final File inputFile, final File outputFile, final boolean createMd5, final boolean createIndex) { @@ -56,7 +56,7 @@ public static void reheaderBamFile(final SAMFileHeader samFileHeader, final File */ public static void reheaderBamFile(final SAMFileHeader samFileHeader, final Path inputFile, final Path outputFile, final boolean createMd5, final boolean createIndex) { IOUtil.assertFileIsReadable(inputFile); - // IOUtil.assertFileIsWritable(outputFile); // tsato: what do I do with this... + IOUtil.assertFileIsWritable(outputFile); // tsato: what do I do with this... try { BlockCompressedInputStream.assertNonDefectivePath(inputFile); @@ -74,6 +74,19 @@ public static void reheaderBamFile(final SAMFileHeader samFileHeader, final Path } } + @Deprecated + public static void blockCopyBamFile(final File inputFile, final OutputStream outputStream, final boolean skipHeader, final boolean skipTerminator) { + blockCopyBamFile(inputFile.toPath(), outputStream, skipHeader, skipTerminator); + } + + /** + * Copy data from a BAM file to an OutputStream by directly copying the gzip blocks + * + * @param inputFile The file to be copied + * @param outputStream The stream to write the copied data to + * @param skipHeader If true, the header of the input file will not be copied to the output stream + * @param skipTerminator If true, the terminator block of the input file will not be written to the output stream + */ // tsato: let's do it....this is the path version of blockCopyBamFile. Keeping the File version below, to be deleted. public static void blockCopyBamFile(final Path inputFile, final OutputStream outputStream, final boolean skipHeader, final boolean skipTerminator) { // FileInputStream in = null; @@ -124,63 +137,7 @@ public static void blockCopyBamFile(final Path inputFile, final OutputStream out throw new RuntimeIOException(ioe); } } - - - /** - * Copy data from a BAM file to an OutputStream by directly copying the gzip blocks - * - * @param inputFile The file to be copied - * @param outputStream The stream to write the copied data to - * @param skipHeader If true, the header of the input file will not be copied to the output stream - * @param skipTerminator If true, the terminator block of the input file will not be written to the output stream - */ -// public static void blockCopyBamFile(final File inputFile, final OutputStream outputStream, final boolean skipHeader, final boolean skipTerminator) { -// FileInputStream in = null; -// try { -// in = new FileInputStream(inputFile); -// -// // a) It's good to check that the end of the file is valid and b) we need to know if there's a terminator block and not copy it if skipTerminator is true -// final BlockCompressedInputStream.FileTermination term = BlockCompressedInputStream.checkTermination(inputFile); -// if (term == BlockCompressedInputStream.FileTermination.DEFECTIVE) -// throw new SAMException(inputFile.getAbsolutePath() + " does not have a valid GZIP block at the end of the file."); -// -// if (skipHeader) { -// final long vOffsetOfFirstRecord = SAMUtils.findVirtualOffsetOfFirstRecordInBam(inputFile); -// final BlockCompressedInputStream blockIn = new BlockCompressedInputStream(inputFile); -// blockIn.seek(vOffsetOfFirstRecord); -// final long remainingInBlock = blockIn.available(); -// -// // If we found the end of the header then write the remainder of this block out as a -// // new gzip block and then break out of the while loop -// if (remainingInBlock >= 0) { -// final BlockCompressedOutputStream blockOut = new BlockCompressedOutputStream(outputStream, (Path)null); -// IOUtil.transferByStream(blockIn, blockOut, remainingInBlock); -// blockOut.flush(); -// // Don't close blockOut because closing underlying stream would break everything -// } -// -// long pos = BlockCompressedFilePointerUtil.getBlockAddress(blockIn.getFilePointer()); -// blockIn.close(); -// while (pos > 0) { -// pos -= in.skip(pos); -// } -// } -// -// // Copy remainder of input stream into output stream -// final long currentPos = in.getChannel().position(); -// final long length = inputFile.length(); -// final long skipLast = ((term == BlockCompressedInputStream.FileTermination.HAS_TERMINATOR_BLOCK) && skipTerminator) ? -// BlockCompressedStreamConstants.EMPTY_GZIP_BLOCK.length : 0; -// final long bytesToWrite = length - skipLast - currentPos; -// -// IOUtil.transferByStream(in, outputStream, bytesToWrite); -// } catch (final IOException ioe) { -// throw new RuntimeIOException(ioe); -// } finally { -// CloserUtil.close(in); -// } -// } - + /** * Assumes that all inputs and outputs are block compressed VCF files and copies them without decompressing and parsing * most of the gzip blocks. Will decompress and parse blocks up to the one containing the end of the header in each file From 1c8f57a9974211a6f2663b6d9194895cb90c5c13 Mon Sep 17 00:00:00 2001 From: Takuto Sato Date: Thu, 3 Aug 2023 13:56:23 -0400 Subject: [PATCH 07/14] test looks good --- .../java/htsjdk/samtools/BamFileIoUtils.java | 11 ++--- .../samtools/BamFileIoUtilsUnitTest.java | 43 +++++++++++++------ 2 files changed, 37 insertions(+), 17 deletions(-) diff --git a/src/main/java/htsjdk/samtools/BamFileIoUtils.java b/src/main/java/htsjdk/samtools/BamFileIoUtils.java index b230c96f24..9d61d08601 100644 --- a/src/main/java/htsjdk/samtools/BamFileIoUtils.java +++ b/src/main/java/htsjdk/samtools/BamFileIoUtils.java @@ -56,7 +56,7 @@ public static void reheaderBamFile(final SAMFileHeader samFileHeader, final File */ public static void reheaderBamFile(final SAMFileHeader samFileHeader, final Path inputFile, final Path outputFile, final boolean createMd5, final boolean createIndex) { IOUtil.assertFileIsReadable(inputFile); - IOUtil.assertFileIsWritable(outputFile); // tsato: what do I do with this... + // IOUtil.assertFileIsWritable(outputFile); // tsato: what do I do with this... try { BlockCompressedInputStream.assertNonDefectivePath(inputFile); @@ -87,16 +87,17 @@ public static void blockCopyBamFile(final File inputFile, final OutputStream out * @param skipHeader If true, the header of the input file will not be copied to the output stream * @param skipTerminator If true, the terminator block of the input file will not be written to the output stream */ - // tsato: let's do it....this is the path version of blockCopyBamFile. Keeping the File version below, to be deleted. public static void blockCopyBamFile(final Path inputFile, final OutputStream outputStream, final boolean skipHeader, final boolean skipTerminator) { // FileInputStream in = null; try (final CountingInputStream in = new CountingInputStream(Files.newInputStream(inputFile))){ // in = new FileInputStream(inputFile); - // check that a) the end of the file is valid and b) if there's a terminator block and not copy it if skipTerminator is true + // check: + // a) that the end of the file is valid, and + // b) if there's a terminator block and not copy it if skipTerminator is true final BlockCompressedInputStream.FileTermination term = BlockCompressedInputStream.checkTermination(inputFile); if (term == BlockCompressedInputStream.FileTermination.DEFECTIVE) - throw new SAMException(inputFile + " does not have a valid GZIP block at the end of the file."); + throw new SAMException(inputFile.toUri() + " does not have a valid GZIP block at the end of the file."); if (skipHeader) { final long vOffsetOfFirstRecord = SAMUtils.findVirtualOffsetOfFirstRecordInBam(inputFile); // tsato: this is where we "seek" @@ -137,7 +138,7 @@ public static void blockCopyBamFile(final Path inputFile, final OutputStream out throw new RuntimeIOException(ioe); } } - + /** * Assumes that all inputs and outputs are block compressed VCF files and copies them without decompressing and parsing * most of the gzip blocks. Will decompress and parse blocks up to the one containing the end of the header in each file diff --git a/src/test/java/htsjdk/samtools/BamFileIoUtilsUnitTest.java b/src/test/java/htsjdk/samtools/BamFileIoUtilsUnitTest.java index 431047f22b..2f95c743f2 100644 --- a/src/test/java/htsjdk/samtools/BamFileIoUtilsUnitTest.java +++ b/src/test/java/htsjdk/samtools/BamFileIoUtilsUnitTest.java @@ -1,25 +1,44 @@ package htsjdk.samtools; import htsjdk.HtsjdkTest; +import htsjdk.beta.exception.HtsjdkException; +import org.testng.Assert; import org.testng.annotations.Test; import java.io.File; -import java.nio.file.Paths; - -import static org.testng.Assert.*; +import java.io.IOException; +import java.util.Iterator; public class BamFileIoUtilsUnitTest extends HtsjdkTest { - private String inputBam = "gs://broad-dsde-methods-takuto/gatk/test/K-562_test.bam"; - private String bamWithNewHeader = "gs://broad-dsde-methods-takuto/gatk/test/K-562_test_new_header.bam"; - - // Or, should we just write this in Picard? - // But this functionality is in htsjdk, so definitely should be tested here - // Does Paths.get() take a gs:// file and figure it out though... + final static String TEST_DATA_DIR = "src/test/resources/htsjdk/samtools/cram/"; + final String originalBam = TEST_DATA_DIR + "CEUTrio.HiSeq.WGS.b37.NA12878.20.first.8000.bam"; + final String bamWithDesiredHeader = TEST_DATA_DIR + "NA12878.20.21.unmapped.orig.bam"; @Test public void testReheaderBamFile(){ - SAMFileHeader header = null; // tsato: to implement - BamFileIoUtils.reheaderBamFile(header, Paths.get(inputBam), Paths.get(bamWithNewHeader)); // tsato: is this going to cut it? - // Creating a temp file....needed in htsjdk too... + final File originalBam = new File(this.originalBam); + SAMFileHeader header = SamReaderFactory.make().getFileHeader(new File(bamWithDesiredHeader)); + try { + final File output = File.createTempFile("output", ".bam"); + BamFileIoUtils.reheaderBamFile(header, originalBam.toPath(), output.toPath()); + + // Confirm that the header has been replaced + final SamReader outputReader = SamReaderFactory.make().open(output.toPath()); + Assert.assertEquals(outputReader.getFileHeader(), header); + + // Confirm that the reads are the same as the original + final Iterator originalBamIterator = SamReaderFactory.make().open(originalBam.toPath()).iterator(); + final Iterator outputBamIterator = outputReader.iterator(); + final int numRecordsToRead = 10; + for (int i = 0; i < numRecordsToRead; i++){ + final SAMRecord originalRead = originalBamIterator.next(); + final SAMRecord outputRead = outputBamIterator.next(); + Assert.assertEquals(originalRead, outputRead); + } + + } catch (IOException e){ + throw new HtsjdkException("Could not create a temporary output file.", e); + } + } } \ No newline at end of file From 215019c6282a95370f21809e171b0f76a924e869 Mon Sep 17 00:00:00 2001 From: Takuto Sato Date: Thu, 3 Aug 2023 17:54:14 -0400 Subject: [PATCH 08/14] one more test --- .../java/htsjdk/samtools/BamFileIoUtils.java | 15 +++-- .../samtools/util/SortingCollection.java | 2 +- .../samtools/BamFileIoUtilsUnitTest.java | 59 +++++++++++++++---- 3 files changed, 56 insertions(+), 20 deletions(-) diff --git a/src/main/java/htsjdk/samtools/BamFileIoUtils.java b/src/main/java/htsjdk/samtools/BamFileIoUtils.java index 9d61d08601..fd4e08d410 100644 --- a/src/main/java/htsjdk/samtools/BamFileIoUtils.java +++ b/src/main/java/htsjdk/samtools/BamFileIoUtils.java @@ -13,7 +13,10 @@ import htsjdk.samtools.util.Md5CalculatingOutputStream; import htsjdk.samtools.util.RuntimeIOException; -import java.io.*; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; @@ -74,6 +77,9 @@ public static void reheaderBamFile(final SAMFileHeader samFileHeader, final Path } } + /** + * @deprecated as of August 2023. Use the method by the same name below with Path input + */ @Deprecated public static void blockCopyBamFile(final File inputFile, final OutputStream outputStream, final boolean skipHeader, final boolean skipTerminator) { blockCopyBamFile(inputFile.toPath(), outputStream, skipHeader, skipTerminator); @@ -88,13 +94,10 @@ public static void blockCopyBamFile(final File inputFile, final OutputStream out * @param skipTerminator If true, the terminator block of the input file will not be written to the output stream */ public static void blockCopyBamFile(final Path inputFile, final OutputStream outputStream, final boolean skipHeader, final boolean skipTerminator) { - // FileInputStream in = null; + // tsato: why use CountingInputStream? try (final CountingInputStream in = new CountingInputStream(Files.newInputStream(inputFile))){ - // in = new FileInputStream(inputFile); - // check: - // a) that the end of the file is valid, and - // b) if there's a terminator block and not copy it if skipTerminator is true + // a) It's good to check that the end of the file is valid and b) we need to know if there's a terminator block and not copy it if skipTerminator is true final BlockCompressedInputStream.FileTermination term = BlockCompressedInputStream.checkTermination(inputFile); if (term == BlockCompressedInputStream.FileTermination.DEFECTIVE) throw new SAMException(inputFile.toUri() + " does not have a valid GZIP block at the end of the file."); diff --git a/src/main/java/htsjdk/samtools/util/SortingCollection.java b/src/main/java/htsjdk/samtools/util/SortingCollection.java index 90a2a3ca15..a983a26e85 100644 --- a/src/main/java/htsjdk/samtools/util/SortingCollection.java +++ b/src/main/java/htsjdk/samtools/util/SortingCollection.java @@ -55,7 +55,7 @@ * If Snappy DLL is available and snappy.disable system property is not set to true, then Snappy is used * to compress temporary files. */ -public class SortingCollection implements Iterable { +public class SortingCollection implements Iterable { private static final Log log = Log.getInstance(SortingCollection.class); /** diff --git a/src/test/java/htsjdk/samtools/BamFileIoUtilsUnitTest.java b/src/test/java/htsjdk/samtools/BamFileIoUtilsUnitTest.java index 2f95c743f2..5c6109ed7b 100644 --- a/src/test/java/htsjdk/samtools/BamFileIoUtilsUnitTest.java +++ b/src/test/java/htsjdk/samtools/BamFileIoUtilsUnitTest.java @@ -7,17 +7,23 @@ 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.nio.file.Paths; import java.util.Iterator; public class BamFileIoUtilsUnitTest extends HtsjdkTest { final static String TEST_DATA_DIR = "src/test/resources/htsjdk/samtools/cram/"; - final String originalBam = TEST_DATA_DIR + "CEUTrio.HiSeq.WGS.b37.NA12878.20.first.8000.bam"; - final String bamWithDesiredHeader = TEST_DATA_DIR + "NA12878.20.21.unmapped.orig.bam"; + final static String NA12878_8000 = TEST_DATA_DIR + "CEUTrio.HiSeq.WGS.b37.NA12878.20.first.8000.bam"; + final static String NA12878_20_21 = TEST_DATA_DIR + "NA12878.20.21.unmapped.orig.bam"; + final static int DEFAULT_NUM_RECORDS_TO_COMPARE = 10; @Test public void testReheaderBamFile(){ - final File originalBam = new File(this.originalBam); - SAMFileHeader header = SamReaderFactory.make().getFileHeader(new File(bamWithDesiredHeader)); + final File originalBam = new File(this.NA12878_8000); + SAMFileHeader header = SamReaderFactory.make().getFileHeader(new File(NA12878_20_21)); try { final File output = File.createTempFile("output", ".bam"); BamFileIoUtils.reheaderBamFile(header, originalBam.toPath(), output.toPath()); @@ -27,18 +33,45 @@ public void testReheaderBamFile(){ Assert.assertEquals(outputReader.getFileHeader(), header); // Confirm that the reads are the same as the original - final Iterator originalBamIterator = SamReaderFactory.make().open(originalBam.toPath()).iterator(); - final Iterator outputBamIterator = outputReader.iterator(); - final int numRecordsToRead = 10; - for (int i = 0; i < numRecordsToRead; i++){ - final SAMRecord originalRead = originalBamIterator.next(); - final SAMRecord outputRead = outputBamIterator.next(); - Assert.assertEquals(originalRead, outputRead); - } - + Assert.assertTrue(compareBamReads(originalBam, output, DEFAULT_NUM_RECORDS_TO_COMPARE)); } catch (IOException e){ throw new HtsjdkException("Could not create a temporary output file.", e); } + } + + /** + * Compare first (numRecordsToCompare) reads of two bam files. + * In particular we do not check for equality of the headers. + * + * @param numRecordsToCompare the number of reads to compare + * + * @return true if the first (numRecordsToCompare) reads are equal, else false + */ + private boolean compareBamReads(final File bam1, final File bam2, final int numRecordsToCompare){ + final Iterator originalBamIterator = SamReaderFactory.make().open(bam1.toPath()).iterator(); + final Iterator outputBamIterator = SamReaderFactory.make().open(bam2.toPath()).iterator(); + for (int i = 0; i < numRecordsToCompare; i++){ + final SAMRecord originalRead = originalBamIterator.next(); + final SAMRecord outputRead = outputBamIterator.next(); + if (! originalRead.equals(outputRead)){ + return false; + } + } + return true; + } + + @Test + public void testBlockCopyBamFile() throws IOException { + final File output = File.createTempFile("output", ".bam"); + final OutputStream out = Files.newOutputStream(output.toPath()); + final Path input = Paths.get(NA12878_8000); + final InputStream in = Files.newInputStream(input); + + BamFileIoUtils.blockCopyBamFile(Paths.get(NA12878_8000), out, false, false); + final SamReader inputReader = SamReaderFactory.make().open(input); + final SamReader outputReader = SamReaderFactory.make().open(output); + Assert.assertEquals(outputReader.getFileHeader(), inputReader.getFileHeader()); + Assert.assertTrue(compareBamReads(input.toFile(), output, DEFAULT_NUM_RECORDS_TO_COMPARE)); } } \ No newline at end of file From d0158f3d87b80451a23aa97c428dcb0cf005513e Mon Sep 17 00:00:00 2001 From: Takuto Sato Date: Mon, 7 Aug 2023 12:03:27 -0400 Subject: [PATCH 09/14] test done --- .../samtools/BamFileIoUtilsUnitTest.java | 38 ++++++++++++++++--- 1 file changed, 32 insertions(+), 6 deletions(-) diff --git a/src/test/java/htsjdk/samtools/BamFileIoUtilsUnitTest.java b/src/test/java/htsjdk/samtools/BamFileIoUtilsUnitTest.java index 5c6109ed7b..006754c75e 100644 --- a/src/test/java/htsjdk/samtools/BamFileIoUtilsUnitTest.java +++ b/src/test/java/htsjdk/samtools/BamFileIoUtilsUnitTest.java @@ -2,7 +2,9 @@ import htsjdk.HtsjdkTest; import htsjdk.beta.exception.HtsjdkException; +import htsjdk.samtools.util.BlockCompressedInputStream; import org.testng.Assert; +import org.testng.annotations.DataProvider; import org.testng.annotations.Test; import java.io.File; @@ -60,18 +62,42 @@ private boolean compareBamReads(final File bam1, final File bam2, final int numR return true; } - @Test - public void testBlockCopyBamFile() throws IOException { + // tsato: kinda ugly? + @DataProvider(name="BlockCopyBamFileTestInput") + public Object[][] getBlockCopyBamFileTestInput() { + return new Object[][] { + {true, true}, + {true, false}, + {false, true}, + {false, false} + }; + } + + @Test(dataProvider = "BlockCopyBamFileTestInput") + public void testBlockCopyBamFile(final boolean skipHeader, final boolean skipTerminator) throws IOException { + System.out.println(skipHeader + ", " + skipTerminator); final File output = File.createTempFile("output", ".bam"); final OutputStream out = Files.newOutputStream(output.toPath()); final Path input = Paths.get(NA12878_8000); - final InputStream in = Files.newInputStream(input); - BamFileIoUtils.blockCopyBamFile(Paths.get(NA12878_8000), out, false, false); + BamFileIoUtils.blockCopyBamFile(Paths.get(NA12878_8000), out, skipHeader, skipTerminator); final SamReader inputReader = SamReaderFactory.make().open(input); final SamReader outputReader = SamReaderFactory.make().open(output); - Assert.assertEquals(outputReader.getFileHeader(), inputReader.getFileHeader()); - Assert.assertTrue(compareBamReads(input.toFile(), output, DEFAULT_NUM_RECORDS_TO_COMPARE)); + + if (skipHeader){ + SAMFileHeader h = outputReader.getFileHeader(); + Assert.assertTrue(h.getReadGroups().isEmpty()); // a proxy for the header being empty + } else { + Assert.assertEquals(outputReader.getFileHeader(), inputReader.getFileHeader()); + // Reading will fail when the header is absent + Assert.assertTrue(compareBamReads(input.toFile(), output, DEFAULT_NUM_RECORDS_TO_COMPARE)); + } + + if (skipTerminator){ + BlockCompressedInputStream.FileTermination termination = BlockCompressedInputStream.checkTermination(output); + Assert.assertEquals(termination, BlockCompressedInputStream.FileTermination.HAS_HEALTHY_LAST_BLOCK); + } + } } \ No newline at end of file From a7eaf52d61681e343601bb02f00b7a31f2fd261b Mon Sep 17 00:00:00 2001 From: Takuto Sato Date: Thu, 10 Aug 2023 05:56:50 -0400 Subject: [PATCH 10/14] some investigating bits of code included --- .../java/htsjdk/samtools/BamFileIoUtils.java | 59 +++++++++++-------- src/main/java/htsjdk/samtools/SAMUtils.java | 4 +- 2 files changed, 37 insertions(+), 26 deletions(-) diff --git a/src/main/java/htsjdk/samtools/BamFileIoUtils.java b/src/main/java/htsjdk/samtools/BamFileIoUtils.java index fd4e08d410..43bce82d37 100644 --- a/src/main/java/htsjdk/samtools/BamFileIoUtils.java +++ b/src/main/java/htsjdk/samtools/BamFileIoUtils.java @@ -12,10 +12,13 @@ import htsjdk.samtools.util.Log; import htsjdk.samtools.util.Md5CalculatingOutputStream; import htsjdk.samtools.util.RuntimeIOException; +import org.apache.commons.compress.utils.FileNameUtils; import java.io.File; +import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; +import java.io.InputStream; import java.io.OutputStream; import java.nio.file.Files; import java.nio.file.Path; @@ -94,22 +97,20 @@ public static void blockCopyBamFile(final File inputFile, final OutputStream out * @param skipTerminator If true, the terminator block of the input file will not be written to the output stream */ public static void blockCopyBamFile(final Path inputFile, final OutputStream outputStream, final boolean skipHeader, final boolean skipTerminator) { - // tsato: why use CountingInputStream? + // tsato: use CountingInputStream to replace FileInputStream. The latter has .getChannel().getPosition + // The regular InputStream from Files.newInputStream() doesn't have it, but I might be able to do so by creating Channel object and avoid using CountingInputStream try (final CountingInputStream in = new CountingInputStream(Files.newInputStream(inputFile))){ - + // try (final InputStream in = Files.newInputStream(inputFile)){ // a) It's good to check that the end of the file is valid and b) we need to know if there's a terminator block and not copy it if skipTerminator is true final BlockCompressedInputStream.FileTermination term = BlockCompressedInputStream.checkTermination(inputFile); if (term == BlockCompressedInputStream.FileTermination.DEFECTIVE) throw new SAMException(inputFile.toUri() + " does not have a valid GZIP block at the end of the file."); - if (skipHeader) { - final long vOffsetOfFirstRecord = SAMUtils.findVirtualOffsetOfFirstRecordInBam(inputFile); // tsato: this is where we "seek" - // tsato: passing an inputStream directly to BlockCompressed... won't make it seekable (which we need, I think, buy why for block copying...) - // can we just construct a seekable input stream? Is it buffered? Buffering probably not needed. See transferByStream + if (skipHeader) { // tsato: this method assumes bam file...should I test cram? final SeekablePathStream seekablePathStream = new SeekablePathStream(inputFile); - final BlockCompressedInputStream blockIn = new BlockCompressedInputStream(seekablePathStream); // tsato hmmm... - // final BlockCompressedInputStream blockIn = new BlockCompressedInputStream(IOUtil.openFileForReading(inputFile)); // tsato hmmm... - blockIn.seek(vOffsetOfFirstRecord); // tsato: if seekablePathStream is not used mFile is null so this throws an error + final long vOffsetOfFirstRecord = SAMUtils.findVirtualOffsetOfFirstRecordInBam(seekablePathStream); + final BlockCompressedInputStream blockIn = new BlockCompressedInputStream(seekablePathStream); + blockIn.seek(vOffsetOfFirstRecord); // This is why we give the BlockCompressedInputStream a SeekablePathStream rather than the regular InputStream final long remainingInBlock = blockIn.available(); // If we found the end of the header then write the remainder of this block out as a @@ -118,7 +119,7 @@ public static void blockCopyBamFile(final Path inputFile, final OutputStream out final BlockCompressedOutputStream blockOut = new BlockCompressedOutputStream(outputStream, (Path)null); IOUtil.transferByStream(blockIn, blockOut, remainingInBlock); blockOut.flush(); - // Don't close blockOut because closing underlying stream would break everything (tsato: why?) + // Don't close blockOut because closing underlying stream would break everything } long pos = BlockCompressedFilePointerUtil.getBlockAddress(blockIn.getFilePointer()); @@ -128,15 +129,24 @@ public static void blockCopyBamFile(final Path inputFile, final OutputStream out } } + // *** START INVESTIGATION *** // // Copy remainder of input stream into output stream (tsato: why would there be anything left? Didn't we close the input stream already?) - final long currentPos = in.getCount(); + // Test wha this does and how it gets the position of the first record without the SeekableInputStream. + final long vOffsetOfFirstRecord2 = SAMUtils.findVirtualOffsetOfFirstRecordInBam(inputFile.toFile()); + FileInputStream inOld = new FileInputStream(inputFile.toFile()); + final long currentPosOld = inOld.getChannel().position(); // tsato: why use the channel here...well, is there a different way of doing this? + InputStream in2 = Files.newInputStream(inputFile); // tsato: might be good to compare performance though, just in case. + // I see, FileInputStream vs InputStream. Is + // *** END INVESTIGATION *** // + + final long currentPos = in.getCount(); // tsato: assuming currentPos is the position after writing the first block, this should be ok. // final long length = inputPath.toFile().length(); // tsato: this right? length of the file in bytes..vs size? -- see below final long length = Files.size(inputFile); // tsato: rename to size final long skipLast = ((term == BlockCompressedInputStream.FileTermination.HAS_TERMINATOR_BLOCK) && skipTerminator) ? BlockCompressedStreamConstants.EMPTY_GZIP_BLOCK.length : 0; final long bytesToWrite = length - skipLast - currentPos; - IOUtil.transferByStream(in, outputStream, bytesToWrite); + IOUtil.transferByStream(in, outputStream, bytesToWrite); // tsato: this method is manually buffered so make sure BufferedReader/Writer is not used (or does that matter?) } catch (final IOException ioe) { throw new RuntimeIOException(ioe); } @@ -183,28 +193,29 @@ public static void gatherWithBlockCopying(final List bams, final File outp } } + @Deprecated + private static OutputStream buildOutputStream(final File outputFile, final boolean createMd5, final boolean createIndex) throws IOException { + return buildOutputStream(outputFile.toPath(), createMd5, createIndex); + } + // tsato: consolidate as needed.... private static OutputStream buildOutputStream(final Path outputFile, final boolean createMd5, final boolean createIndex) throws IOException { OutputStream outputStream = Files.newOutputStream(outputFile); if (createMd5) { - outputStream = new Md5CalculatingOutputStream(outputStream, Paths.get(outputFile + ".md")); // tsato: is this right? + outputStream = new Md5CalculatingOutputStream(outputStream, Paths.get(outputFile + ".md")); // tsato: is this right? maybe need .getURI() } if (createIndex) { - outputStream = new StreamInflatingIndexingOutputStream(outputStream, Paths.get(IOUtil.basename(outputFile.toFile()) + FileExtensions.BAI_INDEX)); // tsato: what happens when I run toFile on a cloud file... + final String baseName = FileNameUtils.getBaseName(outputFile); + outputStream = new StreamInflatingIndexingOutputStream(outputStream, Paths.get(baseName + FileExtensions.BAI_INDEX)); } return outputStream; } -// private static OutputStream buildOutputStream(final File outputFile, final boolean createMd5, final boolean createIndex) throws IOException { -// OutputStream outputStream = new FileOutputStream(outputFile); -// if (createMd5) { -// outputStream = new Md5CalculatingOutputStream(outputStream, new File(outputFile.getAbsolutePath() + ".md5")); -// } -// if (createIndex) { -// outputStream = new StreamInflatingIndexingOutputStream(outputStream, new File(outputFile.getParentFile(), IOUtil.basename(outputFile) + FileExtensions.BAI_INDEX)); -// } -// return outputStream; -// } + + @Deprecated + private static void assertSortOrdersAreEqual(final SAMFileHeader newHeader, final File inputFile) throws IOException { + assertSortOrdersAreEqual(newHeader, inputFile.toPath()); + } private static void assertSortOrdersAreEqual(final SAMFileHeader newHeader, final Path inputFile) throws IOException { final SamReader reader = SamReaderFactory.makeDefault().open(inputFile); diff --git a/src/main/java/htsjdk/samtools/SAMUtils.java b/src/main/java/htsjdk/samtools/SAMUtils.java index 6941bbf657..a3cc74ba10 100644 --- a/src/main/java/htsjdk/samtools/SAMUtils.java +++ b/src/main/java/htsjdk/samtools/SAMUtils.java @@ -675,10 +675,10 @@ public static int combineMapqs(int m1, int m2) { } - // tsatof: + // tsato: to delete public static long findVirtualOffsetOfFirstRecordInBam(final Path bamFile) { try { - SeekableStream ss = new SeekablePathStream(bamFile); // tsato: best way? buffering needed? + SeekableStream ss = new SeekablePathStream(bamFile); return BAMFileReader.findVirtualOffsetOfFirstRecord(ss); } catch (final IOException ioe) { throw new RuntimeEOFException(ioe); From db7129434d00a6dee0b4e44dcbd5eb11b083bc1c Mon Sep 17 00:00:00 2001 From: Takuto Sato Date: Thu, 10 Aug 2023 06:03:44 -0400 Subject: [PATCH 11/14] looks good --- src/main/java/htsjdk/samtools/BamFileIoUtils.java | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/src/main/java/htsjdk/samtools/BamFileIoUtils.java b/src/main/java/htsjdk/samtools/BamFileIoUtils.java index 43bce82d37..1bb85adc57 100644 --- a/src/main/java/htsjdk/samtools/BamFileIoUtils.java +++ b/src/main/java/htsjdk/samtools/BamFileIoUtils.java @@ -129,18 +129,8 @@ public static void blockCopyBamFile(final Path inputFile, final OutputStream out } } - // *** START INVESTIGATION *** // - // Copy remainder of input stream into output stream (tsato: why would there be anything left? Didn't we close the input stream already?) - // Test wha this does and how it gets the position of the first record without the SeekableInputStream. - final long vOffsetOfFirstRecord2 = SAMUtils.findVirtualOffsetOfFirstRecordInBam(inputFile.toFile()); - FileInputStream inOld = new FileInputStream(inputFile.toFile()); - final long currentPosOld = inOld.getChannel().position(); // tsato: why use the channel here...well, is there a different way of doing this? - InputStream in2 = Files.newInputStream(inputFile); // tsato: might be good to compare performance though, just in case. - // I see, FileInputStream vs InputStream. Is - // *** END INVESTIGATION *** // - final long currentPos = in.getCount(); // tsato: assuming currentPos is the position after writing the first block, this should be ok. - // final long length = inputPath.toFile().length(); // tsato: this right? length of the file in bytes..vs size? -- see below + // final long length = inputFile.length(); final long length = Files.size(inputFile); // tsato: rename to size final long skipLast = ((term == BlockCompressedInputStream.FileTermination.HAS_TERMINATOR_BLOCK) && skipTerminator) ? BlockCompressedStreamConstants.EMPTY_GZIP_BLOCK.length : 0; @@ -198,7 +188,6 @@ private static OutputStream buildOutputStream(final File outputFile, final boole return buildOutputStream(outputFile.toPath(), createMd5, createIndex); } - // tsato: consolidate as needed.... private static OutputStream buildOutputStream(final Path outputFile, final boolean createMd5, final boolean createIndex) throws IOException { OutputStream outputStream = Files.newOutputStream(outputFile); if (createMd5) { From 583e0f379fe3763d6e06cb33fae5753a5430a55e Mon Sep 17 00:00:00 2001 From: Takuto Sato Date: Wed, 16 Aug 2023 07:31:55 -0400 Subject: [PATCH 12/14] ready to open a pr --- src/main/java/htsjdk/samtools/SAMUtils.java | 9 ++++++--- src/main/java/htsjdk/samtools/SamReader.java | 2 +- .../htsjdk/samtools/filter/FilteringSamIterator.java | 10 +++++----- .../samtools/util/BlockCompressedInputStream.java | 6 ++---- 4 files changed, 14 insertions(+), 13 deletions(-) diff --git a/src/main/java/htsjdk/samtools/SAMUtils.java b/src/main/java/htsjdk/samtools/SAMUtils.java index a3cc74ba10..31d92cdf5c 100644 --- a/src/main/java/htsjdk/samtools/SAMUtils.java +++ b/src/main/java/htsjdk/samtools/SAMUtils.java @@ -25,12 +25,16 @@ import htsjdk.samtools.seekablestream.SeekablePathStream; import htsjdk.samtools.seekablestream.SeekableStream; -import htsjdk.samtools.util.*; +import htsjdk.samtools.util.BinaryCodec; +import htsjdk.samtools.util.CigarUtil; +import htsjdk.samtools.util.CloserUtil; +import htsjdk.samtools.util.CoordMath; +import htsjdk.samtools.util.RuntimeEOFException; +import htsjdk.samtools.util.StringUtil; import htsjdk.tribble.annotation.Strand; import java.io.File; import java.io.IOException; -import java.io.InputStream; import java.io.UnsupportedEncodingException; import java.math.BigInteger; import java.nio.file.Path; @@ -675,7 +679,6 @@ public static int combineMapqs(int m1, int m2) { } - // tsato: to delete public static long findVirtualOffsetOfFirstRecordInBam(final Path bamFile) { try { SeekableStream ss = new SeekablePathStream(bamFile); diff --git a/src/main/java/htsjdk/samtools/SamReader.java b/src/main/java/htsjdk/samtools/SamReader.java index b427482c3c..74e4016f68 100644 --- a/src/main/java/htsjdk/samtools/SamReader.java +++ b/src/main/java/htsjdk/samtools/SamReader.java @@ -593,7 +593,7 @@ public SAMRecord next() { final SAMRecord previous = checker.getPreviousRecord(); if (!checker.isSorted(result)) { throw new IllegalStateException(String.format( - "Record %s should come after %s when sorting with %s ordering.", // tsato: why doesn't this code get triggered when you have a bad sort order? + "Record %s should come after %s when sorting with %s ordering.", previous.getSAMString().trim(), result.getSAMString().trim(), checker.getSortOrder())); } diff --git a/src/main/java/htsjdk/samtools/filter/FilteringSamIterator.java b/src/main/java/htsjdk/samtools/filter/FilteringSamIterator.java index 9b2e2a9b60..a70156ad6e 100644 --- a/src/main/java/htsjdk/samtools/filter/FilteringSamIterator.java +++ b/src/main/java/htsjdk/samtools/filter/FilteringSamIterator.java @@ -60,9 +60,9 @@ public class FilteringSamIterator implements CloseableIterator { public FilteringSamIterator(final Iterator iterator, final SamRecordFilter filter, final boolean filterByPair) { - if (filterByPair && iterator instanceof SAMRecordIterator) { // tsato: this whole thing has code smell - ((SAMRecordIterator)iterator).assertSorted(SAMFileHeader.SortOrder.queryname); // tsato: wtf is this? - } // tsato: this does not throw an error here if the sort order is mismatched...bizarre. + if (filterByPair && iterator instanceof SAMRecordIterator) { + ((SAMRecordIterator)iterator).assertSorted(SAMFileHeader.SortOrder.queryname); + } this.iterator = new PeekableIterator(iterator); this.filter = filter; @@ -137,7 +137,7 @@ private SAMRecord getNextRecord() { if (filterReadPairs && record.getReadPairedFlag() && record.getFirstOfPairFlag() && iterator.hasNext()) { - SamPairUtil.assertMate(record, iterator.peek()); // tsato: why is the code complaining? + SamPairUtil.assertMate(record, iterator.peek()); if (filter.filterOut(record, iterator.peek())) { // skip second read @@ -148,7 +148,7 @@ private SAMRecord getNextRecord() { } else if (filterReadPairs && record.getReadPairedFlag() && record.getSecondOfPairFlag()) { // assume that we did a pass(first, second) and it passed the filter - return record; // tsato: doesn't the first of pair always appear first if queryname sorted? + return record; } else if (!filter.filterOut(record)) { return record; } diff --git a/src/main/java/htsjdk/samtools/util/BlockCompressedInputStream.java b/src/main/java/htsjdk/samtools/util/BlockCompressedInputStream.java index 681d32475e..d384359fb1 100755 --- a/src/main/java/htsjdk/samtools/util/BlockCompressedInputStream.java +++ b/src/main/java/htsjdk/samtools/util/BlockCompressedInputStream.java @@ -704,13 +704,11 @@ static void readFully(SeekableByteChannel channel, ByteBuffer dst) throws IOExce } } + @Deprecated public static void assertNonDefectiveFile(final File file) throws IOException { - if (checkTermination(file) == FileTermination.DEFECTIVE) { - throw new SAMException(file.getAbsolutePath() + " does not have a valid GZIP block at the end of the file."); - } + assertNonDefectivePath(file.toPath()); } - // tsato: can I consolidate with above? public static void assertNonDefectivePath(final Path file) throws IOException { if (checkTermination(file) == FileTermination.DEFECTIVE) { throw new SAMException(file.toUri() + " does not have a valid GZIP block at the end of the file."); From 4b92e771e5f9fefc117c80559cbee3764fab88e0 Mon Sep 17 00:00:00 2001 From: Takuto Sato Date: Tue, 29 Aug 2023 11:50:30 -0400 Subject: [PATCH 13/14] louis --- .../java/htsjdk/samtools/BamFileIoUtils.java | 91 ++++++------- src/main/java/htsjdk/samtools/SAMUtils.java | 4 +- .../htsjdk/samtools/SamFileValidator.java | 7 +- .../util/BlockCompressedInputStream.java | 17 ++- .../htsjdk/samtools/util/FileExtensions.java | 1 + .../java/htsjdk/samtools/util/IOUtil.java | 15 ++- .../samtools/BamFileIoUtilsUnitTest.java | 123 +++++++++--------- .../java/htsjdk/samtools/HtsjdkTestUtils.java | 15 +++ .../java/htsjdk/samtools/util/IOUtilTest.java | 23 ++++ ...rio.HiSeq.WGS.b37.NA12878.20.first.500.bai | Bin 0 -> 96 bytes ...rio.HiSeq.WGS.b37.NA12878.20.first.500.bam | Bin 0 -> 130040 bytes 11 files changed, 179 insertions(+), 117 deletions(-) create mode 100644 src/test/java/htsjdk/samtools/HtsjdkTestUtils.java create mode 100644 src/test/resources/htsjdk/samtools/cram/CEUTrio.HiSeq.WGS.b37.NA12878.20.first.500.bai create mode 100644 src/test/resources/htsjdk/samtools/cram/CEUTrio.HiSeq.WGS.b37.NA12878.20.first.500.bam diff --git a/src/main/java/htsjdk/samtools/BamFileIoUtils.java b/src/main/java/htsjdk/samtools/BamFileIoUtils.java index 1bb85adc57..709c8ed76d 100644 --- a/src/main/java/htsjdk/samtools/BamFileIoUtils.java +++ b/src/main/java/htsjdk/samtools/BamFileIoUtils.java @@ -1,6 +1,6 @@ package htsjdk.samtools; -import htsjdk.samtools.cram.io.CountingInputStream; +import htsjdk.beta.exception.HtsjdkException; import htsjdk.samtools.seekablestream.SeekablePathStream; import htsjdk.samtools.util.BlockCompressedFilePointerUtil; import htsjdk.samtools.util.BlockCompressedInputStream; @@ -12,13 +12,11 @@ import htsjdk.samtools.util.Log; import htsjdk.samtools.util.Md5CalculatingOutputStream; import htsjdk.samtools.util.RuntimeIOException; -import org.apache.commons.compress.utils.FileNameUtils; +import htsjdk.utils.ValidationUtils; import java.io.File; -import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; -import java.io.InputStream; import java.io.OutputStream; import java.nio.file.Files; import java.nio.file.Path; @@ -46,9 +44,8 @@ public static void reheaderBamFile(final SAMFileHeader samFileHeader, final Path /** * Support File input types for backward compatibility. Use the same method with Path inputs below. */ - @Deprecated public static void reheaderBamFile(final SAMFileHeader samFileHeader, final File inputFile, final File outputFile, final boolean createMd5, final boolean createIndex) { - reheaderBamFile(samFileHeader, inputFile.toPath(), outputFile.toPath(), createMd5, createIndex); + reheaderBamFile(samFileHeader, IOUtil.toPath(inputFile), IOUtil.toPath(outputFile), createMd5, createIndex); } /** @@ -61,8 +58,10 @@ public static void reheaderBamFile(final SAMFileHeader samFileHeader, final File * @param createIndex Whether or not to create an index file for the new BAM */ public static void reheaderBamFile(final SAMFileHeader samFileHeader, final Path inputFile, final Path outputFile, final boolean createMd5, final boolean createIndex) { + ValidationUtils.nonNull(inputFile); + ValidationUtils.nonNull(outputFile); IOUtil.assertFileIsReadable(inputFile); - // IOUtil.assertFileIsWritable(outputFile); // tsato: what do I do with this... + IOUtil.assertFileIsWritable(inputFile); try { BlockCompressedInputStream.assertNonDefectivePath(inputFile); @@ -80,63 +79,59 @@ public static void reheaderBamFile(final SAMFileHeader samFileHeader, final Path } } - /** - * @deprecated as of August 2023. Use the method by the same name below with Path input - */ - @Deprecated public static void blockCopyBamFile(final File inputFile, final OutputStream outputStream, final boolean skipHeader, final boolean skipTerminator) { - blockCopyBamFile(inputFile.toPath(), outputStream, skipHeader, skipTerminator); + blockCopyBamFile(IOUtil.toPath(inputFile), outputStream, skipHeader, skipTerminator); } /** - * Copy data from a BAM file to an OutputStream by directly copying the gzip blocks + * Copy data from a BAM file to an OutputStream by directly copying the gzip blocks. * - * @param inputFile The file to be copied + * @param inputFile The BAM file to be copied * @param outputStream The stream to write the copied data to * @param skipHeader If true, the header of the input file will not be copied to the output stream * @param skipTerminator If true, the terminator block of the input file will not be written to the output stream */ public static void blockCopyBamFile(final Path inputFile, final OutputStream outputStream, final boolean skipHeader, final boolean skipTerminator) { - // tsato: use CountingInputStream to replace FileInputStream. The latter has .getChannel().getPosition - // The regular InputStream from Files.newInputStream() doesn't have it, but I might be able to do so by creating Channel object and avoid using CountingInputStream - try (final CountingInputStream in = new CountingInputStream(Files.newInputStream(inputFile))){ - // try (final InputStream in = Files.newInputStream(inputFile)){ + try (final SeekablePathStream in = new SeekablePathStream(inputFile)){ // a) It's good to check that the end of the file is valid and b) we need to know if there's a terminator block and not copy it if skipTerminator is true final BlockCompressedInputStream.FileTermination term = BlockCompressedInputStream.checkTermination(inputFile); if (term == BlockCompressedInputStream.FileTermination.DEFECTIVE) throw new SAMException(inputFile.toUri() + " does not have a valid GZIP block at the end of the file."); - if (skipHeader) { // tsato: this method assumes bam file...should I test cram? - final SeekablePathStream seekablePathStream = new SeekablePathStream(inputFile); - final long vOffsetOfFirstRecord = SAMUtils.findVirtualOffsetOfFirstRecordInBam(seekablePathStream); - final BlockCompressedInputStream blockIn = new BlockCompressedInputStream(seekablePathStream); - blockIn.seek(vOffsetOfFirstRecord); // This is why we give the BlockCompressedInputStream a SeekablePathStream rather than the regular InputStream - final long remainingInBlock = blockIn.available(); - - // If we found the end of the header then write the remainder of this block out as a - // new gzip block and then break out of the while loop - if (remainingInBlock >= 0) { - final BlockCompressedOutputStream blockOut = new BlockCompressedOutputStream(outputStream, (Path)null); - IOUtil.transferByStream(blockIn, blockOut, remainingInBlock); - blockOut.flush(); - // Don't close blockOut because closing underlying stream would break everything - } - - long pos = BlockCompressedFilePointerUtil.getBlockAddress(blockIn.getFilePointer()); - blockIn.close(); - while (pos > 0) { - pos -= in.skip(pos); + if (skipHeader) { + final long vOffsetOfFirstRecord = SAMUtils.findVirtualOffsetOfFirstRecordInBam(inputFile); + + // tsato: curious --- why do we need BlockCompressedInputStream at all here? + try (final BlockCompressedInputStream blockIn = new BlockCompressedInputStream(inputFile)) { + blockIn.seek(vOffsetOfFirstRecord); + final long remainingInBlock = blockIn.available(); + + // If we found the end of the header then write the remainder of this block out as a + // new gzip block and then break out of the while loop (tsato: update this comment) + if (remainingInBlock >= 0) { + final BlockCompressedOutputStream blockOut = new BlockCompressedOutputStream(outputStream, (Path) null); + IOUtil.transferByStream(blockIn, blockOut, remainingInBlock); + blockOut.flush(); + // Don't close blockOut because closing underlying stream would break everything + } + + final long pos = BlockCompressedFilePointerUtil.getBlockAddress(blockIn.getFilePointer()); + blockIn.close(); // tsato: why doesn't IntelliJ say this is unnecessary? + + in.seek(pos); + } catch (IOException e){ + throw new HtsjdkException("Encountered an error.", e); } } - final long currentPos = in.getCount(); // tsato: assuming currentPos is the position after writing the first block, this should be ok. - // final long length = inputFile.length(); - final long length = Files.size(inputFile); // tsato: rename to size + // Copy remainder of input stream into output stream + final long currentPos = in.position(); + final long length = Files.size(inputFile); final long skipLast = ((term == BlockCompressedInputStream.FileTermination.HAS_TERMINATOR_BLOCK) && skipTerminator) ? BlockCompressedStreamConstants.EMPTY_GZIP_BLOCK.length : 0; final long bytesToWrite = length - skipLast - currentPos; - IOUtil.transferByStream(in, outputStream, bytesToWrite); // tsato: this method is manually buffered so make sure BufferedReader/Writer is not used (or does that matter?) + IOUtil.transferByStream(in, outputStream, bytesToWrite); } catch (final IOException ioe) { throw new RuntimeIOException(ioe); } @@ -162,7 +157,7 @@ public static void gatherWithBlockCopying(final List bams, final File outp for (final File f : bams) { LOG.info(String.format("Block copying %s ...", f.getAbsolutePath())); - blockCopyBamFile(f.toPath(), out, !isFirstFile, true); + blockCopyBamFile(IOUtil.toPath(f), out, !isFirstFile, true); isFirstFile = false; } @@ -183,19 +178,17 @@ public static void gatherWithBlockCopying(final List bams, final File outp } } - @Deprecated private static OutputStream buildOutputStream(final File outputFile, final boolean createMd5, final boolean createIndex) throws IOException { - return buildOutputStream(outputFile.toPath(), createMd5, createIndex); + return buildOutputStream(IOUtil.toPath(outputFile), createMd5, createIndex); } private static OutputStream buildOutputStream(final Path outputFile, final boolean createMd5, final boolean createIndex) throws IOException { OutputStream outputStream = Files.newOutputStream(outputFile); if (createMd5) { - outputStream = new Md5CalculatingOutputStream(outputStream, Paths.get(outputFile + ".md")); // tsato: is this right? maybe need .getURI() + outputStream = new Md5CalculatingOutputStream(outputStream, IOUtil.addExtension(outputFile, FileExtensions.MD5)); } if (createIndex) { - final String baseName = FileNameUtils.getBaseName(outputFile); - outputStream = new StreamInflatingIndexingOutputStream(outputStream, Paths.get(baseName + FileExtensions.BAI_INDEX)); + outputStream = new StreamInflatingIndexingOutputStream(outputStream, outputFile.resolveSibling(outputFile.getFileName() + FileExtensions.BAI_INDEX)); } return outputStream; } @@ -203,7 +196,7 @@ private static OutputStream buildOutputStream(final Path outputFile, final boole @Deprecated private static void assertSortOrdersAreEqual(final SAMFileHeader newHeader, final File inputFile) throws IOException { - assertSortOrdersAreEqual(newHeader, inputFile.toPath()); + assertSortOrdersAreEqual(newHeader, IOUtil.toPath(inputFile)); } private static void assertSortOrdersAreEqual(final SAMFileHeader newHeader, final Path inputFile) throws IOException { diff --git a/src/main/java/htsjdk/samtools/SAMUtils.java b/src/main/java/htsjdk/samtools/SAMUtils.java index 31d92cdf5c..fde94cde8c 100644 --- a/src/main/java/htsjdk/samtools/SAMUtils.java +++ b/src/main/java/htsjdk/samtools/SAMUtils.java @@ -679,9 +679,9 @@ public static int combineMapqs(int m1, int m2) { } + public static long findVirtualOffsetOfFirstRecordInBam(final Path bamFile) { - try { - SeekableStream ss = new SeekablePathStream(bamFile); + try (SeekableStream ss = new SeekablePathStream(bamFile)){ return BAMFileReader.findVirtualOffsetOfFirstRecord(ss); } catch (final IOException ioe) { throw new RuntimeEOFException(ioe); diff --git a/src/main/java/htsjdk/samtools/SamFileValidator.java b/src/main/java/htsjdk/samtools/SamFileValidator.java index 50f289e3ab..bbcede3d76 100644 --- a/src/main/java/htsjdk/samtools/SamFileValidator.java +++ b/src/main/java/htsjdk/samtools/SamFileValidator.java @@ -210,7 +210,7 @@ public boolean validateSamFileVerbose(final SamReader samReader, final Reference } public void validateBamFileTermination(final File inputFile) { - validateBamFileTermination(inputFile.toPath()); + validateBamFileTermination(IOUtil.toPath(inputFile)); } public void validateBamFileTermination(final Path inputFile) { @@ -222,12 +222,11 @@ public void validateBamFileTermination(final Path inputFile) { BlockCompressedInputStream.checkTermination(inputFile); if (terminationState.equals(BlockCompressedInputStream.FileTermination.DEFECTIVE)) { addError(new SAMValidationError(Type.TRUNCATED_FILE, "BAM file has defective last gzip block", - inputFile.toAbsolutePath().toString())); // tsato: confirm toAbsolutePath() is the right thing to do here + inputFile.toUri().toString())); } else if (terminationState.equals(BlockCompressedInputStream.FileTermination.HAS_HEALTHY_LAST_BLOCK)) { addError(new SAMValidationError(Type.BAM_FILE_MISSING_TERMINATOR_BLOCK, "Older BAM file -- does not have terminator block", - inputFile.toAbsolutePath().toString())); - + inputFile.toUri().toString())); } } catch (IOException e) { throw new SAMException("IOException", e); diff --git a/src/main/java/htsjdk/samtools/util/BlockCompressedInputStream.java b/src/main/java/htsjdk/samtools/util/BlockCompressedInputStream.java index d384359fb1..898b15c495 100755 --- a/src/main/java/htsjdk/samtools/util/BlockCompressedInputStream.java +++ b/src/main/java/htsjdk/samtools/util/BlockCompressedInputStream.java @@ -29,6 +29,7 @@ import htsjdk.samtools.seekablestream.SeekableBufferedStream; import htsjdk.samtools.seekablestream.SeekableFileStream; import htsjdk.samtools.seekablestream.SeekableHTTPStream; +import htsjdk.samtools.seekablestream.SeekablePathStream; import htsjdk.samtools.seekablestream.SeekableStream; import htsjdk.samtools.util.zip.InflaterFactory; @@ -63,7 +64,7 @@ public class BlockCompressedInputStream extends InputStream implements LocationA private InputStream mStream = null; private boolean mIsClosed = false; - private SeekableStream mFile = null; // tsato: change name to mPath? + private SeekableStream mFile = null; private byte[] mFileBuffer = null; private DecompressedBlock mCurrentBlock = null; private int mCurrentOffset = 0; @@ -123,6 +124,15 @@ public BlockCompressedInputStream(final File file) throws IOException { this(file, BlockGunzipper.getDefaultInflaterFactory()); } + + /** + * Equivalent constructor for Path as the one that takes a File. Supports seeking. + */ + public BlockCompressedInputStream(final Path file) throws IOException { + this(new SeekablePathStream(file)); + } + + /** * Use this ctor if you wish to call seek() * @param file source of bytes @@ -135,6 +145,7 @@ public BlockCompressedInputStream(final File file, final InflaterFactory inflate blockGunzipper = new BlockGunzipper(inflaterFactory); } + /** * @param url source of bytes */ @@ -359,7 +370,7 @@ public void seek(final long pos) throws IOException { } // Cannot seek on streams that are not file based - if (mFile == null) { // tsato: mFile is a seekable stream--- + if (mFile == null) { throw new IOException(CANNOT_SEEK_STREAM_MSG); } @@ -706,7 +717,7 @@ static void readFully(SeekableByteChannel channel, ByteBuffer dst) throws IOExce @Deprecated public static void assertNonDefectiveFile(final File file) throws IOException { - assertNonDefectivePath(file.toPath()); + assertNonDefectivePath(IOUtil.toPath(file)); } public static void assertNonDefectivePath(final Path file) throws IOException { diff --git a/src/main/java/htsjdk/samtools/util/FileExtensions.java b/src/main/java/htsjdk/samtools/util/FileExtensions.java index fc2e37d6c6..b67154a629 100755 --- a/src/main/java/htsjdk/samtools/util/FileExtensions.java +++ b/src/main/java/htsjdk/samtools/util/FileExtensions.java @@ -75,6 +75,7 @@ public final class FileExtensions { public static final String GZI = ".gzi"; public static final String SBI = ".sbi"; public static final String CSI = ".csi"; + public static final String MD5 = ".md5"; public static final Set BLOCK_COMPRESSED = Collections.unmodifiableSet(new HashSet<>(Arrays.asList(".gz", ".gzip", ".bgz", ".bgzf"))); diff --git a/src/main/java/htsjdk/samtools/util/IOUtil.java b/src/main/java/htsjdk/samtools/util/IOUtil.java index d473bebbe0..ce3c96fcd5 100644 --- a/src/main/java/htsjdk/samtools/util/IOUtil.java +++ b/src/main/java/htsjdk/samtools/util/IOUtil.java @@ -582,6 +582,18 @@ public static void assertFilesAreWritable(final List files) { for (final File file : files) assertFileIsWritable(file); } + + /** + * In some filesystems (e.g. google cloud) it may not make sense to check writability. + * This method only checks writability when it's (i.e. for now when the path points to a file + * in the local filesystem) + */ + public static void assertFileIsWritable(final Path path){ // tsato: perhaps the input type should be IOPath + if (path.toUri().getScheme().equals("file")){ + IOUtil.assertFileIsWritable(path.toFile()); + } + } + /** * Checks that a directory is non-null, extent, writable and a directory * otherwise a runtime exception is thrown. @@ -867,7 +879,8 @@ public static OutputStream openFileForMd5CalculatingWriting(final File file) { } public static OutputStream openFileForMd5CalculatingWriting(final Path file) { - return new Md5CalculatingOutputStream(IOUtil.openFileForWriting(file), file.resolve(".md5")); + final Path digestFile = file.resolveSibling(file.getFileName() + FileExtensions.MD5); + return new Md5CalculatingOutputStream(IOUtil.openFileForWriting(file), digestFile); } /** diff --git a/src/test/java/htsjdk/samtools/BamFileIoUtilsUnitTest.java b/src/test/java/htsjdk/samtools/BamFileIoUtilsUnitTest.java index 006754c75e..33d54f7e7e 100644 --- a/src/test/java/htsjdk/samtools/BamFileIoUtilsUnitTest.java +++ b/src/test/java/htsjdk/samtools/BamFileIoUtilsUnitTest.java @@ -3,66 +3,72 @@ import htsjdk.HtsjdkTest; import htsjdk.beta.exception.HtsjdkException; import htsjdk.samtools.util.BlockCompressedInputStream; +import htsjdk.samtools.util.FileExtensions; +import htsjdk.samtools.util.IOUtil; import org.testng.Assert; import org.testng.annotations.DataProvider; import org.testng.annotations.Test; 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.nio.file.Paths; import java.util.Iterator; public class BamFileIoUtilsUnitTest extends HtsjdkTest { - final static String TEST_DATA_DIR = "src/test/resources/htsjdk/samtools/cram/"; - final static String NA12878_8000 = TEST_DATA_DIR + "CEUTrio.HiSeq.WGS.b37.NA12878.20.first.8000.bam"; - final static String NA12878_20_21 = TEST_DATA_DIR + "NA12878.20.21.unmapped.orig.bam"; - final static int DEFAULT_NUM_RECORDS_TO_COMPARE = 10; - - @Test - public void testReheaderBamFile(){ - final File originalBam = new File(this.NA12878_8000); - SAMFileHeader header = SamReaderFactory.make().getFileHeader(new File(NA12878_20_21)); - try { - final File output = File.createTempFile("output", ".bam"); - BamFileIoUtils.reheaderBamFile(header, originalBam.toPath(), output.toPath()); - - // Confirm that the header has been replaced - final SamReader outputReader = SamReaderFactory.make().open(output.toPath()); - Assert.assertEquals(outputReader.getFileHeader(), header); - - // Confirm that the reads are the same as the original - Assert.assertTrue(compareBamReads(originalBam, output, DEFAULT_NUM_RECORDS_TO_COMPARE)); - } catch (IOException e){ - throw new HtsjdkException("Could not create a temporary output file.", e); + @DataProvider(name="ReheaderBamFileTestInput") + public Object[][] getReheaderBamFileTestInput() { // tsato: is this the right naming scheme? e.g. get(method-name)Input + // tsato: extract this as a method that takes a number of arguments and returns 2^n elements + return new Object[][] { + {true, true}, + {true, false}, + {false, true}, + {false, false} + }; + } + + @Test(dataProvider = "ReheaderBamFileTestInput") + public void testReheaderBamFile(final boolean createMd5, final boolean createIndex) throws IOException { + final File originalBam =HtsjdkTestUtils.NA12878_500; + SAMFileHeader header = SamReaderFactory.make().getFileHeader(HtsjdkTestUtils.NA12878_500); + header.addComment("This is a new, modified header"); + + final Path output = Files.createTempFile("output", ".bam"); + BamFileIoUtils.reheaderBamFile(header, originalBam.toPath(), output, createMd5, createIndex); + + // Confirm that the header has been replaced + final SamReader outputReader = SamReaderFactory.make().open(output); + Assert.assertEquals(outputReader.getFileHeader(), header); + + // Check that the reads are the same as the original + // tsato: should I be using something similar to IOUtil.toPath for converting Path -> File to propagate null? + assertBamRecordsEqual(originalBam, output.toFile()); + + if (createMd5){ + Assert.assertTrue(Files.exists(output.resolveSibling(output.getFileName() + FileExtensions.MD5))); + } + + if (createIndex){ + Assert.assertTrue(SamReaderFactory.make().open(output).hasIndex()); } } /** - * Compare first (numRecordsToCompare) reads of two bam files. - * In particular we do not check for equality of the headers. - * - * @param numRecordsToCompare the number of reads to compare - * - * @return true if the first (numRecordsToCompare) reads are equal, else false + * Compares all the reads in the two bam files are equal (but does not check the headers). */ - private boolean compareBamReads(final File bam1, final File bam2, final int numRecordsToCompare){ - final Iterator originalBamIterator = SamReaderFactory.make().open(bam1.toPath()).iterator(); - final Iterator outputBamIterator = SamReaderFactory.make().open(bam2.toPath()).iterator(); - for (int i = 0; i < numRecordsToCompare; i++){ - final SAMRecord originalRead = originalBamIterator.next(); - final SAMRecord outputRead = outputBamIterator.next(); - if (! originalRead.equals(outputRead)){ - return false; - } + private void assertBamRecordsEqual(final File bam1, final File bam2){ + try (SamReader reader1 = SamReaderFactory.make().open(bam1); + SamReader reader2 = SamReaderFactory.make().open(bam2)) { + final Iterator originalBamIterator = reader1.iterator(); + final Iterator outputBamIterator = reader2.iterator(); + + Assert.assertEquals(originalBamIterator, outputBamIterator); + } catch (Exception e){ + throw new HtsjdkException("Encountered an error reading bam files: " + bam1.getAbsolutePath() + " and " + bam2.getAbsolutePath(), e); } - return true; } - // tsato: kinda ugly? @DataProvider(name="BlockCopyBamFileTestInput") public Object[][] getBlockCopyBamFileTestInput() { return new Object[][] { @@ -75,29 +81,30 @@ public Object[][] getBlockCopyBamFileTestInput() { @Test(dataProvider = "BlockCopyBamFileTestInput") public void testBlockCopyBamFile(final boolean skipHeader, final boolean skipTerminator) throws IOException { - System.out.println(skipHeader + ", " + skipTerminator); final File output = File.createTempFile("output", ".bam"); - final OutputStream out = Files.newOutputStream(output.toPath()); - final Path input = Paths.get(NA12878_8000); + try (final OutputStream out = Files.newOutputStream(output.toPath())) { + final Path input = IOUtil.toPath(HtsjdkTestUtils.NA12878_500); - BamFileIoUtils.blockCopyBamFile(Paths.get(NA12878_8000), out, skipHeader, skipTerminator); + BamFileIoUtils.blockCopyBamFile(IOUtil.toPath(HtsjdkTestUtils.NA12878_500), out, skipHeader, skipTerminator); - final SamReader inputReader = SamReaderFactory.make().open(input); - final SamReader outputReader = SamReaderFactory.make().open(output); + final SamReader inputReader = SamReaderFactory.make().open(input); + final SamReader outputReader = SamReaderFactory.make().open(output); - if (skipHeader){ - SAMFileHeader h = outputReader.getFileHeader(); - Assert.assertTrue(h.getReadGroups().isEmpty()); // a proxy for the header being empty - } else { - Assert.assertEquals(outputReader.getFileHeader(), inputReader.getFileHeader()); - // Reading will fail when the header is absent - Assert.assertTrue(compareBamReads(input.toFile(), output, DEFAULT_NUM_RECORDS_TO_COMPARE)); - } + if (skipHeader) { + SAMFileHeader h = outputReader.getFileHeader(); + Assert.assertTrue(h.getReadGroups().isEmpty()); // a proxy for the header being empty + } else { + Assert.assertEquals(outputReader.getFileHeader(), inputReader.getFileHeader()); + // Reading will fail when the header is absent + assertBamRecordsEqual(input.toFile(), output); + } - if (skipTerminator){ - BlockCompressedInputStream.FileTermination termination = BlockCompressedInputStream.checkTermination(output); - Assert.assertEquals(termination, BlockCompressedInputStream.FileTermination.HAS_HEALTHY_LAST_BLOCK); + if (skipTerminator) { + BlockCompressedInputStream.FileTermination termination = BlockCompressedInputStream.checkTermination(output); + Assert.assertEquals(termination, BlockCompressedInputStream.FileTermination.HAS_HEALTHY_LAST_BLOCK); + } + } catch (IOException e){ + throw new HtsjdkException("Caught an IO exception block copying a bam file to " + output.getAbsolutePath(), e); } - } } \ No newline at end of file diff --git a/src/test/java/htsjdk/samtools/HtsjdkTestUtils.java b/src/test/java/htsjdk/samtools/HtsjdkTestUtils.java new file mode 100644 index 0000000000..9eb4831fc0 --- /dev/null +++ b/src/test/java/htsjdk/samtools/HtsjdkTestUtils.java @@ -0,0 +1,15 @@ +package htsjdk.samtools; + +import htsjdk.io.HtsPath; + +import java.io.File; + +public class HtsjdkTestUtils { + // tsato: change to HtsPath... + public static final String TEST_DATA_DIR = "src/test/resources/htsjdk/samtools/cram/"; + // tsato: silly to use HtsPath when I know it can't handle e.g. gcloud files? + // opting to use file here to stress that these are *local* test files + public static final File NA12878_8000 = new File(TEST_DATA_DIR + "CEUTrio.HiSeq.WGS.b37.NA12878.20.first.8000.bam"); + public static final File NA12878_20_21 = new File(TEST_DATA_DIR + "NA12878.20.21.unmapped.orig.bam"); + final static File NA12878_500 = new File(TEST_DATA_DIR + "CEUTrio.HiSeq.WGS.b37.NA12878.20.first.500.bam"); +} diff --git a/src/test/java/htsjdk/samtools/util/IOUtilTest.java b/src/test/java/htsjdk/samtools/util/IOUtilTest.java index 8354b5ba29..b61db443f0 100644 --- a/src/test/java/htsjdk/samtools/util/IOUtilTest.java +++ b/src/test/java/htsjdk/samtools/util/IOUtilTest.java @@ -35,7 +35,14 @@ import java.nio.file.Paths; import java.nio.file.spi.FileSystemProvider; +import htsjdk.beta.exception.HtsjdkException; +import htsjdk.samtools.BAMFileWriter; +import htsjdk.samtools.BamFileIoUtils; +import htsjdk.samtools.HtsjdkTestUtils; import htsjdk.samtools.SAMException; +import htsjdk.samtools.SAMFileHeader; +import htsjdk.samtools.SamReaderFactory; +import org.apache.commons.compress.compressors.FileNameUtil; import org.testng.Assert; import org.testng.annotations.AfterClass; import org.testng.annotations.BeforeClass; @@ -796,5 +803,21 @@ public void isGZIPInputStreamTest(byte[] data, boolean isGzipped) throws IOExcep Assert.assertEquals(IOUtil.isGZIPInputStream(inputStream), isGzipped); } } + + @Test + public void testOpenFileForMd5CalculatingWriting() throws IOException { + Path output = Files.createTempFile("test", FileExtensions.BAM); + + try (final OutputStream outputStream = IOUtil.openFileForMd5CalculatingWriting(output)){ + // tsato: perhaps BamFileIoUtils is a better place for this test + BamFileIoUtils.blockCopyBamFile(IOUtil.toPath(HtsjdkTestUtils.NA12878_8000), outputStream, false, false); + } catch (IOException e) { + throw new HtsjdkException("Encountered an IO error", e); + } + + final String md5FileName = output.getFileName() + FileExtensions.MD5; + Assert.assertTrue(md5FileName.endsWith(".bam.md5")); + Assert.assertTrue(Files.exists(output.resolveSibling(md5FileName))); + } } diff --git a/src/test/resources/htsjdk/samtools/cram/CEUTrio.HiSeq.WGS.b37.NA12878.20.first.500.bai b/src/test/resources/htsjdk/samtools/cram/CEUTrio.HiSeq.WGS.b37.NA12878.20.first.500.bai new file mode 100644 index 0000000000000000000000000000000000000000..9b0cb993ea95c9ed11e7f66eccc9695fc7338a16 GIT binary patch literal 96 ycmZ>A^kigYU|?VZVoxCk1`yjt1V{iuLeSdBAimcmh&;Nu4>MR9oIutK69fRz90%zD literal 0 HcmV?d00001 diff --git a/src/test/resources/htsjdk/samtools/cram/CEUTrio.HiSeq.WGS.b37.NA12878.20.first.500.bam b/src/test/resources/htsjdk/samtools/cram/CEUTrio.HiSeq.WGS.b37.NA12878.20.first.500.bam new file mode 100644 index 0000000000000000000000000000000000000000..a2b96fecb46927e307e2ced9b962aacc1c526390 GIT binary patch literal 130040 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

SbnO3E>XcB^^}v_Y}%q^P+&UaR-L z8uXx8o6b&CXtEtbzE#w3ccoRM2cyhrg20uRJmlgvdocdHYzR+s;L6lvtRc`l3OJ7IMuY`L%yW3Z$6z$wHh8Hq#>XB`IUW}dHZQha?7O9 zvw8JXhp~)x?bKRk$Lw|tQ$O*Vb?uYc)lZFPJ6o5DsS0T&`9hR{vb)atFpAU&*;3e2 zzbZk7Zbq(f-*4#7#A1cn)rxE4Lzf8gNentgBy^2iS2SP^lU`-L)v$yV9@}eJr{Pge z1pH3UIw8(~lHJ%()N6*|0G82ISHx?IV6EJd1VJP{jniCdGKFIO8`?-^2#vy?Ok|HG zBF((l3jxnxu-wi-XXrEHuX#JAdMb(w`H}Pp2vuqm8nYoXP!?IT!Qxt5TyG2|E!9|U z??$YH%by9H=;ok}5EBPP#>*(zEQ`G#4t{9XSL2m2DRz8cx-K@~Pot=#24o2iLZMS^ zt@;q0_bMIT=c%txa)aX4Q=f0^C*lpFao!gfyUrzkxQEu~Bw>_yX3)5cul3CF-bK9w z(L8wzDfzh|P4Cdg%HLS@p$)9}2~t8A@{2b_bHwpuGXbso6CqD)N|aEzGS=_kzkdxZ znUPD}ohS`)Oh%%wZ+GgTMAu%y_Btl8PGf(#by;O)W*VVRZ30W;GX)C49FBE@GF)L_eX zOwb{*dN#1IQ)q;}va-iO4x}>&0+)C5x@OW}^0{ZUQFW8mKWA0(r1|YR$tqN3Gd$m- zA_u9x*}k+S;ua-K5!Ku-LyCu%7g?{d+Hr0%nyi2!S4h5De=p%#{|^zTCGS5t^X1Xy zzofbG8{G^mDJ-g7V%eh7-w8>Xdd;37vtUtdQ&_wq=0>LoculQ<%?}M*b89w%cJVmF zq6JC0%+_28Ji#rrp$&=;+Vz;zYfV2b7vJ~<50=~XlKRHXbiC$L6Qt7v`rtSClihNt zM##1-LY3^g7?54b}{8fjY%5$IN$$hR)G(P|B2vS*ZpQi@A`mm(za;0+AibLLte#p+Z1WUzz% zD$kuLAM)Z}Gemv8lv?Q1_~2cyF0h`;410RfkX60YskWizhzqUf@$!Px!K*ICf56?X zv2y2>ib+tMywdc#9`xlr_(cZvzoa3bA{bKw?lsuzD#a{l*$?#^9Z(cuR=MqMbI>Bj zOzUd1bOqE(X1r(#)jdP|%0$#2Uo7l7wLx2-muOyO??egH)>o|(@wFrJ_Y56aMn}Yx z&wDNowd!ZB(1X#It&N=+DDstbi&SWG? znN|OyCH)D22I4kJ)gFv{gWjo%o3aJ+)^Lrba5NkjKr5&d^8E)E2yfLRY7oOaT^`7#GIRU^DG1UPY+}QU2R6L z_HxLZZfUDxB<&JC2`4tVDOA{sxx1;ce@W`bV&hHe!}+frp|D5#`t9iC5NxNE`F*v; zQ>$t#Z6-0`nl0>Db(%r(QGd%TY{vqf{`%0ATukbG8!OkH2XjBQ)b=WF>;j;dFv*qW zrfsS(D<;3;cur^P1DLF|so!nZr*v6=7D><@B@xX6NOV(Z``=SXdiB=Y%1zTIYVjtVmp)XAk9$zMImX0^|% z02tiSnk&uKa%dbe+t%FUf3xusq9?J~$wGspR$>aLw$fBswO;wYw%1m1S99T~73+^C z>Bx5{OQU61j)H0=hEk&&(KBgqJ(L=j$*$_|C}UA@$NZ0$MUq#<1%SFiODnBHFp4R` z0dSkB&X?T63<3MdHFqCd#ABSQ7=T?ga&>lV*!y$ zYyayA4dx0I|MzUf!;eovIb+{M)0)t5<#E@g?#GNo`4kkN%XS@kbWAeg8rDl^Fs*_; zLE5@7X!dQhWS~)=3FiT;@xtnznC1@S9E*x4ZJwtY?Mt8ZD^UsN)uX{WT+zH86l`Hy zRTgEIiyoLfxt{5JrojU)x5F36>8>+?u2(&^wv!(_Diu12z;EPI1y zWe~IdQ-=}GqlMa$m~T?}gn1HsIupB-6G(A0@w|M8SKpbd)gU^dd)bUUTv%?`Mq;w_ zbQΜ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

({7@HA&$$A~G&9?leN!-O1tT-^ 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

UX-qc1dnma}`L@eLmPw_jSR(8^%ey&UW07 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