1. Overview

In this article, we are going to explore an interesting feature of NIO2 – the FileVisitor interface.

All operating systems and several third party applications have a file search function where a user defines search criteria.

This interface is what we need to implement such a functionality in a Java application. Should you need to search for all .mp3 files, find and delete .class files or find all files that haven’t been accessed in the last month, then this interface is what you need.

All the classes we need to implement this functionality are bundled in one package:

import java.nio.file.*;

2. How FileVisitor Works

With the FileVisitor interface, you can traverse the file tree to any depth and perform any action on the files or directories found on any branch.

A typical implementation of the FileVisitor interface looks like this:

public class FileVisitorImpl implements FileVisitor<Path> {

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

    @Override
    public FileVisitResult visitFile(
      Path file, BasicFileAttributes attrs) {
        return null;
    }

    @Override
    public FileVisitResult visitFileFailed(
      Path file, IOException exc) {       
        return null;
    }

    @Override
    public FileVisitResult postVisitDirectory(
      Path dir, IOException exc) {    
        return null;
    }
}

The four interface methods allow us to specify the required behavior at key points in the traversal process: before a directory is accessed, when a file is visited, or when a failure occurs and after a directory is accessed respectively.

The return value at each stage is of type FileVisitResult and controls the flow of the traversal. Perhaps you want to walk the file tree looking for a particular directory and terminate the process when it is found or you want to skip specific directories or files.

FileVisitResult is an enum of four possible return values for the FileVisitor interface methods:

  • FileVisitResult.CONTINUE – indicates that the file tree traversal should continue after the method returning it exits
  • FileVisitResult.TERMINATE – stops the file tree traversal and no further directories or files are visited
  • FileVisitResult.SKIP_SUBTREE – this result is only meaningful when returned from the preVisitDirectory API, elsewhere, it works like CONTINUE. It indicates that the current directory and all its subdirectories should be skipped
  • FileVisitResult.SKIP_SIBLINGS – indicates that traversal should continue without visiting the siblings of the current file or directory. If called in the preVisitDirectory phase, then even the current directory is skipped and the postVisitDirectory is not invoked

Finally, there has to be a way to trigger the traversal process, perhaps when the user clicks the search button from a graphical user interface after defining search criteria. This is the simplest part.

We just have to call the static walkFileTree API of the Files class and pass to it an instance of Path class which represents the starting point of the traversal and then an instance of our FileVisitor:

Path startingDir = Paths.get("pathToDir");
FileVisitorImpl visitor = new FileVisitorImpl();
Files.walkFileTree(startingDir, visitor);

3. File Search Example

In this section, we are going to implement a file search application using the FileVisitor interface. We want to make it possible for the user to specify the complete file name with extension and a starting directory in which to look.

When we find the file, we print a success message to the screen and when the entire file tree is searched without the file being found, we also print an appropriate failure message.

3.1. The Main Class

We will call this class FileSearchExample.java:

public class FileSearchExample implements FileVisitor<Path> {
    private String fileName;
    private Path startDir;

    // standard constructors
}

We are yet to implement the interface methods. Notice that we have created a constructor which takes the name of the file to search for and the path to start searching from. We will only use the starting path as a base case to conclude that the file has not been found.

In the following subsections, we will implement each interface method and discuss it’s role in this particular example application.

3.2. The preVisitDirectory API

Let’s start by implementing the preVisitDirectory API:

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

As we say earlier, this API is called each time the process encounters a new directory in the tree. Its return value determines what will happen next depending on what we decide. This is the point at which we would skip specific directories and eliminate them from the search sample space.

Let’s choose not to discriminate any directories and just search in all of them.

3.3. The visitFile API

Next, we will implement the visitFile API. This is where the main action happens. This API is called everytime a file is encountered. We take advantage of this to check the file attributes and compare with our criteria and return an appropriate result:

@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
    String fileName = file.getFileName().toString();
    if (FILE_NAME.equals(fileName)) {
        System.out.println("File found: " + file.toString());
        return TERMINATE;
    }
    return CONTINUE;
}

In our case, we are only checking the name of the file being visited to know if it’s the one the user is searching for. If the names match, we print a success message and terminate the process.

However, there is so much one can do here, especially after reading the File Attributes section. You could check created time, last modified time or last accessed times or several attributes available in the attrs parameter and decide accordingly.

3.4. The visitFileFailed API

Next, we will implement the visitFileFailed API. This API is called when a specific file is not accessible to the JVM. Perhaps it has been locked by another application or it could just be a permission issue:

@Override
public FileVisitResult visitFileFailed(Path file, IOException exc) {
    System.out.println("Failed to access file: " + file.toString());
    return CONTINUE;
}

We simply log a failure message and continue with traversal of the rest of the directory tree. Within a graphical application, you could choose to ask the user whether to continue or not using a dialog box or just log the message somewhere and compile a report for later use.

3.5. The postVisitDirectory API

Finally, we will implement the postVisitDirectory API. This API is called each time a directory has been fully traversed:

@Override
public FileVisitResult postVisitDirectory(Path dir, IOException exc){
    boolean finishedSearch = Files.isSameFile(dir, START_DIR);
    if (finishedSearch) {
        System.out.println("File:" + FILE_NAME + " not found");
        return TERMINATE;
    }
    return CONTINUE;
}

We use the Files.isSameFile API to check if the directory that has just been traversed is the directory where we started traversal from. If the return value is true, that means the search is complete and the file has not been found. So we terminate the process with a failure message.

However, if the return value is false, that means we just finished traversing a subdirectory and there is still a probability of finding the file in some other subdirectory. So we continue with traversal.

We can now add our main method to execute the FileSearchExample application:

public static void main(String[] args) {
    Path startingDir = Paths.get("C:/Users/user/Desktop");
    String fileToSearch = "hibernate-guide.txt"
    FileSearchExample crawler = new FileSearchExample(
      fileToSearch, startingDir);
    Files.walkFileTree(startingDir, crawler);
}

You can play around with this example by changing the values of startingDir and fileToSearch variables. When fileToSearch exists in startingDir or any of its subdirectories, then you will get a success message, else, a failure message.

4. Conclusion

In this article, we have explored some of the less commonly used features available in the Java 7 NIO.2 filesystem APIs, particularly the FileVisitor interface. We have also managed to go through the steps of building a file search application to demonstrate its functionality.

The full source code for the examples used in this article is available in the Github project.