Java File I/O (NIO.2)

Use the Path class to operate on file and directory paths

A java.nio.file.Path object is somewhat analogous to a java.io.File object as it can represent a file or directory on the file system. A Path object is more abstract though, in that it is a sequence of names that represent a directory hierarchy (that may or may not include a file) on the file system. There are no methods in the Path interface that allow for working with directories or files. The methods defined are for working with or manipulating Path objects only, resolving one Path to another. (There is one method that can be used to obtain a java.io.File object from a java.nio.file.Path, toFile. Likewise the java.io.File class now contains a toPath method.) To work with files and directories, Path objects are used in conjunction with the java.nio.file.Files class. The java.nio.file.Files class consists entirely of static methods for manipulating directories and files, including copy, move and functions for working with symbolic links.

Implementations of java.nio.file.Path interface are immutable and safe for use by multiple concurrent threads.

In Java 6 and earlier you do that:

File file = new File("README.TXT");
					

In Java 7 you do that:

// Paths class consists exclusively of static methods that return a Path by
// converting a path string or URI

Path path = Paths.get("README.TXT");
					

To make the migration to Java 7 easier, the File class has a new method toPath() that allows you to transform File to Path:

Path path = new File("README.TXT").toPath();
					

Like File, a Path can also refer to a not existing file. That is only file path, NOT the data containing in a file.

The following code snippet defines a Path instance and then invoke several methods to obtain information about the path. Assume we have following folders structure on Windows platform:

C:\home\zaikin\foo
					

  • Create Path:

    Path path = Paths.get("C:\\home\\zaikin\\foo");
    								

    NOTE: None of listed here methods requires that the file corresponding to the Path existed.

  • String s = path.toString();
    								

    returns

    C:\home\zaikin\foo
    								

    The toString() method returns the string representation of the Path. If the path was created using Filesystems.getDefault().getPath(String) or Paths.get(...) (the latter is a convenience method for getPath), the method performs minor syntactic cleanup. For example, in a UNIX operating system, it will correct the input string //home/zaikin/foo to /home/zaikin/foo.

  • Path p = path.getFileName()
    								

    returns

    foo
    								

    The getFileName() method returns the name of the file or directory denoted by this path as a Path object. The file name is the farthest element from the root in the directory hierarchy (last element of the sequence of name elements).

  • Path p = path.getName(0)
    								

    returns

    home
    								

    The getName(int index) method returns a name element of this path as a Path object.

    The index parameter is the index of the name element to return. The element that is closest to the root in the directory hierarchy has index 0. The element that is farthest from the root has index count-1.

    NOTE: if index is negative, index is greater than or equal to the number of elements, or this path has zero name elements, then IllegalArgumentException is thrown.

  • int i = path.getNameCount();
    								

    returns

    3
    								

    The getNameCount() method returns the number of name elements in the path.

  • Path p = path.subpath(0,2);
    								

    returns

    home\zaikin
    								

    The subpath(int beginIndex, int endIndex) method returns a relative Path that is a subsequence of the name elements of this path.

    The beginIndex and endIndex parameters specify the subsequence of name elements. The name that is closest to the root in the directory hierarchy has index 0. The name that is farthest from the root has index count-1. The returned Path object has the name elements that begin at beginIndex and extend to the element at index endIndex-1.

  • Path p = path.getRoot();
    								

    returns

    C:\
    								

    The getRoot() method returns the root component of this path as a Path object, or null if this path does not have a root component (e.g. for relative paths).

    For UNIX platform the root will be "/". For Windows, something like "C:\".

  • Path p = path.resolveSibling("bar");
    								

    returns

    C:\home\zaikin\bar
    								

    The resolveSibling(Path other) and resolveSibling(String other) methods resolve the given path against this path's parent path. This is useful where a file name needs to be replaced with another file name. For example, suppose that the name separator is "/" and a path represents "dir1/dir2/foo", then invoking this method with the Path "bar" will result in the Path "dir1/dir2/bar". If this path does not have a parent path, or other is absolute, then this method returns other. If other is an empty path then this method returns this path's parent, or where this path doesn't have a parent, the empty path.

Removing redundancies from a Path

Given the following code:

Path path = Paths.get("C:\\home\\zaikin\\..\\..\\foo");
Path p = path.normalize();
System.out.println(p);
System.out.println(p.getNameCount());
					

it prints (NOTE: implementations of Path interface are immutable by design)

C:\foo
5
					

The normalize() method returns a path that is this path with redundant name elements eliminated.

The precise definition of this method is implementation dependent but in general it derives from this path, a path that does not contain redundant name elements. In many file systems, the "." and ".." are special names used to indicate the current directory and parent directory. In such file systems all occurrences of "." are considered redundant. If a ".." is preceded by a non-".." name then both names are considered redundant (the process to identify such names is repeated until is it no longer applicable).

NOTE: This method does not access the file system; the path may not locate a file that exists. Eliminating ".." and a preceding name from a path may result in the path that locates a different file than the original path. This can arise when the preceding name is a symbolic link.

Creating a path between two paths

A common requirement when you are writing file I/O code is the capability to construct a path from one location in the file system to another location. You can meet this using the relativize() method. This method constructs a path originating from the original path and ending at the location specified by the passed-in path. The new path is relative to the original path. Relativization is the inverse of resolution.

For example, consider two relative paths:

Path p1 = Paths.get("home");
Path p2 = Paths.get("home/zaikin/foo");
					

the folowing code:

Path p1_p2 = p1.relativize(p2);
System.out.println(p1_p2);

Path p2_p1 = p2.relativize(p1);
System.out.println(p2_p1);
					

will produce the following output:

zaikin\foo
..\..
					

In this example, the two paths share the same node, home. To navigate from home to foo, you first navigate one level down to zaikin and then one more level down to foo. Navigating from foo to home requires moving up two levels.

A relative path CANNOT be constructed if only one of the paths includes a root element. If both paths include a root element, the capability to construct a relative path is system dependent.

Joining two Paths

You can combine paths by using the Path.resolve(Path other) and Path.resolve(String other) methods. You pass in a partial path, which is a path that does not include a root element, and that partial path is appended to the original path.

If the other parameter is an absolute path then this method trivially returns other. If other is an empty path then this method trivially returns this path. Otherwise this method considers this path to be a directory and resolves the given path against this path. In the simplest case, the given path does not have a root component, in which case this method joins the given path to this path and returns a resulting path that ends with the given path. Where the given path has a root component then resolution is highly implementation dependent and therefore unspecified.

For example, consider the following code snippet:

Path p3 = Paths.get("C:\\home\\zaikin\\foo");
System.out.format("%s%n", p3.resolve("bar"));
					

the result is:

C:\home\zaikin\foo\bar
					

Passing an absolute path to the resolve method returns the passed-in path:

Path p4 = Paths.get("bar");
System.out.format("%s%n", p4.resolve("C:\\home\\zaikin\\foo"));
					

the result is:

C:\home\zaikin\foo
					



Use the Files class to check, delete, copy, or move a file or directory

The java.nio.file.Files class consists of static methods that use Path objects to work with files and directories.

With file I/O, unexpected conditions are a fact of life: a file exists (or doesn't exist) when expected, the program does not have access to the file system, the default file system implementation does not support a particular function, and so on. All methods that access the file system can throw an IOException. It is best practice to catch these exceptions by embedding these methods into a try-with-resources statement, introduced in the Java SE 7 release.

In addition to IOException, many specific exceptions extend FileSystemException. This class has some useful methods that return the file involved (getFile), the detailed message string (getMessage), the reason why the file system operation failed (getReason), and the "other" file involved, if any (getOtherFile).

Several Files methods accept an arbitrary number of arguments when flags are specified. For example, in the following method signature, the ellipses notation after the CopyOption argument indicates that the method accepts a variable number of arguments, or varargs, as they are typically called:

Files.move(Path, Path, CopyOption...);
					

When a method accepts a varargs argument, you can pass it a comma-separated list of values or an array (CopyOption[]) of values:

import static java.nio.file.StandardCopyOption.*;
...
Path source = ...;
Path target = ...;
Files.move(source, target, REPLACE_EXISTING, ATOMIC_MOVE);
					

Copy a file

To copy one file to another you would use the Files.copy method:

Files.copy(Path source, Path target, CopyOption... options);
					

The options argument are enums that specify how the file should be copied. There are actually two different Enum classes, LinkOption and StandardCopyOption, but both implement the CopyOption interface.

Here is the list of available options for Files.copy:

  • LinkOption.NOFOLLOW_LINKS

    Indicates that symbolic links should NOT be followed. If the file to be copied is a symbolic link, the link is copied (and not the target of the link).

  • StandardCopyOption.COPY_ATTRIBUTES

    Copies the file attributes associated with the file to the target file. The exact file attributes supported are file system and platform dependent, but last-modified-time is supported across platforms and is copied to the target file.

  • StandardCopyOption.REPLACE_EXISTING

    Performs the copy even when the target file already exists. If the target is a symbolic link, the link itself is copied (and not the target of the link). If the target is a non-empty directory, the copy fails with the FileAlreadyExistsException exception.

There is also a StandardCopyOption.ATOMIC_MOVE enum, but if this option is specified, an UsupportedOperationException is thrown. If no options are specified, the default is to throw an error if the target file exists or is a symbolic link.

Move a file

You can move a file by using the following method:

Files.move(Path source, Path target, CopyOption... options);
					

The available StandardCopyOptions enums available are:

  • StandardCopyOption.REPLACE_EXISTING

    Performs the move even when the target file already exists. If the target is a symbolic link, the symbolic link is replaced but what it points to is not affected.

  • StandardCopyOption.ATOMIC_MOVE

    Performs the move as an atomic file operation. If the file system does not support an atomic move, an exception is thrown. With an ATOMIC_MOVE you can move a file into a directory and be guaranteed that any process watching the directory accesses a complete file.

If Files.move(...) is called with StandardCopyOption.COPY_ATTRIBUTES an UnsupportedOperationException is thrown.

Reading, Writing, and Creating Files

  1. Commonly used methods for small files

    If you have a small file and you would like to read its entire contents in one pass, you can use the readAllBytes(Path) or readAllLines(Path, Charset) method. These methods take care of most of the work for you, such as opening and closing the stream, but are not intended for handling large files. The following code shows how to use the readAllBytes method:

    Path file = ...;
    byte[] fileArray;
    fileArray = Files.readAllBytes(file);
    								

  2. Buffered I/O methods for text files

    The java.nio.file package supports channel I/O, which moves data in buffers, bypassing some of the layers that can bottleneck stream I/O.

    The newBufferedReader(Path, Charset) method opens a file for reading, returning a BufferedReader that can be used to read text from a file in an efficient manner.

    The following code snippet shows how to use the newBufferedReader method to read from a file. The file is encoded in "US-ASCII"

    Charset charset = Charset.forName("US-ASCII");
    try (BufferedReader reader = Files.newBufferedReader(file, charset)) {
        String line = null;
        while ((line = reader.readLine()) != null) {
            System.out.println(line);
        }
    } catch (NoSuchFileException  x ) {
        System.err.format("No such file: %s", x.getFile());
    } catch (IOException  x) {
        System.err.println(x);
    }
    								

    You can use the newBufferedWriter(Path, Charset, OpenOption...) method to write to a file using a BufferedWriter:

    /**
     * Opens or creates a file for writing, returning a BufferedWriter that may be used to write text to the
     * file in an efficient manner. The options parameter specifies how the the file is created or opened.
     * If no options are present then this method works as if the CREATE, TRUNCATE_EXISTING, and WRITE options
     * are present. In other words, it opens the file for writing, creating the file if it doesn't exist, or
     * initially truncating an existing regular-file to a size of 0 if it exists.
     *
     * The Writer methods to write text throw IOException if the text cannot be encoded using the specified charset.
     */
    public static BufferedWriter newBufferedWriter(Path path,
                                                   Charset cs,
                                                   OpenOption... options) throws IOException
    								

    The following code snippet shows how to create a file encoded in "US-ASCII" using this method:

    Charset charset = Charset.forName("US-ASCII");
    String s = "ABC";
    try (BufferedWriter writer = Files.newBufferedWriter(file, charset)) {
        writer.write(s, 0, s.length());
    } catch (IOException x) {
        System.err.format("IOException: %s%n", x);
    }
    								

  3. Methods for unbuffered streams and interoperable with java.io APIs

    To open a file for reading, you can use the newInputStream(Path, OpenOption...) method. This method returns an unbuffered input stream for reading bytes from the file.

    try (InputStream in = Files.newInputStream(file);
        BufferedReader reader = new BufferedReader(new InputStreamReader(in))) {
        String line = null;
        while ((line = reader.readLine()) != null) {
            System.out.println(line);
        }
    } catch (NoSuchFileException  x) {
        System.err.println("No such file exists: " + x.getFile());
    } catch (IOException  x) {
        System.err.println(x);
    }
    								

Several of the Files methods take an optional OpenOptions parameter. This parameter is optional and the API tells you what the default behavior is for the method when none is specified. The following StandardOpenOptions enum constants are supported:

  • APPEND - If the file is opened for WRITE access then bytes will be written to the end of the file rather than the beginning. If the file is opened for write access by other programs, then it is file system specific if writing to the end of the file is atomic.

  • CREATE - Create a new file if it does not exist. This option is ignored if the CREATE_NEW option is also set. The check for the existence of the file and the creation of the file if it does not exist is atomic with respect to other file system operations.

  • CREATE_NEW - Create a new file, failing if the file already exists. The check for the existence of the file and the creation of the file if it does not exist is atomic with respect to other file system operations.

  • DELETE_ON_CLOSE - Delete on close. When this option is present then the implementation makes a best effort attempt to delete the file when closed by the appropriate close method. If the close method is not invoked then a best effort attempt is made to delete the file when the Java virtual machine terminates (either normally, as defined by the Java Language Specification, or where possible, abnormally). This option is primarily intended for use with work files that are used solely by a single instance of the Java virtual machine. This option is not recommended for use when opening files that are open concurrently by other entities. Many of the details as to when and how the file is deleted are implementation specific and therefore not specified. In particular, an implementation may be unable to guarantee that it deletes the expected file when replaced by an attacker while the file is open. Consequently, security sensitive applications should take care when using this option.

  • DSYNC - Requires that every update to the file's content be written synchronously to the underlying storage device.

  • READ - Open for read access.

  • SPARSE - Sparse file. When used with the CREATE_NEW option then this option provides a hint that the new file will be sparse. The option is ignored when the file system does not support the creation of sparse files.

  • SYNC - Requires that every update to the file's content or metadata be written synchronously to the underlying storage device.

  • TRUNCATE_EXISTING - If the file already exists and it is opened for WRITE access, then its length is truncated to 0. This option is ignored if the file is opened only for READ access.

  • WRITE - Open for write access.

Glob argument

The newDirectoryStream method in the Files class accepts a glob argument:


/**
 * Opens a directory, returning a DirectoryStream to iterate over the entries in the directory.
 * The elements returned by the directory stream's iterator are of type Path, each one representing
 * an entry in the directory. The Path objects are obtained as if by resolving the name of the
 * directory entry against dir. The entries returned by the iterator are filtered by matching the
 * String representation of their file names against the given globbing pattern.
 */
public static DirectoryStream<Path> newDirectoryStream(Path dir, String glob) throws IOException;

					

You can use glob syntax to specify pattern-matching behavior. A glob pattern is specified as a string and is matched against other strings, such as directory or file names. For example:

  • *.java - Matches a path that represents a file name ending in .java

  • *.* - Matches file names containing a dot

  • *.{java,class} - Matches file names ending with .java or .class

  • foo.? - Matches file names starting with foo. and a single character extension

  • /home/*/* - Matches /home/mikalai/data on UNIX platforms

  • /home/** - Matches /home/mikalai and /home/mikalai/data on UNIX platforms

  • C:\\* - Matches C:\foo and C:\bar on the Windows platform (note that the backslash is escaped; as a string literal in the Java Language the pattern would be "C:\\\\*")

Creating temporary files

You can create a temporary file using one of the following Files.createTempFile methods:


/**
 * Creates a new empty file in the specified directory, using the given prefix and suffix strings to generate
 * its name. The resulting Path is associated with the same FileSystem as the given directory.
 *
 * The details as to how the name of the file is constructed is implementation dependent and therefore not specified.
 * Where possible the prefix and suffix are used to construct candidate names in the same manner as the
 * File.createTempFile(String,String,File) method.
 *
 * As with the File.createTempFile methods, this method is only part of a temporary-file facility. Where used as a work
 * files, the resulting file may be opened using the DELETE_ON_CLOSE option so that the file is deleted when the appropriate
 * close method is invoked. Alternatively, a shutdown-hook, or the File.deleteOnExit() mechanism may be used to delete the
 * file automatically.
 *
 * The attrs parameter is optional file-attributes to set atomically when creating the file. Each attribute is identified by
 * its name. If more than one attribute of the same name is included in the array then all but the last occurrence is ignored.
 * When no file attributes are specified, then the resulting file may have more restrictive access permissions to files
 * created by the File.createTempFile(String,String,File) method.
 */
 public static Path createTempFile(Path dir,
                  String prefix,
                  String suffix,
                  FileAttribute<?>... attrs)
                           throws IOException;

/**
 * Creates an empty file in the default temporary-file directory, using the given prefix and suffix to generate its name.
 * The resulting Path is associated with the default FileSystem.
 *
 * This method works in exactly the manner specified by the createTempFile(Path,String,String,FileAttribute[]) method for
 * the case that the dir parameter is the temporary-file directory.
 */
 public static Path createTempFile(String prefix,
                  String suffix,
                  FileAttribute<?>... attrs)
                           throws IOException;

					

The first method allows the code to specify a directory for the temporary file and the second method creates a new file in the default temporary-file directory. Both methods allow you to specify a suffix for the filename and the first method allows you to also specify a prefix. The following code snippet gives an example of the second method:

try {
    Path tempFile = Files.createTempFile(null, ".myapp");
    System.out.format("The temporary file: %s%n", tempFile);
} catch (IOException x) {
    System.err.format("IOException: %s%n", x);
}
					

The result of running this file would be something like the following:

The temporary file: C:\DOCUME~1\zaikin\LOCALS~1\Temp\2410322004839770958.myapp
					

Another example:

 try {
    Path tempFile = Files.createTempFile("mz", ".myapp");
     System.out.format("The temporary file: %s%n", tempFile);
} catch (IOException x) {
    System.err.format("IOException: %s%n", x);
}
					

produces:

The temporary file: C:\DOCUME~1\zaikin\LOCALS~1\Temp\mz7254193327871146359.myapp
					

A temporary file is just a simple file until YOU make sure that it is truly temporary, which means that an automatic mechanism must delete temporary files periodically or at a specified time. There are three approaches to automatic cleanup of temporary files:

  • automatic file cleanup via File.deleteOnExit()

  • automatic file cleanup via shutdown-hook: Runtime.getRuntime().addShutdownHook(new Thread() { ... });

  • automatic file cleanup via StandardOpenOption.DELETE_ON_CLOSE



Read and change file and directory attributes

Java 7 NIO.2 API offers access many file attributes. 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. Java 7 allows 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.

The following attribute views are available in the new Java 7 NIO.2 API, 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.

  1. AclFileAttributeView and AclEntry - Supports reading or updating a file's Access Control Lists (ACL). The NFSv4 ACL model is supported. Any ACL model, such as the Windows ACL model, that has a well-defined mapping to the NFSv4 model might also be supported.

    The 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.

  2. BasicFileAttributeView and BasicFileAttributes - Provides a view of basic attributes that are required to be supported by all file system implementations.

    This view class allows you to get a set of 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.

    To get a file attribute view for a particular file we start by creating a Path object for the file we're interested in:

    Path file = Paths.get("C:\\home\\zaikin\\foo\\test.txt");
     								

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

    BasicFileAttributeView basicView = Files.getFileAttributeView(file, BasicFileAttributeView.class);
     								

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

    BasicFileAttributes basicAttrs = basicView.readAttributes();
     								

    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 would not 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 newTime = FileTime.fromMillis(basicAttrs.lastModifiedTime().toMillis() + 60000);
    basicView.setTimes(newTime, 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.

  3. DosFileAttributeView and DosFileAttributes - Extends the basic attribute view with the standard four bits supported on file systems that support the DOS attributes.

    This view class allows you to get attributes specific to DOS. 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 setXXX(boolean) methods for each of these properties.

    However, you can set a DOS attribute using the Files.setAttribute(Path, String, Object, LinkOption...) method, as follows:

    Path file = ...;
    Files.setAttribute(file, "dos:hidden", true);
    								

  4. FileOwnerAttributeView and UserPrincipal - Supported by any file system implementation that supports the concept of a file owner.

    This view class allows you to get and set the owner of a particular file. Its getOwner() method returns a java.nio.file.attribute.UserPrincipal, 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.

  5. FileStoreSpaceAttributeView and FileStoreSpaceAttributes

    This view 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.

  6. PosixFileAttributeView and PosixFileAttributes - Extends the basic attribute view with attributes supported on file systems that support the POSIX family of standards, such as UNIX. These attributes include file owner, group owner, and the nine related access permissions.

    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.

  7. UserDefinedFileAttributeView and String - Enables support of metadata that is user defined. This view can be mapped to any extension mechanisms that a system supports. In the Solaris OS, for example, you can use this view to store the MIME type of a file.

    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, so you can store any binary data in there you want.

    First, we will get the attribute view:

    Path file = Paths.get("C:\\home\\zaikin\\foo\\test.txt");
    UserDefinedFileAttributeView userView = Files.getFileAttributeView(file, UserDefinedFileAttributeView.class);
    								

    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:

    String attribName  = "COPYRIGHT";
    String attribValue = "MIKALAI.ZAIKIN";
    userView.write(attribName, Charset.defaultCharset().encode(attribValue));
    								

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

    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:

    for (String s : attribList) {
        System.out.print(String.format("User defined attribute: %s", s));
        ByteBuffer buf = ByteBuffer.allocate(userView.size(s));
        userView.read(s, buf);
        buf.flip();
        String value = Charset.defaultCharset().decode(buf).toString();
        System.out.println(String.format("; value: %s", value));
    }
    								

    The output will be as follows:

    User defined attribute: COPYRIGHT; value: MIKALAI.ZAIKIN
    								

File store attributes

You can use the FileStore class to learn information about a file store, such as how much space is available. The getFileStore(Path) method fetches the file store for the specified file.

The following code snippet prints the space usage for the file store where a particular file resides:

Path file = ...;
FileStore store = Files.getFileStore(file);

long total = store.getTotalSpace()  / 1024 / 1024;
long avail = store.getUsableSpace() / 1024 / 1024;
long used = (store.getTotalSpace() - store.getUnallocatedSpace()) / 1024 / 1024;

System.out.print(String.format("Root : %s%n", file.getRoot()));
System.out.print(String.format("Store name: %s%n", store.name()));
System.out.print(String.format("Total: %d MB; used: %d MB; available: %d MB", total, used, avail));
					



Recursively access a directory tree

The new FileVisitor Java 7 NIO.2 API allows you to implement 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:


public interface FileVisitor<T> {

    FileVisitResult preVisitDirectory(T dir, BasicFileAttributes attrs) throws IOException;

    FileVisitResult visitFile(T file, BasicFileAttributes attrs) throws IOException;

    FileVisitResult visitFileFailed(T file, IOException exc) throws IOException;

    FileVisitResult postVisitDirectory(T dir, IOException exc) throws IOException;
}


					

public enum FileVisitResult {
    /**
     * Continue. When returned from a FileVisitor#preVisitDirectory method then the entries in the
     * directory should also be visited.
     */
    CONTINUE,

    /**
     * Terminate.
     */
    TERMINATE,

    /**
     * Continue without visiting the entries in this directory. This result is only meaningful when
     * returned from the FileVisitor#preVisitDirectory method; otherwise this result type is the
     * same as returning CONTINUE.
     */
    SKIP_SUBTREE,

    /**
     * Continue without visiting the siblings of this file or directory. If returned from the
     * FileVisitor#preVisitDirectory method then the entries in the directory are also skipped
     * and the FileVisitor#postVisitDirectory method is not invoked.
     */
    SKIP_SIBLINGS;
}

					

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 four 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, BasicFileAttributes attrs) 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.

    If this method returns FileVisitResult.CONTINUE, then entries in the directory are visited. If this method returns FileVisitResult.SKIP_SUBTREE or FileVisitResult.SKIP_SIBLINGS then entries in the directory (and any descendants) will not be visited.

  • FileVisitResult visitFile(T file, BasicFileAttributes attrs) is called when a file in the current directory is being visited. The attributes for this file are passed into the second parameter.

  • FileVisitResult visitFileFailed(T file, IOException exc) is called when the visit to a file has failed. The second parameter specifies the exception that caused the visit to fail.

    This method is invoked if the file's attributes could not be read, the file is a directory that could not be opened, and other reasons.

  • FileVisitResult postVisitDirectory(T dir, IOException exc) 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 provided an implementation of the FileVisitor interface: java.nio.file.SimpleFileVisitor. This class is as basic as it gets: for the xxxxxFailed() methods, it just rethrows the exception, and for the other methods it continues without doing anything at all. What is 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.

Code below shows how to create anonymous inner FileVisitor instance:


FileVisitor<Path> myFileVisitor = new SimpleFileVisitor<Path>() {
    @Override
    public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) {
        System.out.println(String.format("Before visit the '%s' directory", dir));
        return FileVisitResult.CONTINUE;
    }

    @Override
    public FileVisitResult visitFile(Path file, BasicFileAttributes attribs) {
        System.out.println(String.format("Visiting file '%s' which has size %d bytes", file, attribs.size()));
        return FileVisitResult.CONTINUE;
    }
};

					

The FileVisitor implementation from code above 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.file.Paths class:

Path headDir = Paths.get("C:\\home");
					

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

  • public static Path walkFileTree(Path start, FileVisitor<? super Path> visitor) throws IOException

    Walks the file tree under the head directory, invoking the callback methods implemented in FileVisitor as it goes.

    It does not follow symbolic links, and visits all levels of the file tree.

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

    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 will use the simpler version of the walkFileTree(...) method to start the process of walking the file tree:

Files.walkFileTree(headDir, myFileVisitor);
					

it shows the output similar to this:

Before visit the 'C:\home' directory
Before visit the 'C:\home\zaikin' directory
Before visit the 'C:\home\zaikin\foo' directory
Before visit the 'C:\home\zaikin\foo\company' directory
Visiting file 'C:\home\zaikin\foo\company\readme.txt' which has size 2 bytes
Before visit the 'C:\home\zaikin\foo\sample' directory
Visiting file 'C:\home\zaikin\foo\test.txt' which has size 7 bytes
					

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.



Find a file by using the PathMatcher class

The new Java 7 NIO.2 java.nio.file package provides programmatic support for files locating by pattern matching. Each file system implementation provides a PathMatcher interface implementation:

public interface PathMatcher {

    /**
     * Tells if given path matches this matcher's pattern.
     */
    boolean matches(Path path);

}
					

You can retrieve a file system's PathMatcher by using the getPathMatcher(String) method in the FileSystem class.

You can get the path matcher from the file system using this code:

FileSystem fs = FileSystems.getDefault();
PathMatcher matcher = fs.getPathMatcher("glob:" + pattern);
					

The string argument passed to getPathMatcher specifies the syntax flavor and the pattern to be matched. This example specifies glob syntax.

At least two syntaxes are supported (others may be supported by a particular file system implementation):

  • glob

    When the syntax is "glob" then the String representation of the path is matched using a limited pattern language that resembles regular expressions but with a simpler syntax. For example:

    • *.java - Matches a path that represents a file name ending in .java

      The * character matches zero or more characters of a name component without crossing directory boundaries.

    • *.* - Matches file names containing a dot

    • *[0-9]* - Matches file names containing a numeric value.

      The [ ] characters are a bracket expression that match a single character of a name component out of a set of characters. For example, [abc] matches "a", "b", or "c". The hyphen (-) may be used to specify a range so [a-z] specifies a range that matches from "a" to "z" (inclusive).

      These forms can be mixed so [abce-g] matches "a", "b", "c", "e", "f" or "g". If the character after the [ is a ! then it is used for negation so [!a-c] matches any character except "a", "b", or "c".

    • *.{java,class} - Matches file names ending with .java or .class

      The { } characters are a group of subpatterns, where the group matches if any subpattern in the group matches. The "," character is used to separate the subpatterns. Groups cannot be nested.

    • foo.? - Matches file names starting with foo. and a single character extension

      The ? character matches exactly one character of a name component.

    • /home/*/* - Matches /home/mikalai/data on UNIX platforms

      The * character matches zero or more characters of a name component without crossing directory boundaries.

    • /home/** - Matches /home/mikalai and /home/mikalai/data on UNIX platforms

      The ** characters matches zero or more characters crossing directory boundaries.

    • C:\\* - Matches C:\foo and C:\bar on the Windows platform (note that the backslash is escaped; as a string literal in the Java Language the pattern would be "C:\\\\*")

  • regex

    When the syntax is "regex" then the pattern component is a regular expression as defined by the java.util.regex.Pattern class.

For both the glob and regex syntaxes, the matching details, such as whether the matching is case sensitive, are implementation-dependent and therefore not specified.

If you want to use some other form of string-based pattern matching, you can create your own PathMatcher class.

Once you have created your PathMatcher instance, you are ready to match files against it. The PathMatcher interface has a single method, matches(...), that takes a Path argument and returns a boolean: it either matches the pattern, or it does not. The following code snippet looks for files that end in .txt and .doc and prints those files to standard output:


Path startDir = Paths.get("C:\\home");

String pattern = "*.{txt,doc}";

FileSystem fs = FileSystems.getDefault();
final PathMatcher matcher = fs.getPathMatcher("glob:" + pattern);

FileVisitor<Path> matcherVisitor = new SimpleFileVisitor<Path>() {
    @Override
    public FileVisitResult visitFile(Path file, BasicFileAttributes attribs) {
        Path name = file.getFileName();
        if (matcher.matches(name)) {
            System.out.print(String.format("Found matched file: '%s'.%n", file));
        }
        return FileVisitResult.CONTINUE;
    }
};
Files.walkFileTree(startDir, matcherVisitor);

					

the output could be as follows:

Found matched file: 'C:\home\zaikin\foo\company\readme.txt'.
Found matched file: 'C:\home\zaikin\foo\resume.doc'.
Found matched file: 'C:\home\zaikin\foo\test.txt'.
					



Watch a directory for changes by using WatchService

The new NIO.2 Java 7 WatchService allows you 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 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. In most cases you will 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:

Path watchDir = Paths.get("C:\\home");
					

The Path class implements the java.nio.file.Watchable interface, and that interface defines two register(...) methods:


public interface Watchable {

	WatchKey register(WatchService watcher, WatchEvent.Kind<?>[] events, WatchEvent.Modifier... modifiers) throws IOException;

	WatchKey register(WatchService watcher, WatchEvent.Kind<?>... events) throws IOException;

}

					

The 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.StandardWatchEventKinds class defines four static implementations of WatchEvent.Kind that can be used in the register(...) calls:

  • StandardWatchEventKinds.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.

    The event count for this event is always 1.

  • StandardWatchEventKinds.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.

    The event count for this event is 1 or greater.

  • StandardWatchEventKinds.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.

    The event count for this event is always 1.

  • StandardWatchEventKinds.OVERFLOW indicates that events may have been lost or discarded. You DO NOT have to register for the OVERFLOW event to receive it.

    The event count may be greater than 1.

For example, let's watch the ENTRY_CREATE and ENTRY_MODIFY events:

WatchKey watchKey = watchDir.register(watchService, StandardWatchEventKinds.ENTRY_CREATE, StandardWatchEventKinds.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.

The register(...) method call returns a WatchKey class instance. This class represents your registration with the WatchService. Whether you store this reference or not is up to you, because the WatchService returns the relevant WatchKey to you when an event is triggered. When you are 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 WatchService.poll() - returns and removes the next WatchKey that has had some of its events occur, or null if no registered events have happened.

  • WatchKey WatchService.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 and removing the relevant WatchKey. If there are no WatchKeys to return by the end of the timeout, this method returns null.

  • WatchKey WatchService.take() - similar to the preceding methods (returns and removes the next WatchKey), 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 you put the key back into a Ready state by invoking its reset() method, even if events it is registered for occur.

A WatchKey has a state. At any given time, its state might be one of the following:

  • Ready - indicates that the key is ready to accept events. When first created, a key is in the ready state.

  • Signalled - indicates that one or more events are queued. Once the key has been signalled, it is no longer in the ready state until the reset() method is invoked.

  • Invalid - indicates that the key is no longer active. This state happens when one of the following events occurs:

    • The process explicitly cancels the key by using the cancel() method.

    • The directory becomes inaccessible.

    • The watch service is closed.

Once a WatchKey is returned by the WatchService, you can inspect and remove all its pending events that have been triggered by calling the WatchKey's pollEvents() method, which returns a List of WatchEvents.

Simple example:


// Create a file inside our watched directory
File newFile = new File(watchDir.toFile(), "newFile.txt");
newFile.createNewFile();

// Call take() and see if the event has been registered
WatchKey key = watchService.take();
for (WatchEvent<?> event : key.pollEvents()) {
    System.out.print(String.format("An event was found of kind %s.%n", event.kind()));
    System.out.print(String.format("The event occurred on file '%s'.%n", event.context()));
}

// Reset the key - this step is critical if you want to receive further watch events !!!
key.reset();

					

output is:

An event was found of kind ENTRY_CREATE.
The event occurred on file 'newFile.txt'.
					

As you can see, we got the ENTRY_CREATE event for the newly created newFile.txt as expected.

The Watch Service API is designed for applications that need to be notified about file change events. It is well suited for any application, like an editor or IDE, that potentially has many open files and needs to ensure that the files are synchronized with the file system.

This API is NOT designed for indexing a hard drive. Most file system implementations have native support for file change notification. The Watch Service API takes advantage of this support where available. However, when a file system does not support this mechanism, the Watch Service will poll the file system, waiting for events.