Showing posts with label java7. Show all posts
Showing posts with label java7. Show all posts

2014-03-17

Working with the Java 7 file API: recursive copy and deletion

Introduction

In one of my previous posts, I compared the file API in Java 6 and the new file API in Java 7.

You will have noticed, if you have been curious enough to read the Javadoc an tried it, that there are still two convenience methods missing from the new API (and they were not in the old API either): recursive copy or deletion of a directory.

In this post, I will show an implementation of both. The basis for both of these is to use the Files.walkFileTree() method. Copying and deleting is therefore "only" a matter of implementing the FileVisitor interface.

There are limitations to both; refer to the end of this post for more details.

Recursive copy

Here is the code of a FileVisitor for recursive copying:

public final class CopyFileVisitor
    implements FileVisitor<Path>
{
    private final Path srcdir;
    private final Path dstdir;

    public CopyFileVisitor(final Path srcdir, final Path dstdir)
    {
        this.srcdir = srcdir.toAbsolutePath();
        this.dstdir = dstdir.toAbsolutePath();
    }

    @Override
    public FileVisitResult preVisitDirectory(final Path dir,
        final BasicFileAttributes attrs)
        throws IOException
    {
        Files.createDirectories(toDestination(dir));
        return FileVisitResult.CONTINUE;
    }

    @Override
    public FileVisitResult visitFile(final Path file,
        final BasicFileAttributes attrs)
        throws IOException
    {
        Files.copy(file, toDestination(file));
        return FileVisitResult.CONTINUE;
    }

    @Override
    public FileVisitResult visitFileFailed(final Path file,
        final IOException exc)
        throws IOException
    {
        throw exc;
    }

    @Override
    public FileVisitResult postVisitDirectory(final Path dir,
        final IOException exc)
        throws IOException
    {
        if (exc != null)
            throw exc;
        return FileVisitResult.CONTINUE;
    }

    private Path toDestination(final Path victim)
    {
        final Path tmp = victim.toAbsolutePath();
        final Path rel = srcdir.relativize(tmp);
        return dstdir.resolve(rel.toString());
    }
}

In order to use it, you would then do:

final Path srcdir = Paths.get("/the/source/dir");
final Path dstdir = Paths.get("/the/destination/dir");
Files.walkFileTree(srcdir, new CopyFileVisitor(srcdir, dstdir);

Recursive deletion

Here is the code of a FileVisitor for recursive deletion:

public final class DeletionFileVisitor
    implements FileVisitor<Path>
{
    @Override
    public FileVisitResult preVisitDirectory(final Path dir,
        final BasicFileAttributes attrs)
        throws IOException
    {
        return FileVisitResult.CONTINUE;
    }

    @Override
    public FileVisitResult visitFile(final Path file,
        final BasicFileAttributes attrs)
        throws IOException
    {
        Files.delete(file);
        return FileVisitResult.CONTINUE;
    }

    @Override
    public FileVisitResult visitFileFailed(final Path file,
        final IOException exc)
        throws IOException
    {
        throw exc;
    }

    @Override
    public FileVisitResult postVisitDirectory(final Path dir,
        final IOException exc)
        throws IOException
    {
        if (exc != null)
            throw exc;
        Files.delete(dir);
        return FileVisitResult.CONTINUE;
    }
}

To use it:

final Path victim = Paths.get("/directory/to/delete");

Files.walkFileTree(victim, new DeleteFileVisitor());

Limitations

The implementation of recursive copy is limited to paths on the same filesystem. Indeed, you cannot .resolve() a path issued from another filesystem... More on that in another post.

The recursive deletion will stop at the first element (file or directory) which fails to be deleted.

That's all folks...

2014-03-14

Working with files: Java 6 versus Java 7

Introduction

When searching for examples to manipulate files on the net, most examples found use either of:

  • Java 6's old file API,
  • a utility library (Apache commons mostly).

But Java 8 is out now, and it seems people still haven't gotten to grasps with Java 7's new file API... This post aims to do two things:

  • describe some of the advantages of the new API;
  • give examples of Java 6 code and the equivalent (better!) Java 7 code.

Hopefully, after reading this, you will ditch the old API! Which you should, really.

Part 1: advantages of the new API

Meaningful exceptions

Oh boy is that missing from the old API.

Basically, any filesystem-level error (permission denied, file does not exist etc) with the old API would throw FileNotFoundException. Not informative at all. Making sense out of this exception basically requires that you dig into the error message.

With the new API, that changes: you have FileSystemException. And its subclasses have equally meaningful names: NoSuchFileException, AccessDeniedException, NotDirectoryException etc.

Common filesystem operations now throw exceptions

A very common bug with Java 6 code (and which external libraries have struggled to work around) is to not check for return values of many methods on File objects. Examples of such methods are file.delete(), file.createNewFile(), file.move(), file.mkdirs() etc.

Not anymore. For instance, Files.delete() throws an exception on failure; and the exception thrown will be meaningful! You will know whether, for instance, you attempted to delete a non empty directory.

Useful shortcut methods

Just an example: Files.copy()! Several of these shortcut methods will be shown in the examples below.

Less room for errors when dealing with text data

Have you ever been hit by code using a Reader or a Writer without specifying the encoding to use when reading/writing to files?

Well, the good news is that Files methods opening readers or writers (resp. Files.newBufferedReader() and Files.newBufferedWriter()) require you to specify the encoding!

This is also the case of the Files.readAllLines() method.

Advanced usage: filesystem implementations

Here, a filesystem does not mean only an on-disk format of storing files; provided you want to implement it, or find an existing implementation, it can be anything you like: an FTP server, a CIFS filesystem, etc.

Or even a ZIP file (therefore, jars, wars, ears etc as well); in fact, Oracle provides a filesystem implementation for these.

Part 2: sample usages

Abstract path names

In Java 6, it was File. In Java 7, you use Path.

For backwards compatibility reasons, you can convert one to the other and vice versa: file.toPath(), path.toFile().

To create a path in Java 7, use Paths.get().

Operations on abstract paths

The table below lists operations on File objects, and their equivalent using Java 7's Files class. There are fundamental differences between Java 6 and Java 7 here:

  • these operations in Java 7 require that you create the Path object;
  • as mentioned above, Java 7 operations will throw a (meaningful!) exception on failure; Java 6 operations return a boolean which should be checked for, but which most people forget to check for...
  • For all creation operations using Java 7, you can specify file attributes; those are filesystem dependent, and specifying an attribute which is not supported by the filesystem implementation will throw an unchecked exception.
Java 6Java 7Differences
file.createNew() Files.createFile() See above
file.mkdir() Files.createDirectory() See above
file.mkdirs() Files.createDirectories() See above
file.exists() Files.exists() Java 7 supports symbolic links; you can therefore check whether the link itself exists by adding the LinkOption.NO_FOLLOW_LINKS option, regardless of whether the link target exists. On filesystems without symlink support, this option has no effect.
file.delete() Files.delete() Java 7 also has Files.deleteIfExists().
file.isFile(), file.isDirectory() Files.isRegularFile(), Files.isDirectory()
  • Here also, symlink support makes a difference. Symlinks are followed by default, if you do not want to follow them, specify the LinkOption.NO_FOLLOW_LINKS option.
  • Java 7 has also Files.isSymbolicLink().
file.renameTo() Files.move() Like Java 6, this method will fail to move a non empty directory if the target path is not on the same filesystem (the same FileStore).

Copying a file

In plain Java 6, you would have to do something like this:

public static void copyFile(final String from, final String to)
    throws IOException
{
    final byte[] buf = new byte[32768];
    final InputStream in = new FileInputStream(from);
    final OutputStream out = new FileOutputStream(to);
    int read;
    try {
        while ((read = in.read(buf)) != -1)
            out.write(buf, 0, read);
        out.flush();
    } finally {
        in.close();
        out.close();
    }        
}

But even this code is flawed, so if you are stuck with Java 6, do yourself a favour and use Guava, which has (since version 14.0) Closer:

public static void copyFile(final String from, final String to)
    throws IOException
{
    final Closer closer = Closer.create();
    final RandomAccessFile src, dst;
    final FileChannel in, out;

    try {
        src = Closer.register(new RandomAccessFile(from, "r");
        dst = Closer.register(new RandomAccessFile(to, "w");
        in = Closer.register(src.getChannel());
        out = Closer.register(dst.getChannel());
        in.tranfserTo(0L, in.size(), out);
    } finally {
        Closer.close();
    }
}

With Java 7, this becomes very, very simple:

public static void copyFile(final String from, final String to)
    throws IOException
{
    Files.copy(Paths.get(from), Paths.get(to));
}

Opening a BufferedWriter/OutputStream to a file

In this case, the code is not much shorter for Java 7; but you do get the benefit of better exceptions.

With Java 6:

// BufferedWriter
new BufferedWriter(new FileWriter(myFile, Charset.forName("UTF-8")));
// OutputStream
new FileOutputStream(myFile);

With Java 7:

 // BufferedWriter
Files.newBufferedWriter(myFile, StandardCharsets.UTF_8);
// OutputStream
Files.newOutputStream(myFile);

Note that only the most simple form of these methods is presented here. By adding options, you can specify whether you want to fail if the file does not exist, or create it only if it does not exist, or append to it. Many things in fact.

Listing all files in a directory

Java 6:

final File rootDir = new File(...);
for (final File file: rootDir.listFiles())
    // do something

Java 7:

final Path rootDir = Paths.get(...);
for (final Path file: Files.newDirectoryStream(rootDir))
    // do something

OK, this is not really shorter. However, the difference in behaviour alone speaks for itself:

  • if the path is not a directory, .listFiles() will happily return null; with Java 7, you get a meaningful exception (including the NotDirectoryException mentioned above);
  • as Java 7's method name says, it will be a stream; .listFiles() swallowed the whole list of files, making it impossible to use if you have a very large number of files.

To be continued...