Useful abstractions for I/O, part four

In part one of this series, I’ve introduced you to the Transformation interface, which applies or unapplies a function to a Source or Sink. In this posting, I’ll show you how to write a general-purpose composed Transformation.

The take-away-lesson to learn from part one was that a Transformation works like a small Lego brick: If you ever played with Lego bricks, then you know the simple bricks with just a few knobs are the most versatile because you can compose them in an almost unlimited fashion. The same principle applies to the Transformation interface: With only two methods, a Transformation is simple to implement and use, which makes it almost as versatile than a Lego brick with only two knobs.

To see how you can generally compose transformations into a new one, let’s revisit how I used two transformations in order to compress and encrypt some data. First, let’s assume the following:

Transformation compression = new Compression();
Transformation encryption = new PbeEncryption(password);

To apply a compression-then-encryption schema, I would write the following code (assuming Java 7):

Sink sink = ... // some Sink
Sink composition = compression.apply(encryption.apply(sink));
try (OutputStream out = composition.output()) {
    ... // happily write some data
}

To do the inverse when reading data, I would write the following code:

Source source = ... // some Source
Source composition = compression.unapply(encryption.unapply(source));
try (InputStream in = composition.input()) {
    ... // happily read some data
}

Do you spot the pattern? In both cases, I manually composed two transformations (one for compression, one for encryption) into a larger one before doing I/O. This pattern generally applies to whatever two transformations I would want to compose, so let’s write some code to encapsulate it:

public class Transformations {

    public static Transformation compose(
            final Transformation primary,
            final Transformation secondary) {
        return new Transformation() {
            @Override public Sink apply(Sink sink) {
                return primary.apply(secondary.apply(sink));
            }

            @Override public Source unapply(Source source) {
                return primary.unapply(secondary.unapply(source));
            }
        };
    }

    private Transformations() { }
}

Now I can compose the two transformations as follows:

Transformation composition = Transformations.compose(compression, encryption);

Given this composition, I can shorten the previous example for writing compressed-then-encrypted data to:

Sink sink = ... // some Sink
try (OutputStream out = composition.apply(sink).output()) {
    ... // happily write some data
}

… and similar for reading the data back:

Source source = ... // some Source
try (InputStream in = composition.unapply(source).input()) {
    ... // happily read some data
}

The beauty of this is that the composed transformation nicely encapsulates all configuration required for the compression and encryption algorithms into a single, immutable object which you can apply or unapply as often as you need to.

Enjoy!