A fun experiment revisiting classic Java I/O API.
- Fluent builder for Java standard InputStream, OutputStream, Reader and Writer and Filter classes
- Provides File, ByteArray, String, Socket, Pipe, Buffered, Zip, Console and generic stream wrapper resources
- Supports Base64, GZip and Cipher filters
- Retains default Java behavior and parameters
- Composed objects are unambiguously closed as one
- Composed objects expose underlying resource for retrieval of results / follow up operations
try (PrintWriterOf<File> fileWriter = IO.file("myfile.gz").gzip().printWriter()) {
File theFile = fileWriter.get();
fileWriter.write("Hello from file " + theFile.getName());
}
"It's the little things"
Part of the original JDK 1.0, Java's original blocking I/O API celebrated it's 21st birthday on January 23, 2017. The venerable Java "streams" classes are now of legal drinking age in most of the places they're allowed to go! And because of Java's insistence on backward compatibility, everything is still as it was back then. Everything but the rest of the world, that is. Ahem. Who feels like programming like it's 1996?
Mind you, there isn't much that is broken with the streams functionality. Sequential I/O is not something that changes much, if ever. It's just that the classes have become somewhat unwieldy when used alongside contemporary Java libraries. Let's give them a little makeover, shall we?
If you are using Maven, start by adding this snippet to your pom.xml
<dependency>
<groupId>ca.rbon</groupId>
<artifactId>iostream</artifactId>
<version>0.9.1</version>
</dependency>
Sadly I do no have gradle at the ready, but I'm sure you smart foxes will know where to
insert what I believe to be ca.rbon:iostream:0.9.1
.
Because it uses default interface methods, this library requires Java 1.8 (and nothing but).
First, IoStream
wraps the streams and their charset aware sibblings in a more palatable
fluent-builder (or Factory Method, for you pattern freaks) so you don't have to new
anything
when you need to do some sweet blocking I/O.
import ca.rbon.iostream.IO;
Then start by typing IO.
and autocomplete-away!
Because streams and readers/writers are often paired together,
the fluent builder guides you from the selection of the underlying resource (File
, byte[]
, Socket
...)
to the way of accessing it (BufferedStream
, ZipStream
, Writer
...).
PrintWriter writer = IO.file("yesss.txt").printWriter();
You will notice that printWriter()
above actually returns something called PrintWriterOf<File>
.
This is a subclass of the standard java.io.PrintWriter
that can be safely use as such.
All IoStream
final methods return *Of<File>
classes extending normal java.io
classes.
Their added functionality is explained below (hint: checkout their getInner()
method.).
If a resource only implements streams (like socket()
), and you require Writer
or Reader
char-oriented access, the builder will transparently insert an OutputStreamWriter
or InputStreamReader
adapter for you.
BufferedWriter writer = IO.socket("spamaway.net", 25).bufferedWriter();
Obviously, IoStream
goes hand in hand with the try-with syntax introduced in JDK 7.
try (Writer w = IO.file("mouha.txt").printWriter()) {
w.append("haha");
}
It is often required to access the resource after all streams are closed in order to obtain the final operation result. This is a pattern especially common with auto-instantiated resources such as byte arrays out streams, string writers or temp files.
IoStream
makes the job easier by allowing access to the resulting resource straight from the outmost object.
This is done magically with IoStream's *Of<T>
classes, which subclass regular java.io
classes.
Because we are modern, sane people, we can ignore this detail by using the var
keyword from JDK 10+.
Then all you have to do is call the getInner()
method to retrieve the inner resource you just built.
var writer = IO.bytes().printWriter();
// ...
writer.close();
byte[] myPrecious = writer.getInner();
By comparison, classic JDK code forces you to juggle with the inner and outer streams:
ByteArrayOutputStream innerStream = new ByteArrayOutputStream();
Writer outerStream = new PrintWriter(innerStream);
// ...
outerStream.close();
byte[] myPrecious = innerStream.toByteArray();
IoStream
is an excellent companion to Apache Commons IO's
IOUtils class.
There is little overlap between the libraries.
IoStream
helps to build the streams while IOUtils
takes care of the operations, such as copy()
:
try (var in = IO.file("A").reader(); var out = IO.file("B").writer()) {
IOUtils.copy(in, out);
}
try (var pw = IO.file("numbers.bin").inputStream()) {
pw.intStream().sum();
}
IO.bytes().outputStream();
byte[] bytes = IO.bytes().dataOutputStream().getInner();
IO.bytes(new byte[] { 0, 1, 2 }).objectInputStream();
FileOutputStream out = IO.file("noooes.txt").outputStream();
try (var myFile = IO.file("mouha.txt").printWriter()) {
myFile.write("haha");
}
IO.file("doum.zip").zipInputStream("UTF-8");
IO.file("dam.txt", true).bufferedWriter();
var tmpout = IO.tempFile().dataOutputStream();
tmpout.write(42);
String tmpFilename = tmpout.getInner().getAbsolutePath();
IO.string("agaga gogo").bufferedReader();
IO.string("agaga gogo").reader();
String str = IO.string().bufferedWriter().getInner();
IO.socket("gloogloo.com", 80).bufferedOutputStream();
InputStream smtpHoneypot = IO.socket("localhost", 25).inputStream();
IO.socket("gloogloo.com", 80).bufferedOutputStream();
InputStream smtpHoneypot = IO.socket("localhost", 25).inputStream();
try (var pw = IO.random().inputStream()) {
// add 5 random numbers
pw.intStream().limit(5).sum();
}
InputStream providedInput = new ByteArrayInputStream(new byte[7]);
IO.stream(providedInput).gzipInputStream();
OutputStream providedOutput = new ByteArrayOutputStream();
IO.stream(providedOutput).printWriter(256);
// this example is pretty extreme
try (var pw = IO.file("myfile.txt").base64().gzip(55).printWriter("UTF-16", 256, true)) {
File myFileTxt = pw.getInner();
pw.write("Hello from file " + myFileTxt.getName());
}
You mean this? Sure, but it's bigger and comes with its own classes and superseded the original Java classes. Plus, I could not wrap my head around it in 10 seconds, which is all it should take for such a thing. So I spent 10 hours writing this lib instead.
Hey, Java can be a pain. We could push it further with compiler @Annotations or dynamically generated bytecode, but then we'd have a different set of problems. Why don't you try a different language?
The goal is to support just Java stream classes (Sockets, Pipes) with their original semantics. No additional transitive deps. No fixing of the original sins. Some parts are still missing, but once it's done, it's done. You'll know because it'll be version 1.0.
IoStream enabled-code
public byte[] usingIoStream() throws IOException {
var writer = IO.bytes().printWriter();
writer.write("doodoo");
writer.close();
return writer.getInner();
}
Classic java streams
public byte[] classicStreams() throws IOException {
ByteArrayOutputStream byteOutputStream = new ByteArrayOutputStream();
PrintWriter writer = new PrintWriter(byteOutputStream);
writer.write("doodoo");
writer.close();
byteOutputStream.close();
return byteOutputStream.toByteArray();
}
IoStream enabled-code
public byte[] fluent() throws IOException {
// create and combine both objects
var writer = IO.bytes().printWriter();
// write the string
writer.write("doodoo");
// close the writer and the inner stream at once
writer.close();
// extract result
return writer.getInner();
}
Classic java streams
public byte[] classic() throws IOException {
// explicitly create inner stream to extract result
ByteArrayOutputStream byteOutputStream = new ByteArrayOutputStream();
// create outer writer
PrintWriter writer = new PrintWriter(byteOutputStream);
// write a string
writer.write("doodoo");
// close the writer
writer.close();
// dont forget to close inner stream too
byteOutputStream.close();
// extract result from inner stream
return byteOutputStream.toByteArray();
}
IoStream enabled-code
try (var writer = IO.bytes().printWriter()) {
writer.write("doodoo");
return writer.getInner();
}
Classic java streams
try (ByteArrayOutputStream byteOutputStream = new ByteArrayOutputStream();
PrintWriter writer = new PrintWriter(byteOutputStream)) {
writer.write("doodoo");
return byteOutputStream.toByteArray();
}