1/07/2009

Fun with Fluent Interfaces and Java

I've written about fluent interfaces before, but I thought I'd share this one I use quite a bit. You never know how much you like something until it's gone. I never thought I really liked Java's InputStream and OutputStream that much until I had to do a lot of streaming work in Actionscript. They have no abstraction for doing stream manipulations. But, let's be honest Java's io first settler's haven't changed much since their introduction. In fact their interfaces have not changed one bit. Sad really because they are so ubiquitous. I find I'm always copying data from one stream the other, dealing with IOExceptions, remembering to close streams, etc. And I got really tired of doing it over and over. What started out as static methods has evolved into a very simple object called ExtendInputStream. Extended as in extending the interface to add more rich functionality rather than the use of inheritance.

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;
}
}