/*
* This file is part of muCommander, http://www.mucommander.com
* Copyright (C) 2002-2016 Maxence Bernard
*
* muCommander is free software; you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* muCommander is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.mucommander.commons.io;
import java.io.*;
/**
* This class provides convenience static methods that operate on streams. All read/write buffers are allocated using
* {@link BufferPool} for memory efficiency reasons.
*
* @author Maxence Bernard
*/
public class StreamUtils {
/**
* This method is a shorthand for {@link #copyStream(java.io.InputStream, java.io.OutputStream, int)} called with a
* {@link BufferPool#getDefaultBufferSize() default buffer size}.
*
* @param in the InputStream to read from
* @param out the OutputStream to write to
* @return the number of bytes that were copied
* @throws FileTransferException if something went wrong while reading from or writing to one of the provided streams
*/
public static long copyStream(InputStream in, OutputStream out) throws FileTransferException {
return copyStream(in, out, BufferPool.getDefaultBufferSize());
}
/**
* This method is a shorthand for {@link #copyStream(java.io.InputStream, java.io.OutputStream, int, long)} called
* with a {@link Long#MAX_VALUE}.
*
* @param in the InputStream to read from
* @param out the OutputStream to write to
* @param bufferSize size of the buffer to use, in bytes
* @return the number of bytes that were copied
* @throws FileTransferException if something went wrong while reading from or writing to one of the provided streams
*/
public static long copyStream(InputStream in, OutputStream out, int bufferSize) throws FileTransferException {
return copyStream(in, out, bufferSize, Long.MAX_VALUE);
}
/**
* Shorthand for {@link #copyStream(InputStream, OutputStream, byte[], long)} called with a buffer of the specified
* size retrieved from {@link BufferPool}.
*
* @param in the InputStream to read from
* @param out the OutputStream to write to
* @param bufferSize size of the buffer to use, in bytes
* @param length number of bytes to copy from InputStream
* @return the number of bytes that were copied
* @throws FileTransferException if something went wrong while reading from or writing to one of the provided streams
*/
public static long copyStream(InputStream in, OutputStream out, int bufferSize, long length) throws FileTransferException {
// Use BufferPool to reuse any available buffer of the same size
byte buffer[] = BufferPool.getByteArray(bufferSize);
try {
return copyStream(in, out, buffer, length);
}
finally {
// Make the buffer available for further use
BufferPool.releaseByteArray(buffer);
}
}
/**
* Copies up to <code>length</code> bytes from the given <code>InputStream</code> to the specified
* </code>OutputStream</code>, less if the end-of-file was reached before that.
* This method does *NOT* close any of the given streams.
*
* <p>Read and write operations use the specified buffer, making the use of a <code>BufferedInputStream</code>
* unnecessary. A <code>BufferedOutputStream</code> also isn't necessary, unless this method
* is called repeatedly with the same <code>OutputStream</code> and with potentially small <code>InputStream</code>
* (smaller than the buffer's size): in this case, providing a <code>BufferedOutputStream</code> will further
* improve performance by grouping calls to the underlying <code>OutputStream</code> write method.</p>
*
* <p>Copy progress can optionally be monitored by supplying a {@link com.mucommander.commons.io.CounterInputStream} and/or
* {@link com.mucommander.commons.io.CounterOutputStream}.</p>
*
* @param in the InputStream to read from
* @param out the OutputStream to write to
* @param buffer buffer to use for copying
* @param length number of bytes to copy from InputStream
* @return the number of bytes that were copied
* @throws FileTransferException if something went wrong while reading from or writing to one of the provided streams
*/
public static long copyStream(InputStream in, OutputStream out, byte[] buffer, long length) throws FileTransferException {
// Copies the InputStream's content to the OutputStream chunk by chunk
int nbRead;
long totalRead = 0;
while(length>0) {
try {
nbRead = in.read(buffer, 0, (int)Math.min(buffer.length, length)); // the result of min will be int
}
catch(IOException e) {
throw new FileTransferException(FileTransferError.READING_SOURCE);
}
if(nbRead==-1)
break;
try {
out.write(buffer, 0, nbRead);
}
catch(IOException e) {
throw new FileTransferException(FileTransferError.WRITING_DESTINATION, totalRead);
}
length -= nbRead;
totalRead += nbRead;
}
return totalRead;
}
/**
* This method is a shorthand for {@link #transcode(java.io.InputStream, String, java.io.OutputStream, String, int)}
* called with a {@link BufferPool#getDefaultBufferSize() default buffer size}.
*
* @param in the InputStream to read from
* @param inCharset the source charset
* @param out the OutputStream to write to
* @param outCharset the destination charset
* @return the number of bytes that were transcoded
* @throws FileTransferException if something went wrong while reading from or writing to one of the provided streams
* @throws UnsupportedEncodingException if any of the two charsets are not supported by the JVM
*/
public static long transcode(InputStream in, String inCharset, OutputStream out, String outCharset) throws FileTransferException, UnsupportedEncodingException {
return transcode(in, inCharset, out, outCharset, BufferPool.getDefaultBufferSize());
}
/**
* Converts a stream from a charset to another, copying the contents of the given <code>InputStream</code> to the
* <code>OutputStream</code>. A {@link java.io.UnsupportedEncodingException} is thrown if any of the two charsets
* are not supported by the JVM.
*
* <p>Apart from the transcoding part, this method operates exactly like {@link #copyStream(java.io.InputStream, java.io.OutputStream, int)}.
* In particular, none of the given streams are closed.</p>
*
* @param in the InputStream to read the data from
* @param inCharset the source charset
* @param out the OutputStream to write to
* @param outCharset the destination charset
* @param bufferSize size of the buffer to use, in bytes
* @return the number of bytes that were transcoded
* @throws FileTransferException if something went wrong while reading from or writing to one of the provided streams
* @throws UnsupportedEncodingException if any of the two charsets are not supported by the JVM
* @see #copyStream(java.io.InputStream, java.io.OutputStream, int)
* @see java.nio.charset.Charset#isSupported(String)
*/
public static long transcode(InputStream in, String inCharset, OutputStream out, String outCharset, int bufferSize) throws FileTransferException, UnsupportedEncodingException {
InputStreamReader isr = new InputStreamReader(in, inCharset);
OutputStreamWriter osw = new OutputStreamWriter(out, outCharset);
// Use BufferPool to reuse any available buffer of the same size
char buffer[] = BufferPool.getCharArray(bufferSize);
try {
// Copies the InputStreamReader's content to the OutputStreamWriter chunk by chunk
int nbRead;
long totalRead = 0;
while(true) {
try {
nbRead = isr.read(buffer, 0, buffer.length);
}
catch(IOException e) {
throw new FileTransferException(FileTransferError.READING_SOURCE);
}
if(nbRead==-1)
break;
try {
osw.write(buffer, 0, nbRead);
// Let's not forget to flush as the writer will *not* be closed (to avoid closing the OutputStream)
osw.flush();
}
catch(IOException e) {
throw new FileTransferException(FileTransferError.WRITING_DESTINATION);
}
totalRead += nbRead;
}
return totalRead;
}
finally {
// Make the buffer available for further use
BufferPool.releaseCharArray(buffer);
}
}
/**
* This method is a shorthand for {@link #fillWithConstant(java.io.OutputStream, byte, long, int)} called with a
* {@link BufferPool#getDefaultBufferSize default buffer size}.
*
* @param out the OutputStream to write to
* @param value the byte constant to write len times
* @param len number of bytes to write
* @throws java.io.IOException if an error occurred while writing
*/
public static void fillWithConstant(OutputStream out, byte value, long len) throws IOException {
fillWithConstant(out, value, len, BufferPool.getDefaultBufferSize());
}
/**
* Writes the specified byte constant <code>len</code> times to the given <code>OutputStream</code>.
* This method does *NOT* close the stream when it is finished.
*
* @param out the OutputStream to write to
* @param value the byte constant to write len times
* @param len number of bytes to write
* @param bufferSize size of the buffer to use, in bytes
* @throws java.io.IOException if an error occurred while writing
*/
public static void fillWithConstant(OutputStream out, byte value, long len, int bufferSize) throws IOException {
// Use BufferPool to avoid excessive memory allocation and garbage collection
byte buffer[] = BufferPool.getByteArray(bufferSize);
// Fill the buffer with the constant byte value, not necessary if the value is zero
if(value!=0) {
for(int i=0; i<bufferSize; i++)
buffer[i] = value;
}
try {
long remaining = len;
int nbWrite;
while(remaining>0) {
nbWrite = (int)(remaining>bufferSize?bufferSize:remaining);
out.write(buffer, 0, nbWrite);
remaining -= nbWrite;
}
}
finally {
BufferPool.releaseByteArray(buffer);
}
}
/**
* This method is a shorthand for {@link #copyChunk(RandomAccessInputStream, RandomAccessOutputStream, long, long, long, int)}
* called with a {@link BufferPool#getDefaultBufferSize default buffer size}.
*
* @param rais the source stream
* @param raos the destination stream
* @param srcOffset offset to the beginning of the chunk in the source stream
* @param destOffset offset to the beginning of the chunk in the destination stream
* @param length number of bytes to copy
* @throws java.io.IOException if an error occurred while copying data
*/
public static void copyChunk(RandomAccessInputStream rais, RandomAccessOutputStream raos, long srcOffset, long destOffset, long length) throws IOException {
copyChunk(rais, raos, srcOffset, destOffset, length, BufferPool.getDefaultBufferSize());
}
/**
* Copies a chunk of data from the given {@link com.mucommander.commons.io.RandomAccessInputStream} to the specified
* {@link com.mucommander.commons.io.RandomAccessOutputStream}.
*
* @param rais the source stream
* @param raos the destination stream
* @param srcOffset offset to the beginning of the chunk in the source stream
* @param destOffset offset to the beginning of the chunk in the destination stream
* @param length number of bytes to copy
* @param bufferSize size of the buffer to use, in bytes
* @throws java.io.IOException if an error occurred while copying data
*/
public static void copyChunk(RandomAccessInputStream rais, RandomAccessOutputStream raos, long srcOffset, long destOffset, long length, int bufferSize) throws IOException {
rais.seek(srcOffset);
raos.seek(destOffset);
// Use BufferPool to avoid excessive memory allocation and garbage collection
byte buffer[] = BufferPool.getByteArray(bufferSize);
try {
long remaining = length;
int nbBytes;
while(remaining>0) {
nbBytes = (int)(remaining<bufferSize?remaining:bufferSize);
rais.readFully(buffer, 0, nbBytes);
raos.write(buffer, 0, nbBytes);
remaining -= nbBytes;
}
}
finally {
BufferPool.releaseByteArray(buffer);
}
}
/**
* This method is a shorthand for {@link #readFully(java.io.InputStream, byte[], int, int)}.
*
* @param in the InputStream to read from
* @param b the buffer into which the stream data is copied
* @return the same byte array that was passed, returned only for convience
* @throws java.io.EOFException if EOF is reached before all bytes have been read
* @throws IOException if an I/O error occurs
*/
public static byte[] readFully(InputStream in, byte b[]) throws EOFException, IOException {
return readFully(in, b, 0, b.length);
}
/**
* Reads exactly <code>len</code> bytes from the <code>InputStream</code> and copies them into the byte array,
* starting at position <code>off</code>.
*
* <p>This method calls the <code>read()</code> method of the given stream until the requested number of bytes have
* been skipped, or throws an {@link EOFException} if the end of file has been reached prematurely.</p>
*
* @param in the InputStream to read from
* @param b the buffer into which the stream data is copied
* @param off specifies where the copy should start in the buffer
* @param len the number of bytes to read
* @return the same byte array that was passed, returned only for convience
* @throws java.io.EOFException if EOF is reached before all bytes have been read
* @throws IOException if an I/O error occurs
*/
public static byte[] readFully(InputStream in, byte b[], int off, int len) throws EOFException, IOException {
if(len>0) {
int totalRead = 0;
do {
int nbRead = in.read(b, off + totalRead, len - totalRead);
if (nbRead < 0)
throw new EOFException();
totalRead += nbRead;
}
while (totalRead < len);
}
return b;
}
/**
* Skips exactly <code>n</code>bytes from the given InputStream.
*
* <p>This method calls the <code>skip()</code> method of the given stream until the requested number of bytes have
* been skipped, or throws an {@link EOFException} if the end of file has been reached prematurely.</p>
*
* @param in the InputStream to skip bytes from
* @param n the number of bytes to skip
* @throws java.io.EOFException if the EOF is reached before all bytes have been skipped
* @throws java.io.IOException if an I/O error occurs
*/
public static void skipFully(InputStream in, long n) throws IOException {
if(n<=0)
return;
do {
long nbSkipped = in.skip(n);
if(nbSkipped<0)
throw new EOFException();
n -= nbSkipped;
} while(n>0);
}
/**
* This method is a shorthand for {@link #readUpTo(java.io.InputStream, byte[], int, int) readUpTo(in, b, 0, b.length)}.
*
* @param in the InputStream to read from
* @param b the buffer into which the stream data is copied
* @return the number of bytes that have been read, can be less than len if EOF has been reached prematurely
* @throws IOException if an I/O error occurs
*/
public static int readUpTo(InputStream in, byte b[]) throws IOException {
return readUpTo(in, b, 0, b.length);
}
/**
* Reads up to <code>len</code> bytes from the <code>InputStream</code> and copies them into the byte array,
* starting at position <code>off</code>.
*
* <p>This method differs from {@link #readFully(java.io.InputStream, byte[], int, int)} in that it does not throw
* a <code>java.io.EOFException</code> if the end of stream is reached before all bytes have been read. In that
* case (and in that case only), the number of bytes returned by this method will be lower than <code>len</code>.
* </p>
*
* @param in the InputStream to read from
* @param b the buffer into which the stream data is copied
* @param off specifies where the copy should start in the buffer
* @param len the number of bytes to read
* @return the number of bytes that have been read, can be less than len if EOF has been reached prematurely
* @throws IOException if an I/O error occurs
*/
public static int readUpTo(InputStream in, byte b[], int off, int len) throws IOException {
int totalRead = 0;
if(len>0) {
do {
int nbRead = in.read(b, off + totalRead, len - totalRead);
if (nbRead < 0)
break;
totalRead += nbRead;
}
while (totalRead < len);
}
return totalRead;
}
/**
* This method is a shorthand for {@link #readUntilEOF(java.io.InputStream, int)} called with a
* {@link BufferPool#getDefaultBufferSize default buffer size}.
*
* @param in the InputStream to read
* @throws IOException if an I/O error occurs
*/
public static void readUntilEOF(InputStream in) throws IOException {
readUntilEOF(in, BufferPool.getDefaultBufferSize());
}
/**
* This method reads the given InputStream until the End Of File is reached, discarding all the data that is read
* in the process. It is noteworthy that this method does <b>not</b> close the stream.
*
* @param in the InputStream to read
* @param bufferSize size of the read buffer
* @throws IOException if an I/O error occurs
*/
public static void readUntilEOF(InputStream in, int bufferSize) throws IOException {
// Use BufferPool to avoid excessive memory allocation and garbage collection
byte buffer[] = BufferPool.getByteArray(bufferSize);
try {
int nbRead;
while(true) {
nbRead = in.read(buffer, 0, buffer.length);
if(nbRead==-1)
break;
}
}
finally {
BufferPool.releaseByteArray(buffer);
}
}
}