Java I/O and NIO.2

NIO.2 cookbook, Part 3

Advanced recipes for file copying, finding files, and watching directories with NIO.2.

Java I/O and NIO.2

Show More

The previous two posts in my NIO.2 cookbook series presented simple recipes for copying and moving files, deleting files and directories, working with paths and attributes, and performing various testing operations. This article ends this series by presenting a more advanced file-copying recipe as well as advanced recipes for finding files and watching directories.

Copying files, part 2

Q: Can you expand Part 1's file-copying application, which copies a file to another file, to also copy a file to a directory and a directory hierarchy to another hierarchy?

A: Listing 1 presents the source code to an application that accomplishes all three file-copy operations. This application relies on NIO.2's file-visiting feature to walk the file/directory tree.

Listing 1. Copy.java

import java.io.IOException;

import java.nio.file.Files;
import java.nio.file.FileVisitOption;
import java.nio.file.FileVisitResult;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.StandardCopyOption;

import java.nio.file.attribute.BasicFileAttributes;

import java.util.EnumSet;

public class Copy 
{
   public static class CopyDirTree extends SimpleFileVisitor<Path>
   {
      private Path fromPath;
      private Path toPath;

      private StandardCopyOption copyOption = 
         StandardCopyOption.REPLACE_EXISTING;

      CopyDirTree(Path fromPath, Path toPath)
      {
         this.fromPath = fromPath;
         this.toPath = toPath;
      }

      @Override
      public FileVisitResult preVisitDirectory(Path dir, 
                                               BasicFileAttributes attrs) 
         throws IOException 
      {
         System.out.println("dir = " + dir);
         System.out.println("fromPath = " + fromPath);
         System.out.println("toPath = " + toPath);
         System.out.println("fromPath.relativize(dir) = " + 
                            fromPath.relativize(dir));
         System.out.println("toPath.resolve(fromPath.relativize(dir)) = " + 
                            toPath.resolve(fromPath.relativize(dir)));

         Path targetPath = toPath.resolve(fromPath.relativize(dir));
         if (!Files.exists(targetPath))
            Files.createDirectory(targetPath);
         return FileVisitResult.CONTINUE;
      }

      @Override
      public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) 
         throws IOException 
      {
         System.out.println("file = " + file);
         System.out.println("fromPath = " + fromPath);
         System.out.println("toPath = " + toPath);
         System.out.println("fromPath.relativize(file) = " + 
                            fromPath.relativize(file));
         System.out.println("toPath.resolve(fromPath.relativize(file)) = " + 
                            toPath.resolve(fromPath.relativize(file)));

         Files.copy(file, toPath.resolve(fromPath.relativize(file)), copyOption);
         return FileVisitResult.CONTINUE;
      }

      @Override
      public FileVisitResult visitFileFailed(Path file, IOException ioe) 
      {
         System.err.println(ioe);
         return FileVisitResult.CONTINUE;
      }
   }

   public static void main(String[] args) throws IOException
   {
      if (args.length != 2)
      {
         System.err.println("usage: java Copy source target");
         return;
      }

      Path source = Paths.get(args[0]);
      Path target = Paths.get(args[1]);

      if (!Files.exists(source))
      {
         System.err.printf("%s source path doesn't exist%n", source);
         return;
      }

      if (!Files.isDirectory(source)) // Is source a file?
      {
         if (Files.exists(target))
            if (Files.isDirectory(target)) // Is target a directory?
               target = target.resolve(source.getFileName());

         try
         {
            Files.copy(source, target, StandardCopyOption.REPLACE_EXISTING);
         } 
         catch (IOException ioe)
         {
            System.err.printf("I/O error: %s%n", ioe.getMessage());
         }
         return;
      }

      if (Files.exists(target) && !Files.isDirectory(target)) // Is target an
      {                                                       // existing file?
         System.err.printf("%s is not a directory%n", target);
         return;
      }

      EnumSet<FileVisitOption> options 
         = EnumSet.of(FileVisitOption.FOLLOW_LINKS);
      CopyDirTree copier = new CopyDirTree(source, target);
      Files.walkFileTree(source, options, Integer.MAX_VALUE, copier);
   }
}

Listing 1's command-line-based application consists of a single Copy class, which nests a CopyDirTree visitor class (discussed later) and a main() method.

main() first verifies that exactly two command-line arguments, identifying the source and target paths of the copy operation, have been specified and then obtains their java.nio.file.Path objects.

Because there is no point in attempting to copy a non-existent file or directory, main() next invokes the java.nio.file.Files class's exists() method on the source path. If this method returns false, the source path doesn't exist, an error message is output, and the application terminates. Otherwise, main() determines which file-copy operation (file to file, file to directory, or directory hierarchy to directory hierarchy) to perform.

The source path is tested, via Files.isDirectory(source), to find out if it describes a file or a directory. If it describes a file, the compound statement following if (!Files.isDirectory(source)) is executed.

The compound statement first determines whether the target path exists, and if so, whether it describes a directory. If the target path describes an existing directory, the filename is extracted from the source path and resolved against the target path so that the source file will be copied into the directory (and not replace the directory). For example, if the source path is foo (a file) and the target path is bak (a directory), the target path following resolution is bak\foo (on Windows).

If the target path doesn't exist, it will be assumed to be a file. In any case, the copy operation is performed. If the target exists, it is replaced. (As an exercise, you might want to modify the code to prompt the user for permission.)

Following the execution of Files.copy(source, target, StandardCopyOption.REPLACE_EXISTING);, any I/O exception is reported and the application terminates.

At this point, the source path must describe a directory (directly or via a symbolic link). Because the only other file-copy operation being permitted is directory hierarchy to directory hierarchy, main() verifies that the target path describes an existing directory (via Files.exists(target) && !Files.isDirectory(target)), outputting an error message and terminating the application when this isn't the case.

Finally, main() prepares NIO.2's file-visiting feature to recursively visit all files and directories in the source hierarchy, and copy them to the target; and then initiates the visit. (For brevity, I won't delve into the file-visiting feature. Instead, I refer you to the Walking the File Tree section of The Java Tutorials for more information.) The file-visiting feature is based largely on the following types and methods:

  • java.nio.file.FileVisitOption enum
  • walkFileTree() methods
  • java.nio.file.FileVisitor<T> interface
  • java.nio.file.SimpleFileVisitor<T> class -- a trivial implementation of FileVisitor's methods
  • java.nio.file.FileVisitResult enum

main() prepares the file-visiting feature by creating an enumerated set of FileVisitOptions (the only option presented by this interface is FOLLOW_LINKS -- follow symbolic links so that the target of a link instead of the link itself will be copied) and then instantiating CopyDirTree, which I'll describe shortly, passing the source and target paths to its constructor. It then initiates the visit by invoking the following walkFileTree() static method (in the Files class):

Path walkFileTree(Path start, Set<FileVisitOption> options, int maxDepth,
                  FileVisitor<? super Path> visitor)
   throws IOException

walkFileTree() performs a depth-first traversal of the file tree rooted at a given starting file, which is described by path. I pass source as this root. A non-null java.util.Set of FileVisitOptions is passed as the second argument. I pass the previously created enumerated set so that walkFileTree() will follow symbolic links and copy the target of a link instead of the link itself.

The argument passed to maxDepth identifies the maximum number of directory levels to visit. Passing Integer.MAX_VALUE indicates that all levels should be visited. Finally, the previously created CopyDirTree object is passed to walkFileTree(). This object provides several methods that will be called throughout the traversal.

CopyDirTree indirectly implements the FileVisitor<T> interface by extending SimpleFileVisitor<Path>. As well as providing a constructor that saves the source and target paths, it overrides the following FileVisitor methods to perform the actual work:

  • FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException is invoked for a directory before its entries are visited. dir identifies the directory and attrs specifies the directory's basic attributes. java.io.IOException is thrown when a file I/O problem occurs.
  • FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException is invoked for a file in a directory. file identifies the file and attrs specifies the file's basic attributes. IOException is thrown when a file I/O problem occurs.
  • FileVisitResult visitFileFailed(Path file, IOException exc) throws IOException is invoked for a file that could not be visited; for example, the file's attributes could not be read or the file is a directory that could not be opened. file identifies the file and ioe identifies the I/O exception that prevented the file from being visited.

Each method returns one of the values specified by the FileVisitResult enum. Specifically, CONTINUE is returned to indicate that file-visiting should continue until the source directory hierarchy has been copied.

preVisitDirectory() is implemented to create a target directory in the resulting hierarchy when the directory doesn't exist. It first executes the following code:

Path targetPath = toPath.resolve(fromPath.relativize(dir));

This statement may seem confusing at first glance. However, its purpose is very simple. Each incoming directory path is relative to the source path (known in CopyDirTree as fromPath) and it must be made relative to the target path (known in CopyDirTree as toPath). For example, suppose the source path is s, s contains directory d, and the target path is t. When the method is called, dir contains s\d. Relativization produces d; resolution produces t\d.

Variable targetPath is assigned the resolved result (e.g., t\d). preVisitDirectory() determines if this directory exists and creates the directory (via the Files class's Path createDirectory(Path dir, FileAttribute<?>... attrs) method) when it doesn't exist. Assuming that createDirectory() doesn't throw IOException, preVisitDirectory() returns FileVisitResult.CONTINUE to continue file-visiting.

visitFile() is much simpler. It performs the copy operation (after relativizing and resolving the file from the source to the target) and then returns CONTINUE.

Building and running Copy

Execute the following command to compile Copy.java:

javac Copy.java

Assuming successful compilation, and assuming that you have a directory structure consisting of s with a d subdirectory containing file foo, execute the following command to copy this hierarchy to non-existent directory t:

java Copy s t

You should observe the following messages along with an identical directory hierarchy rooted in t:

dir = s
fromPath = s
toPath = t
fromPath.relativize(dir) = 
toPath.resolve(fromPath.relativize(dir)) = t
dir = s\d
fromPath = s
toPath = t
fromPath.relativize(dir) = d
toPath.resolve(fromPath.relativize(dir)) = t\d
file = s\d\foo.txt
fromPath = s
toPath = t
fromPath.relativize(file) = d\foo.txt
toPath.resolve(fromPath.relativize(file)) = t\d\foo.txt
1 2 3 Page 1
Notice to our Readers
We're now using social media to take your comments and feedback. Learn more about this here.