Sunday, May 1, 2011

Starting with JDK7 NIO.2

Hurray, here it is starting withe the JDK7 NIO.2 API. Before trying the service watcher I wanted to start with the basics. My JDK is the ea-b140 version so a recent one. I was surprised to find samples based on slightly different API from the one I found in the version I downloaded. So this small kata became a real exploratory exercise.

As usual I started with TDD. My first shot was to try to create a simple file. Why not creating a directory where to play with my files ?
All starts in my test case with a canary test, checking that I did well in the setup (that's bad I did not use the JUNIT @Rule temporary file this time ;)).
    @Test
public void testDirectory_ShouldHaveBeenCreated() throws IOException {
assertThat(location, is(not(nullValue())));
final BasicFileAttributes attributes =
withProvider().readAttributes(location, BasicFileAttributes.class);
assertThat(attributes.isDirectory(), is(true));
}

The first assert checks that the directory location available during the upcoming test cases executions has been effectively created. Then using the new API, we check the BasicFileAttributes of the directory so to be sure it is really a directory.

Where does the provider come from ? From there:
    private FileSystemProvider withProvider() {
return fileSystem.provider();
}

where -quoting the javadoc -the file system object provides an interface to a file system and is the factory for objects to access files and other objects in the file system.

But in order to manipulate file system artifacts (creation, copy etc...), the file system instance is bound to a FileSystemProvider instance. A tool class Files is provided with static methods and mainly delegate to the FileSystemProvider instance. I have chosen to use one or the other,
depending on the situation.

A file system is obtained using a factory method in the generic FileSystems class. This is done
this way:
 fileSystem = FileSystems.getDefault();
In order to make the upper test comes to green (it needs to compile too), let's implement a setup
method:
        @Before
public void setup() throws IOException {
fileSystem = FileSystems.getDefault();
location = fileSystem.getPath(inUserHome(), forTests());
withProvider().createDirectory(location);
}
So as a rule of thumb, we use the filesystem instance to create a Path - the location - , the NIO.2 abstraction for a system path, not a file, but a more generic concept allowing to identify any
file system artifact.
We then use its bound provider to create effectively the directory.

Test is green.

Ok, then I want to create a file, and test it does exists. Using IntelliJ programming by intention facility, I wrote the following:
    @Test
public void resolve_WithTestingDirectory_ShouldHelpToCreateANewFile() throws IOException {
final Path newFile = location.resolve(newFileName());
assertThat(exists(newFile), is(not(true)));
try (final FileChannel toChannel = withProvider().newFileChannel(newFile, creationOptions())) {
write(toChannel, bufferThatRocks());
}
assertThat(exists(newFile), is(true));
}

private Set creationOptions() {
final Set options = new TreeSet<>();
options.add(StandardOpenOption.CREATE);
options.add(StandardOpenOption.WRITE);
return options;
}

private void write(final FileChannel channel, final ByteBuffer buf) throws IOException {
while (buf.hasRemaining()) {
channel.write(buf);
}
}

private ByteBuffer bufferThatRocks() {
final ByteBuffer buf = allocate(64);
buf.clear();
buf.put("Jdk7 rocks !!".getBytes());
buf.flip();
return buf;
}


Focusing on the test, I have chosen to use the resolve method of the Path class, to identify the up-to-be-created new file in the test location:
final Path newFile = location.resolve(newFileName());
Using the tools class Files, I invoke the exists method tho check that the file has not been created of course ! If you have heard of the coin project implementation in JDK 7, the following try scope
should not surprise you. In effect, to create the file I am going to open a file channel and write into its content. So to avoid code cluttering I will use the try-with-resource feature that will close the channel for me (compiler's my best friend).

But when you open a channel, you open it with a set of options, specifying your purpose. That's what I did! I specified I wanted to create the file and write in it:

private Set creationOptions() {
final Set options = new TreeSet<>();
options.add(StandardOpenOption.CREATE);
options.add(StandardOpenOption.WRITE);
return options;
}

As working on a standard Microsoft OS, I did not want to take risks and used StandardOpenOption enum constant for my options. The scope

 try (final FileChannel toChannel = withProvider().newFileChannel(newFile, creationOptions())){}

has been explained.


People who have been programming with NIO.1 will have recognized the standard code pattern for data writing into a file channel. I first create a "cool" byte buffer:
        private ByteBuffer bufferThatRocks() {
final ByteBuffer buf = allocate(64);
buf.clear();
buf.put("Jdk7 rocks !!".getBytes());
buf.flip();
return buf;
}
and then transfers the info into the channel:
    private void write(final FileChannel channel, final ByteBuffer buf) throws IOException {
while (buf.hasRemaining()) {
channel.write(buf);
}
}

Test green, cool! The file does exist.

What if I want to copy a file? Yet another test, very similar:
    @Test
public void copy_InTestingDirectory_ShouldCreateADuplicate() throws IOException {
final Path sourceFile = location.resolve(newFileName());
try (final FileChannel toChannel = withProvider().newFileChannel(sourceFile, creationOptions())){
write(toChannel, bufferThatRocks());
}
assertThat(exists(sourceFile), is(true));
final Path toFile = location.resolve(newFileName());
withProvider().copy(sourceFile, toFile);
assertThat(exists(toFile), is(true));
}

This time copy, is straight forward: I use the file system provider. You could have achieved the same with the copy method of the Files tool class.

Whats really striking, is the what I see when I gaze at my list of io import:
    import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
import java.nio.file.spi.FileSystemProvider;
import static java.nio.ByteBuffer.allocate;
import static java.nio.file.Files.exists;

There is only one reference to the java.io package. Everything was tested without referencing once the File object. Not difficult, smooth and nice. I hope it was helpful, although quite simple for most of you

Be seeing you !









0 comments:

Post a Comment