The greatest single thing about InputStream and OutputStream is that it's the quintessential example of a decorator. Decorator is one of the foundational software patterns. What I love about decorators is the ability to encapsulate related classes behind a new interface while still retaining interoperability with other decorators.
ExtendedInputStream is a InputStream so it can interact just as plain old InputStream would, but it adds methods like copy, closeQuietly, copyAndClose, and integration with File objects which has always been a pet peeve of mine with InputStream. Let's look at some examples:
Here is copying a file to a directory.
new ExtendedInputStream( someFile ).copyAndClose( dir );
One liner! It's amazing how File object doesn't have these methods already implemented, but then again this approach is much more flexible because we can copy this file to any OutputStream. Here is copying a set of files to a zip.
ZipOutputStream zout = new ZipOutputStream( out );
for( File myfile : files ) {
ZipEntry entry = new ZipEntry( myfile.getName() );
zout.putNextEntry( entry );
new ExtendedInputStream( myFile ).copyAndClose( zout );
}
Five lines of code! Not bad given that 4 of those lines is just to work with ZipOutputStream. Notice how I'm not saving the reference to the ExtendedInputStream here. The copyAndClose() method copies the contents of the file to the OutputStream and closes the InputStream. Closing the OutputStream is your responsibility.
And the more general case of copying an plain old InputStream to any OutputStream.
URLConnection remote = new URL("...").openConnection();
new ExtendedInputStream( new URL("...").openStream() ).copyAndClose( remote.openOutputStream() );
Here is a more advanced version. Say we want to pull down a URL and save it to a file on our local filesystem.
File someDirectory = ...;
new ExtendedInputStream( new URL("...").openStream() ).name( "SavedUrl.txt" ).copyAndClose( someDirectory );
Here we use the optional method name() to set the name of the stream so when we save something to a directory it will use this name as the filename. You could have just as easily done new File( someDirectory, "SaveUrl.txt" ), but it's not always convenient.
You can use a similar pattern for increasing the buffer size used when copying as well.
new ExtendedInputStream( new URL("...").openStream() ).bufferSize( 8096 * 2 ).copyAndClose( someDir );
While I have enjoyed writing this simple class I think I've enjoyed using it more so. I really can't start a new Java project without it now. It's a lot of fun to use. I'd be interested in hearing other features people might want to see added.
package com.wrongnotes.util;
import java.io.*;
public class ExtendedInputStream extends InputStream {
private InputStream delegate;
private String name;
private int bufferSize = 8096;
public ExtendedInputStream( InputStream stream ) {
this( "no_name_file", stream );
}
public ExtendedInputStream(String name, InputStream delegate) {
this.name = name;
this.delegate = delegate;
}
public ExtendedInputStream( File src ) throws FileNotFoundException {
name = src.getName();
delegate = new BufferedInputStream( new FileInputStream( src ) );
}
public int read() throws IOException {
return delegate.read();
}
public int read(byte b[]) throws IOException {
return delegate.read(b);
}
public int read(byte b[], int off, int len) throws IOException {
return delegate.read(b,off,len);
}
public long skip(long n) throws IOException {
return delegate.skip(n);
}
public int available() throws IOException {
return delegate.available();
}
public void close() throws IOException {
delegate.close();
}
public synchronized void mark(int readlimit) {
delegate.mark(readlimit);
}
public synchronized void reset() throws IOException {
delegate.reset();
}
public long copy(File dest) throws IOException {
if( dest.isDirectory() ) {
dest = new File( dest, name );
}
FileOutputStream out = new FileOutputStream(dest);
try {
return copy( out );
} finally {
out.close();
}
}
public long copy(OutputStream out) throws IOException {
long total = 0;
byte[] buffer = new byte[bufferSize];
int len;
while ((len = this.read(buffer)) >= 0) {
out.write(buffer, 0, len);
total += len;
}
out.flush();
return total;
}
public void closeQuietly() {
try {
close();
} catch( IOException ioe ) {
// ignore
}
}
public void copyAndClose( File file ) throws IOException {
try {
copy( file );
} finally {
close();
}
}
public void copyAndClose(OutputStream out) throws IOException {
try {
copy( out );
} finally {
close();
}
}
public ExtendedInputStream bufferSize( int size ) {
bufferSize = size;
return this;
}
public ExtendedInputStream name( String newName ) {
name = newName;
return this;
}
}
2 comments:
That's really a nice one. I think it is not an ExtendedInputStream and I ask myself if it should be a Stream at all, when looking at your examples. Not implementing InputStream directly would allow to extend the fluent interface much more. It would name it something like "IOHandler", because it is more than a Stream.
By the way, I first thought copyAndClose would close the OutputStream parameter, not the ExtendedInputStream itself. This would be much nicer I think.
new IOHandler( new URL("...").openStream() ).copyToAndCloseTarget( new FooOutputStream() ).close();
or perhaps:
new IOHandler( new URL("...").openStream() ).copyTo( new FooOutputStream() ).closeOutput().closeInput();
new IOHandler( new URL("...").openStream() ).copyToAndCloseBoth( new FooOutputStream() );
I you introduce static factory-Methods one could use static imports with Java 5.
handle( new URL("...").openStream() ).copyToAndClose( new FooOutputStream() ).close();
Perhaps that's a nice addition to "Jakarta Commons IO"?
I see where you're going, and I think it could lead to something better so I'd like to keep talking it out.
When I started this I wanted to make it more "Fluent" in that you could close each stream very easily, but the problem I always ran into was exception handling. I think you want separate close methods for the input side as well as output side.
Something like what you wrote:
extendedStream.copy( output ).closeInput().closeOuput();
That way depending on what you're doing you could close either one or the other or both. But, what happens when closeInput() throws an IOException? Your output wouldn't get closed, or more likely copy throws and neither are closed.
To handle both closing input and output your methods start to multiply for each operation you create (say copy, copyCloseInput, copyCloseOutput, copyCloseBoth). Now if you introduce another operation it's 4 more methods.
I'm not too keen on it remembering the output because I might want to use this stream on more than one OutputStream. But, I like the idea of trying to deal with both sides more elegantly.
I'd be willing to make a submission to Commons IO if they're interested.
Thanks.
Post a Comment