Wednesday, 10 June 2015

HOW TO GET NOTIFIED OF EVENTS ON A DIRECTORY - USING JAVA 7 WatchService - A NIO.2 Feature

Java 7 introduced New Input/Output 2 (NIO.2) API, which provides a new I/O API.
The features provided in NIO.2 are essential for working with a file system efficiently.

Out of any many NIO.2 API features, In this post - I am going to discuss a feature  known as a watch service, which watches for events on a directory such as creating, modifying, and deleting a file, etc. So using this watch service - a java program can receives a notification of such events.

The watch service uses a native file event notification facility of the file system. If a file system does not provide a file event notification facility, it may use other mechanisms such as polling.

How it works?

In a nutshell, first we need to create a watch service and register a directory path using Path object (a typical directory on file system) on that watch service. When an object is registered - it is given a watch key as an identity of registration. Initially, the watch key is in a ready state. When watch service detects any changes for the registered object - it then signals and queues the watch key. Then using the watch key methods - we can retrieve the pending events and process the events. Once a watch key is in this state, it always remains in this state until we invoke the reset() method of watch key to bring it to ready state again. Once after all events are processed you need to close the watch service.

Now lets discuss this in a more detail way.

Before diving into code specifics, it is worth knowing the core interfaces and classes.

Some of the important interfaces and classes used in watch service are listed below. All these interfaces & classes are from java.nio.file package.

Important Interfaces & Classes:

WatchService interface

A WatchService represents a service that watches registered objects for changes. Generally, we will register a directory path using Path object.

Syntax: Path <<var>> = Paths.get("<<PATH_STRING>>")

For instance, a directory path can be like this:

In Windows

Path directoryPath = Paths.get("c:\\dinesh\\watchme")

In Linux/UNIX

Path directoryPath = Paths.get("/home/dinesh/watchme")

Here, Path is an object that may be used to locate a file/directory in a file system. It typically represent a system dependent file path.

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

Watchable interface

A Watchable  object represents a file-system object (eg: directory) that can be watched for changes. A Watchable object can be registered with a watch service. A Path object is a watchable object.

In the above example, Path objects declared are considered as Watchable Objects.

WatchKey  interface

WatchKey identifies a registered object with a watch service. Whenever an object is registered with a watch service - WatchService returns a WatchKey that acts as a registration token.

WatchEvent<T> interface

Represent an event or repeated event for a file-system object that is registered with a WatchService.

Note: WatchEvent interface has a method count() returns the number of times an event occured. If the event count is greater than 1 then it is considered as a repeated event.

WatchEvent.Kind<T> interface ( An event kind )

WatchEvent.Kind<T> is an inner interface of WathEvent interface. Any event that occurs on a registered Path object must be of type WatchEvent.Kind<T>.

StandardWatchEventKinds class 

This class defines standard event kinds. It contains four constants to identify an event.

Constants:

ENTRY_CREATE

(Directory entry created)

If a directory is registered for this event then the WatchKey is queued when a directory entry is created or renamed into the directory. For instance, say a new file is created.

ENTRY_DELETE

(Directory entry deleted)

If a directory is registered for this event then the WatchKey is queued when a directory entry is deleted. For instance, say a file is deleted.

ENTRY_MODIFY

(Directory entry modified)

If a directory is registered for this event then the WatchKey is queued when a directory entry is modified. For instance, say a file content is changed in a registered directory.

OVERFLOW

A special event to indicate that events may have been lost or discarded.

File systems may reports faster than they can be retrieved and processed and an
implementation may impose an unspecified limit on number of events that it may accumulate. Where an implementation knowingly discards events then it arranges watch key's pollEvents() method to return an element with an event type of OVERFLOW. This event can be used by the consumer as a trigger to re-examine the state of the object.

All these are of type WatchEvent.Kind<T> .


STEPS FOR CONFIGURING WatchService

STEP 1:

Create a watch service for a file system in which you want a directory to be monitered


                FileSystem fs = FileSystems.getDefault();

                WatchService ws = fs.newWatchService();

                                                     (or)

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

STEP 2:

Register a directory with watch service

// First get a watchable Path object - as aforementioned
Path watchingDir = Paths.get("/home/dinesh/tempdir");

//  Register the above Path object - returns a WatchKey
WatchKey wkey = watchingDir. register( ws, ENTRY_MODIFY, ENTRY_CREATE);

register() method signature is like this:

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

In the above example, I want the watch service to monitor for only creation and modification events - but not deletion events.

STEP 3: 

Retrieve a watch key from the watch service queue

To retrieve a watch key we use either a poll() or take() methods of watch service.

A Watch key has a state. When initially created the watch key is said to be in ready state. When an event is detected the key is signalled and queued so that it can be retrieved by invoking WatchService's poll() or take() methods.

take() method waits until a WatchKey is available.

Eg:

// keep watching for events - an infinite loop
while(true){

 //to retrieve a watch key
WatchKey key = ws.take();

 }
                                                  (or)

poll() method lets you to specify the timeout for wait.

Eg:

WatchKey key = ws.poll(60, TimeUnit.SECONDS);

pollEvents() method - retrives and removes all pending events for this
watch key, returning a List of the events that were retrieved.


STEP 4:

Process events that occurred on the registered directory.

Once after getting a WatchKey - you can use a pollEvents() method to retrieve the events.


// retrieving events using pollEvents() method
for (WatchEvent<?> event : key.pollEvents()) {

         //process the events
}

Important method  of WatchEvent<?>: context()

Context() method returns a Path object that is relative path between the directory registered with the watch service, and the entry that is created, deleted, or modified.

For Example: registered path is /home/dinesh/tempdir

If I delete a file xyz.txt which is in the tempdir directory - then context() method returns the xyz as Path object, which is a relative path. useful to know which file is changed, created, or deleted.


STEP 5:

 Reset the watch key after processing the events.

To bring back the WatchKey to ready state again. Note: Whenever an event is detected - WatchKey is signalled and queued and it is in the same state unitl it reseted using reset() method.

reset() method returns true if the watch key is still valid. Otherwise, it returns false.

A WatchKey would be invalid if a watch service is closed or a watch key is cancelled using its cancel() method.

//Reset the watch key - bringing into ready state
boolean iskeyvalid = key.reset();

Based on the boolean value- we can know the watch service is still watching or not.

                if (!isKeyValid) {
                    System.out.println("No longer watching " + dirToWatch);
                    break;
                }

STEP 6:

Close the watch service.

WatchService is a AutoCloseable. So use java 7 try-with-resource statement for WatchService. By using this statement - you don't need to close the watch service explicitly. Otherwise, you can use close() method to close the watch service.

 Code Sample:

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.WatchEvent.Kind;
import java.nio.file.WatchKey;
import java.nio.file.WatchService;

import static java.nio.file.StandardWatchEventKinds.ENTRY_DELETE;
import static java.nio.file.StandardWatchEventKinds.ENTRY_MODIFY;

public class Watcher {
 public static void main(String[] args) {

  // Step 1: Creating watch service
  try (WatchService ws = FileSystems.getDefault().newWatchService()) {

   /*
    * Step2: Get a Path object for /home/dinesh/tempdir directory to
    * watch
    */
   Path dirToWatch = Paths.get("/home/dinesh/tempdir");

   /*
    * Register the path with the watch service for modify and delete
    * events - doesn't show creating events
    */
   dirToWatch.register(ws, ENTRY_DELETE, ENTRY_MODIFY);

   System.out.println("Watching " + dirToWatch + " for events.");

   /*
    * Keep watching for events on the dirToWatch
    */
   while (true) {

    // Step 3: Retrieve and remove the next available WatchKey
                                // best step for a debug breakpoint
    WatchKey key = ws.take();

    // WatchKey key = ws.poll(60, TimeUnit.SECONDS);

    /*
     * Step 4: Retrieves & remove all pending events in this watch
     * key
     */
    for (WatchEvent event : key.pollEvents()) {

     /*
      * events are of type WatchEvent.Kind these are constants
      * declared in StandardWatchEventKinds ENTRY_DELETE
      * ENTRY_CREATE ENTRY_MODIFY OVERFLOW
      */

     Kind eventKind = event.kind();

     /*
      * Get the context of the event, which is the directory
      * entry on which the event occurred - may be on a file.
      */
     WatchEvent currEvent = (WatchEvent) event;
     Path dirEntry = currEvent.context();

     // Print the event details
     System.out.println(eventKind + " occurred on " + dirEntry);
    }

    /*
     * Step 5: Reset the key - bringing back to ready state so again
     * it ready to be queued for events
     */
    boolean isKeyValid = key.reset();
                                
    //checking whether the watch key is still valid 
    if (!isKeyValid) {
     System.out.println("No longer watching " + dirToWatch);
     break;
    }
   }
  } catch (IOException | InterruptedException e) {
   e.printStackTrace();
  }
 }
}


Source Code: Download