An NIO.2 primer, Part 2: The file system APIs

Explore new APIs that let you visit, watch, and get the attributes of files and directories

This article, the second half of a two-part introduction to More New I/O APIs for the Java™ Platform (NIO.2), covers the most useful classes and functionality in the new java.nio.file package and subpackages. As in Part 1's discussion of the NIO.2 asynchronous channel APIs, the authors demonstrate the concepts involved with extensive code examples.

Share:

Catherine Hope (catherine.hope@uk.ibm.com), Apache Harmony Developer, IBM

Catherine HopeCatherine Hope has worked in the Hursley Runtime Deliveries department of the IBM Java Technology Centre since starting as a graduate in 2006. After three years as a system tester, she has spent the past year working as a Java class library developer and contributor to the Apache Harmony project.



Oliver Deakin (odeakin@uk.ibm.com), Apache Harmony Developer, IBM

Oliver DeakinOliver Deakin joined IBM's Java Technology Centre in 2003. For the last five years, he has been a developer, committer, and PMC member for the Apache Harmony project, an open source implementation of the Java runtime. He has experience working in many areas of the class libraries in both Java and native code. In his spare time, he enjoys rock climbing.



21 September 2010

Also available in Japanese

This article completes our two-part introduction to More New I/O APIs for Java (NIO.2) in Java 7. Like the asynchronous channel APIs explored in Part 1, NIO.2's file system APIs fill some significant gaps in the way previous Java versions handle I/O. According to the NIO.2 Java specification request (JSR 203):

The Java platform has long needed a filesystem interface better than the java.io.File class. That class does not handle filenames in a way that works consistently across platforms, it does not support efficient file-attribute access, it does not allow sophisticated applications to take advantage of filesystem-specific features (for example, symbolic links) when available, and many of its methods simply return false on error instead of throwing an informative exception.

Coming to the rescue are three new file system packages in the Java 7 beta:

  • java.nio.file
  • java.nio.file.attribute
  • java.nio.file.spi

This article focuses on the most useful classes in these packages:

  • java.nio.file.Files and java.nio.file.FileVisitor allow you to walk through file systems, querying files or directories up to a certain depth and executing user-implemented callback methods for each one found.
  • java.nio.file.Path and java.nio.file.WatchService allow you to register to "watch" a specific directory. An application watching directories receives notification if files in those directories are created, modified, or deleted.
  • java.nio.attribute.*AttributeView allow you to view file and directory attributes that were previously hidden from Java users. These attributes include file owner and group permissions, access-control lists (ACLs), and extended file attributes.

We'll provide examples that show how to use these classes. The examples are available in a runnable state (see Download), and you can try them out on the Java 7 betas available from IBM® and Oracle (both still under development at the time of this writing; see Resources).

File visitors

Our first example demonstrates the new FileVisitor API.

Imagine a scenario in which you want to traverse a directory tree recursively, stopping at each file and directory under that tree and having your own callback methods invoked for each entry found. In previous Java versions, this would have been a painful process involving recursively listing directories, inspecting their entries, and invoking the callbacks yourself. In Java 7, this is all provided via the FileVisitor API, and using it couldn't be simpler.

The first step is to implement your own FileVisitor class. This class contains the callback methods that the file-visitor engine will invoke as it traverses the file system. The FileVisitor interface consists of five methods, listed here in the typical order they would be called during traversal (T here stands for either java.nio.file.Path or a superclass):

  • FileVisitResult preVisitDirectory(T dir) is called before the entries in that directory are visited. It returns one of the FileVisitResult's enum values to tell the file visitor API what to do next.
  • FileVisitResult preVisitDirectoryFailed(T dir, IOException exception) is called when a directory could not be visited for some reason. The exception that caused the visit to fail is specified in the second parameter.
  • FileVisitResult visitFile(T file, BasicFileAttributes attribs) is called when a file in the current directory is being visited. The attributes for this file are passed into the second parameter. (You'll learn more about file attributes in this article's File attributes section.)
  • FileVisitResult visitFileFailed(T file, IOException exception) is called when the visit to a file has failed. The second parameter specifies the exception that caused the visit to fail.
  • FileVisitResult postVisitDirectory(T dir, IOException exception) is called after the visit to a directory and all its subdirectories has completed. The exception parameter is null when the directory visit has been successful, or it contains the exception that caused the directory visit to end prematurely.

To help developers save time, NIO.2 has kindly provided an implementation of the FileVisitor interface: java.nio.file.SimpleFileVisitor. This class is as basic as it gets: for the *Failed() methods, it just rethrows the exception, and for the other methods it continues without doing anything at all! What's useful about this class is that you can use anonymous classes to override only the methods you want; the rest of the methods are implemented by default.

Listing 1 shows how we create an example FileVisitor instance:

Listing 1. FileVisitor implementation
FileVisitor<Path> myFileVisitor = new SimpleFileVisitor<Path>() { 

    @Override
    public FileVisitResult preVisitDirectory(Path dir) { 
       System.out.println("I'm about to visit the "+dir+" directory"); 
       return FileVisitResult.CONTINUE;
    }
  
    @Override 
    public FileVisitResult visitFile(Path file, BasicFileAttributes attribs) {

       System.out.println("I'm visiting file "+file+" which has size " +attribs.size());
       return FileVisitResult.CONTINUE;
    }

};

The FileVisitor implementation in Listing 1 should print a message for each directory and file it visits and also give the size of the files from their BasicFileAttributes.

Next we want to create a Path from which to start our file visiting. This is done using the java.nio.Paths class:

Path headDir = Paths.get("headDir");

We can use either of two methods on the java.nio.Files class to start the tree traversal:

  • public static void walkFileTree(Path head, FileVisitor<? super Path> fileVisitor) walks the file tree under the head directory, invoking the callback methods implemented in fileVisitor as it goes.
  • public static void walkFileTree(Path head, Set<FileVisitOption> options, int depth, FileVisitor<? super Path> fileVisitor) is similar to the preceding method but gives you two additional parameters to specify visit options and how many directories deep into the file tree the traversal should go.

We'll use the simpler version of the walkFileTree() method to start the process of walking the file tree:

Files.walkFileTree(headDir, myFileVisitor);

Suppose the directory structure looks like this:

 headDir
  |--- myFile1
  |--- mySubDirectory1
  |     \myFile2
  \--- mySubDirectory2
        |--- myFile3
        \--- mySubdirectory3
              \---myFile4

Listing 2 shows the output from this example:

Listing 2. FileVisitor output
I'm about to visit the headDir directory
I'm about to visit the headDir\mySubDirectory2 directory
I'm about to visit the headDir\mySubDirectory2\mySubDirectory3 directory
I'm visiting file headDir\mySubDirectory2\mySubDirectory3\myFile4 which has size 2
I'm visiting file headDir\mySubDirectory2\myFile3 which has size 2
I'm about to visit the headDir\mySubDirectory1 directory
I'm visiting file headDir\mySubDirectory1\myFile2 which has size 2
I'm visiting file headDir\myFile1 which has size 2

As you can see, the file traversal is depth first but not necessarily in any alphabetical order within a directory. Our callback methods were invoked as expected, and we can see that all the files in the tree have been listed and all the directories have been visited.

In only about 15 lines, we've created a file visitor that will walk any file tree you give it and inspect the files contained therein. This example is basic, but the callbacks can be implemented to be as complex as you want.


Directory watching

This second example covers the exciting world of the new WatchService API and its associated classes.

The scenario for this example is simple: You'd like to track whether any files or directories in a particular directory (or directories) are being created, modified, or deleted. You might use this information to update a file listing in a GUI display or perhaps to detect the modification of configuration files that could then be reloaded. In previous Java versions, you must implement an agent running in a separate thread that keeps track of all the contents of the directories you wish to watch, constantly polling the file system to see if anything relevant has happened. In Java 7, the WatchService API provides the ability to watch directories. It removes all the complexity of writing your own file system poller and, where possible, is based upon existing native system APIs for better performance.

The first step is to create a WatchService instance via the java.nio.file.FileSystems class. We won't go into the details of file systems in this article, so just know that in most cases you'll want to get the default file system and then invoke its newWatchService() method:

WatchService watchService = FileSystems.getDefault().newWatchService();

Now that we have our watch service instance, we want to register a path to watch. We create a Path object for the directory we wish to watch in a slightly different way from the file visitor example, so that we can use its File instance again later:

File watchDirFile = new File("watchDir");
Path watchDirPath = watchDirFile.toPath();

The Path class implements the java.nio.file.Watchable interface, and that interface defines the register() method we'll be using in this example. WatchKey register(WatchService watchService, WatchEvent.Kind<?>... events) registers the Path this method is called on with the specified watchService for the specific events given. Events trigger a notification only if they are specified in the register call.

For the default WatchService implementation, the java.nio.file.StandardWatchEventKind class defines three static implementations of watchEvent.Kind that can be used in the register() calls:

  • StandardWatchEventKind.ENTRY_CREATE indicates that a file or directory has been created within the registered Path. An ENTRY_CREATE event is also triggered when a file is renamed or moved into this directory.
  • StandardWatchEventKind.ENTRY_MODIFY indicates that a file or directory in the registered Path has been modified. Exactly which events constitute a modification is somewhat platform-specific, but suffice it to say that actually modifying the contents of a file always triggers a modify event. On some platforms, changing attributes of files can also trigger this event.
  • StandardWatchEventKind.ENTRY_DELETE indicates that a file or directory has been deleted from the registered Path. An ENTRY_DELETE event is also triggered when a file is renamed or moved out of this directory.

For our example, let's watch the ENTRY_CREATE and ENTRY_MODIFY events, but not ENTRY_DELETE:

WatchKey watchKey = watchDirPath.register(watchService, 
       StandardWatchEventKind.ENTRY_CREATE, StandardWatchEventKind.ENTRY_MODIFY);

Our Path is now registered to be watched, and the WatchService will work away silently in the background, watching that directory intently. The same WatchService instance can watch multiple directories by using the same Path creation and register() calls we've shown above.

The observant among you may have spotted that the register() method call returns a class we haven't come across before: WatchKey. This class represents your registration with the WatchService. Whether you hang onto this reference or not is up to you, because the WatchService returns the relevant WatchKey to you when an event is triggered. However, do note that there are no method calls to find out which directory the WatchKey was registered with, so if you're watching multiple directories you may wish to track which WatchKeys go with which Paths. When you're done with a particular WatchKey and the events it's registered for, you can cancel its registration with the WatchService simply by calling its cancel() method.

Now that our Path is registered, we can check in with the WatchService at our convenience to see if any of the events we were interested in has occurred. WatchService provides three methods for checking if anything exciting has happened:

  • WatchKey poll() returns the next WatchKey that has had some of its events occur, or null if no registered events have happened.
  • WatchKey poll(long timeout, TimeUnit unit) takes a timeout and time units (java.util.concurrent.TimeUnit). If an event occurs during the specified time period, this method exits, returning the relevant WatchKey. If there are no WatchKeys to return by the end of the timeout, this method returns null.
  • WatchKey take() is similar to the preceding methods, except it will wait indefinitely until a WatchKey is available to return.

Once a WatchKey has been returned by one of these three methods, it will not be returned by a further poll() or take() call until its reset() method is invoked, even if events it is registered for occur. Once a WatchKey is returned by the WatchService, you can inspect its events that have been triggered by calling the WatchKey's pollEvents() method, which returns a List of WatchEvents.

To illustrate, the simple example in Listing 3 continues from the WatchKey we registered earlier:

Listing 3. Using pollEvents()
// Create a file inside our watched directory
File tempFile = new File(watchDirFile, "tempFile");
tempFile.createNewFile();

// Now call take() and see if the event has been registered
WatchKey watchKey = watchService.take();
for (WatchEvent<?> event : watchKey.pollEvents()) {
   System.out.println(
   "An event was found after file creation of kind " + event.kind() 
   + ". The event occurred on file " + event.context() + ".");
}

When executed, the code in Listing 3 prints:

An event was found after file creation of kind ENTRY_CREATE. The event occurred 
 on file tempFile.
An event was found after file creation of kind ENTRY_MODIFY. The event occurred 
 on file tempFile.

As you can see, we got the ENTRY_CREATE event for the newly created tempFile as expected, but we also got another event. On some operating systems, a file creation or deletion can sometimes also produce an ENTRY_MODIFY event. If we had not registered to receive modification events, then we would have only gotten the ENTRY_CREATE event, whatever the OS.

Extended sample code (demonstrating file modification and deletion for the registered WatchKey in this section's example) is included in the sample code download.


File attributes

Our third and final example covers the new APIs for getting and setting file attributes using the classes under the java.nio.file.attribute package.

The new APIs offer access to more file attributes than you can imagine. In previous Java releases, you can get only a basic set of file attributes (size, modification time, whether the file is hidden, and whether it is a file or directory). To get or modify any further file attributes, you must implement this yourself in native code specific to the platforms you want to run on — not exactly easy. Brilliantly, Java 7 should allow you to read and, where possible, modify an extended set of attributes in a simple way via the java.nio.file.attribute classes, completely abstracting away the platform-specific nature of these operations.

Seven attribute views are available in the new APIs, some of which are specific to the operating system. These "view" classes allow you to get and set whichever attributes they are associated with, and each one has a counterpart attribute class that contains the actual attribute information. Let's take a look at them in turn.

AclFileAttributeView and AclEntry

AclFileAttributeView allows you to get and set the ACL and file-owner attributes of a particular file. Its getAcl() method returns a List of AclEntry objects, one for each permission set on the file. Its setAcl(List<AclEntry>) method allows you to modify that access list. This attribute view is only available for Microsoft® Windows® systems.

BasicFileAttributeView and BasicFileAttributes

This view class allows you to get a set of — no surprise — basic file attributes, building on those available in previous Java versions. Its readAttributes() method returns a BasicFileAttributes instance containing details of last modified time, last access time, creation time, size, and type of file (regular file, directory, symbolic link, or other). This attribute view is available on all platforms.

Let's look at an example of this view. To get a file attribute view for a particular file we start, as always, by creating a Path object for the file we're interested in:

 File attribFile = new File("attribFile");
 Path attribPath = attribFile.toPath();

To get the file attribute view we want, we'll use the getFileAttributeView(Class viewClass) method on Path. To get the BasicFileAttributeView for attribPath, we simply call:

BasicFileAttributeView basicView 
    = attribPath.getFileAttributeView(BasicFileAttributeView.class);

As described earlier, to get the BasicFileAttributes from BasicFileAttributeView, we just call its readAttributes() method:

BasicFileAttributes basicAttribs = basicView.readAttributes();

And that's it. Now you have all of the basic file attributes for that file to do whatever you wish with. For the BasicFileAttributes, only the creation, last-modified, and last-access times can be altered (because it wouldn't make sense to change the size or type of file). To change these, we can use the java.nio.file.attribute.FileTime class to create a new time and then call the setTimes() method on BasicFileAttributeView. For example, we could move the last-modified time for our file a minute further into the future:

FileTime newModTime 
    = FileTime.fromMillis(basicAttribs.lastModifiedTime().toMillis() + 60000);
 basicView.setTimes(newModTime, null, null);

The two nulls indicate that we do not want to change the last access time or creation time for this file. If you check the basic attributes again, in the same way we did earlier, you should see that the last modified time has been altered but the creation time and last access time have remained the same.

DosFileAttributeView and DosFileAttributes

This view class allows you to get attributes specific to DOS. (As you might guess, this view is for Windows systems only.) Its readAttributes() method returns a DosFileAttributes instance containing details of whether the file in question is read-only, hidden, a system file, and an archive. The view also has set*(boolean) methods for each of these properties.

FileOwnerAttributeView and UserPrincipal

This view class allows you to get and set the owner of a particular file. Its getOwner() method returns a UserPrincipal (also in the java.nio.file.attribute package), which in turn has a getName() method returning a String containing the owner's name. The view also provides a setOwner(UserPrincipal) method allowing you to change a file's owner. This view is available on all platforms.

FileStoreSpaceAttributeView and FileStoreSpaceAttributes

This catchily named class allows you to get information about a particular file store. Its readAttributes() method returns a FileStoreSpaceAttributes instance containing details of the total space, the unallocated space, and the usable space on the file store. This view is available on all platforms.

PosixFileAttributeView and PosixFileAttributes

This view class, available on UNIX® systems only, allows you to get and set attributes specific to POSIX (Portable Operating System Interface). Its readAttributes() method returns a PosixFileAttributes instance containing details of the owner, group owner, and file permissions for this file (those you would normally set using the UNIX chmod command). The view also provides setOwner(UserPrincipal), setGroup(GroupPrincipal), and setPermissions(Set<PosixFilePermission>) methods to modify these attributes.

UserDefinedFileAttributeView and String

This view class, available only on Windows, allows you to get and set extended attributes on files. These attributes are unlike the others in that they are just name-value pairs and can be set to anything you wish. This can be useful if you want to add some hidden metadata to a file without altering the file's content. The view provides a list() method that returns a List of String names of the extended attributes for the relevant file.

To get the contents of a particular attribute once you have its name, the view has a size(String name) method to return the size of the attribute's value and a read(String name, ByteBuffer dest) method to read the attribute value into the ByteBuffer. The view also provides a write(String name, ByteBuffer source) method to create or alter an attribute, and also a delete(String name) method to remove an existing attribute entirely.

This is probably the most interesting new attribute view because it allows you to add attributes with arbitrary String names and ByteBuffer values to files. That's right — its value is a ByteBuffer, so you can store any binary data in there you want.

First, we'll get the attribute view:

UserDefinedFileAttributeView userView 
    = attribPath.getFileAttributeView(UserDefinedFileAttributeView.class);

To get a list of the user-defined attribute names for this file, we call the list() method on the view:

List<String> attribList = userView.list();

Once we have a particular attribute name we wish to get the associated value for, we allocate a ByteBuffer of the right size for the value and then call the view's read(String, ByteBuffer) method:

ByteBuffer attribValue = ByteBuffer.allocate(userView.size(attribName));
userView.read(attribName, attribValue);

attribValue now contains whatever data was stored for that particular attribute. To set your own attribute, you simply need to create your ByteBuffer and fill it with whatever data you wish and then call the write(String, ByteBuffer) method on the view:

userView.write(attribName, attribValue);

Writing an attribute either creates that attribute or overwrites an existing attribute with the same name.

And with that, we conclude the third and final example. Full example code demonstrating four of the attribute views (BasicFileAttributeView, FileOwnerAttributeView, FileStoreSpaceAttributeView, and UserDefinedAttributeView) is included in the sample code download.


Conclusion

In addition to the topics covered in this article are a number of other NIO.2 file APIs. With Java 7 comes the new ability to create, inspect, and modify symbolic links. There are also new classes allowing access to lower-level information about the file system, and even to supply providers (called FileSystem and FileStore) to access any type of file system you wish.

In summary, NIO.2 gives Java developers a simple, consistent, and powerful set of APIs to interact with the file system. Its aim is to abstract away some of the complex platform-specific details of dealing with files and directories and to give programmers more power and flexibility, and it has achieved this well.


Download

DescriptionNameSize
Sample Java code for the examples in this articlej-nio2-2.zip5KB

Resources

Learn

Get products and technologies

Discuss

Comments

developerWorks: Sign in

Required fields are indicated with an asterisk (*).


Need an IBM ID?
Forgot your IBM ID?


Forgot your password?
Change your password

By clicking Submit, you agree to the developerWorks terms of use.

 


The first time you sign into developerWorks, a profile is created for you. Information in your profile (your name, country/region, and company name) is displayed to the public and will accompany any content you post, unless you opt to hide your company name. You may update your IBM account at any time.

All information submitted is secure.

Choose your display name



The first time you sign in to developerWorks, a profile is created for you, so you need to choose a display name. Your display name accompanies the content you post on developerWorks.

Please choose a display name between 3-31 characters. Your display name must be unique in the developerWorks community and should not be your email address for privacy reasons.

Required fields are indicated with an asterisk (*).

(Must be between 3 – 31 characters.)

By clicking Submit, you agree to the developerWorks terms of use.

 


All information submitted is secure.

Dig deeper into Java technology on developerWorks


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=1
Zone=Java technology
ArticleID=524052
ArticleTitle=An NIO.2 primer, Part 2: The file system APIs
publish-date=09212010