Monitoring Progress with Size Input Streams

Progress bars provide visual feedback on the progress of lengthy tasks, such as reading a stream of bytes stored in a file. In some cases, Java progress monitors cannot be used because the number of bytes that remain to be read is not available from the InputStream object that the bytes are read from. This article introduces the SizeInputStream class that enables reading progress to be monitored when the number of bytes to be read can be determined from sources other than an InputStream object, such as from a URLConnection object.

1. Introduction

Interactive applications should provide the user with feedback on the progress of lengthy tasks with a progress bar. As the task progresses, the length of the bar increases from left to right to indicate how much of the task has been completed and to enable users to estimate how much longer it will take to complete:

Progress bars provide visual feedback on the progress of lengthy tasks

A typical example of the need for a progress bar is when reading a stream of bytes, such as the contents of a large file. The Java InputStream class provides a generic interface for reading bytes from a variety of sources. Classes for reading bytes from files, pipes, and filters, for example, are implemented by sub-classing InputStream. As well as providing methods to read one or more bytes from a stream, the InputStream class also provides an int available()method that returns the number of bytes that remain to be read from the stream.

Java also provides a ProgressMonitorInputStream class that reads bytes from an InputStream object and pops up a progress bar if reading the stream will take more than a specified minimum amount of time (Geary 1999, p. 281). The ProgressMonitorInputStream class uses the available() method of class InputStream to determine how many bytes remain to be read from the stream. The length of the bar is calculated by subtracting the initial number of bytes that were available to read from the stream from the number of bytes that are left to read.

Some input stream classes, such as the ZipInputStream class for reading bytes from files compressed in the Zip format, cannot know in advance how many bytes are available to read (see for example Java Bug Parade 4028605 and 4186776). In such cases, the available() method will always return 1 which makes it impossible to monitor the progress of reading from such streams.

2. Size Input Streams

In some cases client code can determine the number of bytes to be read from an input stream other than by calling the available() method. For example, if the bytes to be read can be accessed with a URL, the number of bytes can be determined by calling the getContentLength() method of a URLConnection object. The following Java code determines the number of bytes in a resource that is accessed with a URL.

URL url = new URL("file:/c:/a_compressed_file.zip");
URLConnection connection = url.openConnection();
int size = connection.getContentLength();

When the number of bytes to be read can be determined, the SizeInputStream class can be used to overcome the inability of an InputStream object’s available() method to return a useful byte count. The SizeInputStream class wraps an InputStream object and the number of bytes that can be read from it, as determined, for example, by using a URLConnection object. The available() method of a SizeInputStream object always returns the correct number of bytes that remain to be read. By monitoring the progress of reading from a SizeInputStream object, a ProgressMonitorInputStream object can monitor the progress of reading from an InputStream object, even if it’s available() method does not return a useful byte count.

A SizeInputStream object is constructed with an InputStream to read bytes from, and the number of bytes that are available to read. The three methods to read bytes from a SizeInputStream object, read(), read(byte[] b), and read(byte[] b, int off, int len), read the requested number of bytes from the enclosed InputStream and record the number of bytes read. The available() returns the number of bytes left to read from the InputStream by subtracting the number of bytes that have been read from the number of bytes passed to the constructor.

InputStream inputStream = connection.getInputStream();
SizeInputStream sizeInputStream = new SizeInputStream(inputStream, size);

The following Java code creates a ProgressMonitorInputStream with a SizeInputStream object, which provides the stream to read from and determines the number of bytes to read.

ProgressMonitorInputStream progressInput = new ProgressMonitorInputStream(
    component,
    message,
    sizeInputStream
);

The implementation of the SizeInputStream class is shown below.

class SizeInputStream extends InputStream {
    // The InputStream to read bytes from
    private InputStream in = null;

    // The number of bytes that can be read from the InputStream
    private int size = 0;

    // The number of bytes that have been read from the InputStream
    private int bytesRead = 0;

    public SizeInputStream(InputStream in, int size) {
        this.in = in;
        this.size = size;
    }

    public int available() {
        return (size - bytesRead);
    }

    public int read() throws IOException {
        int b = in.read();
        if (b != -1)
        {
            bytesRead++;
        }
        return b;
    }

    public int read(byte[] b) throws IOException {
        int read = in.read(b);
        bytesRead += read;
        return read;
    }

    public int read(byte[] b, int off, int len) throws IOException {
        int read = in.read(b, off, len);
        bytesRead += read;
        return read;
    }
}

References