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
1 2 3 Page 2
Page 2 of 3
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

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:

.\charts\2015
.\charts\2015\2015.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:

.\charts\2015\barchart.png
.\charts\2015\linechart.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.

Related:
1 2 3 Page 2
Page 2 of 3