NIO.2 cookbook, Part 3

how-to
Jun 1, 201521 mins

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

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 bakfoo (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 sd. Relativization produces d; resolution produces td.

Variable targetPath is assigned the resolved result (e.g., td). 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 = sd
fromPath = s
toPath = t
fromPath.relativize(dir) = d
toPath.resolve(fromPath.relativize(dir)) = td
file = sdfoo.txt
fromPath = s
toPath = t
fromPath.relativize(file) = dfoo.txt
toPath.resolve(fromPath.relativize(file)) = tdfoo.txt

Finding files

Q: I need to create an application that uses pattern matching to locate files. What sort of assistance does NIO.2 provide for this task?

A: NIO.2 provides the java.nio.file.PathMatcher interface, which is implemented by classes whose objects perform match operations on Paths, via the following method:

boolean matches(Path path)

An implementation of this method matches path against some pattern that the implementation makes available to matches(). If there is a match, matches() returns true. Otherwise, this method returns false.

NIO.2 also provides the PathMatcher getPathMatcher(String syntaxAndPattern) method in the java.nio.file.FileSystem interface. getPathMatcher() takes a single argument string that identifies a pattern language and a pattern conforming to the language syntax, and returns a PathMatcher object whose matches() method performs the desired pattern match against its Path argument.

The argument string must conform to the following syntax:

language:pattern

The language component of the argument string identifies the pattern language, which is one of glob or regex — or another file system-dependent language. Case doesn’t matter when specifying this portion of the argument string. For example, you can specify glob, GLOB, regEX, and so on. Following a colon delimiter is the pattern component. Its syntax must conform to that of the pattern language identified by language.

glob is a simple pattern language that’s described in PathMatcher‘s Javadoc. In contrast, regex is somewhat more involved: its patterns must conform to the java.util.regex.Pattern class’s syntax rules.

The following code fragment demonstrates PathMatcher and matches():

PathMatcher pm = FileSystems.getDefault().getPathMatcher("glob:*.html");
System.out.println(pm.matches(Paths.get("index.html")));
System.out.println(pm.matches(Paths.get("demo.java")));

The first line obtains a PathMatcher from the default file system. Argument glob:*.html identifies the glob pattern language and the “all files ending with the .html extension” pattern. The second line invokes the pattern matcher’s matches() method on path index.html. Because this path’s .html file extension matches the pattern, there’s a match and true is output. In contrast, the final line outputs false because .java doesn’t match .html.

Finally, NIO.2 provides the file-visitor infrastructure for visiting all files and directories in a hierarchy, and which I demonstrated in the previous recipe.

Q: Can you provide me with an example application for locating files and directories via PathMatcher and the file-visitor infrastructure?

A: The Finding Files section in the Basic I/O lesson of The Java Tutorials provides a Find application that accomplishes this task. I’ve modified this application to also support the regex pattern language; its source code appears in Listing 2.

Listing 2. Find.java

import java.io.IOException;

import java.nio.file.Files;
import java.nio.file.FileSystems;
import java.nio.file.FileVisitResult;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.PathMatcher;
import java.nio.file.SimpleFileVisitor;

import java.nio.file.attribute.BasicFileAttributes;

public class Find 
{
   public static class Finder extends SimpleFileVisitor<Path> 
   {
      private int numMatches; // defaults to 0

      private PathMatcher matcher;

      Finder(String language, String pattern)
      {
         matcher = FileSystems.getDefault().getPathMatcher(language + ":" +
                                                           pattern);
      }

      void find(Path file) 
      {
         Path name = file.getFileName();
         if (name != null && matcher.matches(name)) 
         {
            numMatches++;
            System.out.println(file);
         }
      }

      int getMatches()
      {
         return numMatches;
      }

      @Override
      public FileVisitResult preVisitDirectory(Path dir, 
                                               BasicFileAttributes attrs) 
      {
         find(dir);
         return FileVisitResult.CONTINUE;
      }

      @Override
      public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) 
      {
         find(file);
         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 Find path [-r] pattern");
         return;
      }
      Path startingDir = Paths.get(args[0]);
      String language = "glob";
      String pattern = args[1];
      if (args.length > 2 && args[1].equals("-r"))
      {
         language = "regex";
         pattern = args[2];
      }
      Finder finder = new Finder(language, pattern);
      Files.walkFileTree(startingDir, finder);
      System.out.printf("Number of matches: %d%n", finder.getMatches());
   }
}

Listing 2 presents a command-line-based application consisting of a single Find class, which nests a Finder visitor class and a main() method.

Finder‘s constructor accepts pattern language and pattern arguments and creates a path matcher based on these arguments. Its find() method invokes the path matcher’s matches() method on the filename portion of the path. The find() method is invoked from preVisitDirectory() to determine if the directory being visited matches the pattern. find() is also invoked from visitFile() to determine if the visited file matches the pattern.

main() validates the command line and then extracts the starting directory, pattern language, and pattern arguments. The pattern language defaults to glob, but is switched to regex when -r is detected. A Finder object is created and configured to the pattern language and pattern, and this object is passed to the Files class’s simpler Path walkFileTree(Path start, FileVisitor<? super Path> visitor) throws IOException method. This method is equivalent to walkFileTree(start, EnumSet.noneOf(FileVisitOption.class), Integer.MAX_VALUE, visitor). Finally, main() invokes Finder‘s getMatches() method to obtain the number of matches, which it outputs.

Building and running Find

Execute the following command to compile Find.java:

javac Find.java

Assuming successful compilation, create the following file/directory hierarchy in the current directory:

charts
   2015
      areachart.gif
      barchart.png
      linechart.jpg
      2015.txt
   charts.txt

Execute the following command to match the charts directory using glob syntax:

java Find . charts

You should observe the following output:

.charts
Number of matches: 1

Next, execute the following command to locate all files and directories whose name is 2015:

java Find . 2015*

You should observe the following output:

.charts2015
.charts20152015.txt
Number of matches: 2

Finally, execute the following command, which uses regex syntax, to locate all files with .png and .jpg extensions:

java Find . -r "([^s]+(.(?i)(png|jpg))$)"

You should observe the following output:

.charts2015barchart.png
.charts2015linechart.jpg
Number of matches: 2

You can achieve identical output by specifying the following glob equivalent:

java Find . "*.{png,jpg}"

Watching directories

Q: I’m creating a text editor and I’d like the editor to present a “reload file?” dialog box to the user when the file being edited is modified by another program. How can I accomplish this task?

A: You can accomplish this task by using NIO.2’s Watch Service API, which lets you detect file/directory creation, deletion, or modification in some specified directory. This API includes the following types (in the java.nio.file package):

  • Watchable: an interface describing any object that may be registered with a watch service so that it can be watched for changes and events. Because Path extends Watchable, all entries in directories represented as Paths can be watched.
  • WatchEvent: an interface describing any event or repeated event for an object that is registered with a watch service.
  • WatchEvent.Kind: a nested interface that identifies an event kind (e.g, directory entry creation).
  • WatchEvent.Modifier: a nested interface qualifying how a watchable is registered with a watch service.
  • WatchKey: an interface describing a token representing the registration of a watchable object with a watch service.
  • WatchService: an interface describing any object that watches registered objects for changes and events.
  • StandardWatchEventKinds: a class describing four event kinds (directory entry creation, deletion, or modification; and overflow [other events may have been lost or discarded]).
  • ClosedWatchServiceException: a class describing an unchecked exception that’s thrown when an attempt is made to invoke an operation on a watch service that is closed.

You would typically perform the following steps to interact with the Watch Service API:

  1. Create a WatchService object for watching one or more directories with the current or some other file system. This object is known as a watcher.
  2. Register each directory to be monitored with the watcher. When registering a directory, specify the kinds of events (described by the StandardWatchEventKinds class) of which you want to receive notification. For each registration, you will receive a WatchKey instance that serves as a registration token.
  3. Implement an infinite loop to wait for incoming events. When an event occurs, the key is signalled and placed into the watcher’s queue.
  4. Retrieve the key from the watcher’s queue. You can obtain the filename from the key.
  5. Retrieve each pending event for the key (there might be multiple events) and process as needed.
  6. Reset the key and resume waiting for events.
  7. Close the service. The watch service exits when either the thread exits or when it’s closed (by invoking its close() method).

I’ve created a small application that demonstrates these steps — close() isn’t called because the watch service is closed automatically when the application ends. Listing 3 presents its source code.

Listing 3. Watch.java

import java.io.IOException;

import java.nio.file.FileSystems;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.WatchEvent;
import java.nio.file.WatchKey;
import java.nio.file.WatchService;

import static java.nio.file.StandardWatchEventKinds.*;

public class Watch
{
   public static void main(String[] args) throws IOException
   {
      if (args.length != 1)
      {
         System.err.println("usage: java WatcherDemo directory");
         return;
      }
      WatchService watcher = FileSystems.getDefault().newWatchService();
      Path dir = Paths.get(args[0]);
      dir.register(watcher, ENTRY_CREATE, ENTRY_DELETE, ENTRY_MODIFY);
      for (;;) 
      {
         WatchKey key;
         try 
         {
            key = watcher.take();
         } 
         catch (InterruptedException ie) 
         {
            return;
         }

         for (WatchEvent<?> event: key.pollEvents()) 
         {
            WatchEvent.Kind<?> kind = event.kind();
            if (kind == OVERFLOW)
            {
               System.out.println("overflow");
               continue;
            }
            @SuppressWarnings("unchecked")
            WatchEvent ev = (WatchEvent) event;
            Path filename = ev.context();
            System.out.printf("%s: %s%n", ev.kind(), filename);
         }
  
         boolean valid = key.reset();
         if (!valid)
            break;
      }
   }
}

Listing 3 presents a command-line-based application consisting of a single Watch class, which nests a main() method. This method first verifies that one argument (representing a directory) has been specified on the command line.

A watch service is subsequently created by invoking the default FileSystem‘s WatchService newWatchService() method. This method throws java.lang.UnsupportedOperationException when the file system doesn’t support watch services (watch services are provided by the host operating system and not by the Java platform, which abstracts over them), and throws IOException when an I/O error occurs.

The command-line argument that represents a directory is wrapped in a Path object, and then Path‘s WatchKey register(WatchService watcher, WatchEvent.Kind<?>... events) method is called to register the directory located by this path with the previously created watch service. During registration, the watch service is informed that the application wants to be notified of directory entry creations, deletions, and modifications.

At this point, an infinite loop is entered. The first task is to invoke WatchService‘s WatchKey take() method to retrieve and remove the next watch key, waiting when none are yet present. Alternatively, the WatchKey poll() method could be called to retrieve and remove the next watch key, or return null when none are present. The returned WatchKey is identical to the WatchKey returned by register(), which is why I didn’t save register()‘s return value.

A watch key has state. It is in the ready state when initially created and in the signalled state when an event is detected (the watch key is then queued for retrieval by poll() or take()). Events detected while the key is in the signalled state are queued but don’t cause the key to be requeued for retrieval from the watch service. Events are retrieved by invoking WatchKey‘s List<WatchEvent<?>> pollEvents() method.

For each WatchEvent object stored in the returned java.util.List object, WatchEvent.Kind<T> kind() returns the kind of event that has been returned. If the event kind is OVERFLOW, events have been lost (because the file system is generating them too quickly to keep up). This condition is reported and the loop is continued (there is no other meaningful information to be obtained and reported).

For every other event kind, the WatchEvent<?> object is cast to WatchEvent<Path> (the resulting unchecked warning message is suppressed) in order to invoke WatchEvent<Path>‘s Path context() method, which returns the event’s context (the relative path between the directory registered with the watch service and the entry that is created, deleted, or modified). The event kind and context are subsequently output.

Finally, the watch key must be reset to the ready state, by invoking WatchKey‘s boolean reset() method. reset() returns true when the watch key is valid and has been reset. It returns false when the watch key could not be reset because it’s no longer valid (perhaps because the watch service has been closed). (This step is very important. Fail to invoke reset() and this key will not receive any further events.)

Building and running Watch

Execute the following command to compile Watch.java:

javac Watch.java

Assuming successful compilation, execute the following command to execute Watch and monitor the current directory, which will contain Watch.java and Watch.class:

java Watch

Using a separate command window, change to the same directory and output the following instructions (I’m assuming a Windows platform):

md test
rd test
copy Watch.java Watch.bak
erase Watch.bak

You should observe the following output, which indicates that directory test has been created and deleted, and that file Watch.bak has been created, modified, and deleted:

ENTRY_CREATE: test
ENTRY_DELETE: test
ENTRY_CREATE: Watch.bak
ENTRY_MODIFY: Watch.bak
ENTRY_MODIFY: Watch.bak
ENTRY_DELETE: Watch.bak

What’s next?

JDK 8u40 introduced several enhancements to JavaFX 8: support for accessibility, new dialog boxes, a new spinner control, and a text-formatting capability. Next time, I’ll introduce you to each of these enhancements.

download
Get the source code for this post’s applications. Created by Jeff Friesen for JavaWorld

The following software was used to develop the post’s code:

  • 64-bit JDK 7u6

The post’s code was tested on the following platform(s):

  • JVM on 64-bit Windows 7 SP1