Introduction
In this post, we will learn about the Apache Commons IO FileAlterationObserver. Using the FileAlterationObserver, we can observe the state of files and directories.
Apache Commons IO FileAlterationListener
The FileAlterationListener is an interface which receives the events of file system modifications. It has 8 methods:
- onStart and onStop: These methods take a FileAlterationObserver as a parameter (We will see the FileAlterationObserver shortly).
- onFileCreate, onFileChange, and onFileDelete: These methods are invoked when files are created/changed or deleted, respectively.
- onDirectoryCreate, onDirectoryChange, and onDelete: Similar to above but for directories.
Apache Commons IO FileAlterationObserver
A FileAlterationObserver has one or more of FileAlterationListeners registered. It checks the file system changes and notifies all of the listeners when the state changes. In other words, when files/directories are created/changed/deleted, it invokes the respective method on the FileAlterationListener(s). It thus follows observer design pattern.
The FileAlterationListener
Let us create a class implementing the FileAlterationListener. It just prints a message when it gets an event.
public class FileChangesListener implements FileAlterationListener {
@Override
public void onStart(FileAlterationObserver observer) {
System.out.println("onStart called for observer " + observer);
}
@Override
public void onDirectoryCreate(File directory) {
System.out.println("Directory created "+ directory);
}
@Override
public void onDirectoryChange(File directory) {
System.out.println("Directory changed "+ directory);
}
@Override
public void onDirectoryDelete(File directory) {
System.out.println("Directory deleted "+ directory);
}
@Override
public void onFileCreate(File file) {
System.out.println("File created "+ file);
}
@Override
public void onFileChange(File file) {
System.out.println("File changed "+ file);
}
@Override
public void onFileDelete(File file) {
System.out.println("File deleted "+ file);
}
@Override
public void onStop(FileAlterationObserver observer) {
System.out.println("onStop called for observer " + observer + "\n");
}
}
For onStart and onStop, it adds the FileAlterationObserver (its toString) in the message. For file or directory changes, it prints a message with the file instance.
Observing the file system changes
Now let us create an instance of FileAlterationObserver. The simplest constructor of the FileAlterationObserver takes the root directory which we want to observe. In the below code, we observe the /tmp directory.
We create an instance of the FileChangesListener and add it as a listener by calling the addListener method.
public static void main(String[] args) throws Exception {
FileAlterationListener fileAlterationListener = new FileChangesListener();
String directory = "/tmp";
FileAlterationObserver fileAlterationObserver = new FileAlterationObserver(directory);
fileAlterationObserver.addListener(fileAlterationListener);
// Init - throws ex
fileAlterationObserver.initialize();
fileAlterationObserver.checkAndNotify();
}
We first call the initialize() method. This checks the current content of the directory we are observing (/tmp here) and initializes the observer. If we don’t do this, then for each of the files and folders in the /tmp directory we will get a create event(s) i.e., it will invoke onFileCreate and onDirectoryCreate methods on the configured listener instance(s).
Next we call checkAndNotify() method. This checks the current state of the file system under the root directory and sees if there are any changes since the previous call to checkAndNotify (or initialization in this case). Then for each of the changes, it will invoke the appropriate method on the FileAlterationListener. It does this for each of the registered listeners.
For each listener, first it will call the onStart method passing itself as an argument. Following that, it invokes one or more of create/change/delete methods for files and directories. Finally, it will finish by calling the onStop method passing itself as an argument for each of the listeners.
Basically, it does the following:
checkAndNotify() {
for each listener:
call onStart(this)
for each of file system changes:
for each of the listeners:
call on[File|Directory][Create|Delete|Change] method depending on the file system change
for each listener:
call onStop(this)
}
But when we run this, we will only get onStart and onStop events as there would be no changes to the file system.
Let us run this observer in an infinite thread and manually make some changes to the /tmp directory.
Running FileAlterationObserver in a thread
The FileAlterationObserverRunner is a Runnable. It takes a FileAlterationObserver instance and calls checkAndNotify() in an infinite loop once every 3 seconds.
public class FileAlterationObserverRunner implements Runnable {
private final FileAlterationObserver fileAlterationObserver;
public FileAlterationObserverRunner(FileAlterationObserver fileAlterationObserver) {
this.fileAlterationObserver = fileAlterationObserver;
}
@Override
public void run() {
while (true) {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
fileAlterationObserver.checkAndNotify();
}
}
}
Now after we create the file alteration observer and initializing it, we will create an instance of the above runner class. Then we start a new thread with this runnable. This will start a Java process which runs our observer runner thread infinitely. Now we can go and make some changes in the /tmp directory and see what events are triggered.
//.. same code as earlier
fileAlterationObserver.initialize();
FileAlterationObserverRunner fileAlterationObserverRunner = new FileAlterationObserverRunner(fileAlterationObserver);
Thread observer = new Thread(fileAlterationObserverRunner);
observer.start();
When the above is running, we get onStart and onStop events. Thus it prints,
onStart called for observer FileAlterationObserver[file='/tmp', listeners=1]
onStop called for observer FileAlterationObserver[file='/tmp', listeners=1]
We keep getting these two events once every 3 seconds. Note that it will trigger the onStart and onStop events (method call) on all listeners even when there is no file system changes. From the above we can see that the default toString of the FileAlterationObserver prints the root directory it is observing along with the number of registered listeners.
Creating, Modifying and Deleting files
With the above running, let us first create a file. I’m going to use the touch command to create a file from the console.
Note: My pwd is /tmp and I run all commands from there.
touch file1.txt
The next set of notifications on the listener will have a call to the onFileCreate method (in addition to the onStart and onStop methods) as shown below. From the printed file instance, we can see that /tmp/file1.txt got created.
onStart called for observer FileAlterationObserver[file='/tmp', listeners=1]
File changed /tmp/file1.txt
onStop called for observer FileAlterationObserver[file='/tmp', listeners=1]
Now, let us modify this file by writing some data into it.
echo "some-data" >> file1.txt
The next set of notifications will call onStart, onStop (as usual) and the onFileChange method.
onStart called for observer FileAlterationObserver[file='/tmp', listeners=1]
File changed /tmp/file1.txt
onStop called for observer FileAlterationObserver[file='/tmp', listeners=1]
Finally, let us delete this file.
rm file1.txt
As you would have correctly guessed, it calls the onStart, onStop and the onFileDelete methods.
onStart called for observer FileAlterationObserver[file='/tmp', listeners=1]
File deleted /tmp/file1.txt
onStop called for observer FileAlterationObserver[file='/tmp', listeners=1]
Multiple file system changes
When we create/delete/change more than one file between two of our observer thread runs (3 seconds interval), it will call the listener methods for each of the changes made. It knows the previous state of the file system at the configured root directory. Then when we call checkAndNotify(), it gets the current file system state and compares with the previous snapshot or state to find the list of changes. For each of the changes, it calls the appropriate methods on the listener.
Let us create two files (quickly) between two runs.
touch file3.txt
touch file4.txt
Now, we get,
nStart called for observer FileAlterationObserver[file='/tmp', listeners=1]
File created /tmp/file3.txt
File created /tmp/file4.txt
onStop called for observer FileAlterationObserver[file='/tmp', listeners=1]
It calls the onFileCreate method for each of the created files.
But if we write multiple times into the file, it will create only one onFileChange event.
Note: You have to make sure to run these exactly between two runs of our observer runnable. You can increase the time interval from 3 seconds to a higher number to make it easier.
echo "data1" >> file3.txt
echo "data2" >> file3.txt
echo "data3" >> file3.txt
onStart called for observer FileAlterationObserver[file='/tmp', listeners=1]
File changed /tmp/file3.txt
onStop called for observer FileAlterationObserver[file='/tmp', listeners=1]
Creating, Modifying and Deleting directories
Let us now create, delete and modify directories. As before, run the FileAlterationObserverRunner in a thread and create a new directory.
mkdir myDir
We get the onStart, onStop and onDirectoryCreate events.
onStart called for observer FileAlterationObserver[file='/tmp', listeners=1]
Directory created /tmp/myDir
onStop called for observer FileAlterationObserver[file='/tmp', listeners=1]
We can change or modify a directory by creating a file (or another directory) inside it. Let us create a file, file1.txt inside myDir.
touch myDir/file1.txt
Now we get four events in total viz., onStart, onStop (as usual), onDirectoryChange and onFileCreate. It notified for both the new file creation and the directory change as a result of that.
onStart called for observer FileAlterationObserver[file='/tmp', listeners=1]
Directory changed /tmp/myDir
File created /tmp/myDir/file1.txt
onStop called for observer FileAlterationObserver[file='/tmp', listeners=1]
In the current state, let us remove the directory.
rm -r myDir
Now, as a result of the directory deletion all contents of that directory would be deleted. Since we had a file inside that, we get
- onFileDelete event for deletion of file1.txt inside myDir.
- onDirectoryDelete for deletion of myDir.
onStart called for observer FileAlterationObserver[file='/tmp', listeners=1]
File deleted /tmp/myDir/file1.txt
Directory deleted /tmp/myDir
onStop called for observer FileAlterationObserver[file='/tmp', listeners=1]
Java FileFilter
A FileFilter is a filter (like a predicate) for files. It is a FunctionalInterface with the below shown signature. It takes a File instance and returns a boolean.
public interface FileFilter {
boolean accept(File pathname);
}
Why am I suddenly talking about FileFilter? We can use this with the file alteration observer. I’ll show a quick example of using FileFilter.
The Apache Commons has a FileFilterUtils class which enables us to build FileFilter instances.
FileFilterUtils - directoryFileFilter and fileFileFilter
The directoryFileFilter returns an IOFileFilter (which extends the FileFilter interface) that checks if a file is a directory. Similarly, the fileFileFilter checks if a file is a file (and not a directory).
IOFileFilter directoryFileFilter = FileFilterUtils.directoryFileFilter();
System.out.println(directoryFileFilter.accept(new File("/tmp"))); //true
System.out.println(directoryFileFilter.accept(new File("/tmp/file.txt"))); //false
IOFileFilter fileFileFilter = FileFilterUtils.fileFileFilter();
System.out.println(fileFileFilter.accept(new File("/tmp"))); //false
System.out.println(fileFileFilter.accept(new File("/tmp/file.txt"))); //true
The suffixFileFilter and prefixFileFilter
The suffixFileFilter returns a filter which checks if the file name ends with the passed suffix string. And the prefixFileFilter checks if the name starts with the passed prefix string.
IOFileFilter suffixFileFilter = FileFilterUtils.suffixFileFilter(".txt");
System.out.println(suffixFileFilter.accept(new File("/tmp/file.txt"))); //true
System.out.println(suffixFileFilter.accept(new File("/tmp/MyClass.java"))); //false
IOFileFilter prefixFileFilter = FileFilterUtils.prefixFileFilter("My");
System.out.println(prefixFileFilter.accept(new File("MyClass.java"))); //true
System.out.println(prefixFileFilter.accept(new File("MyFile.txt"))); //true
System.out.println(prefixFileFilter.accept(new File("YourFile.txt"))); //false
Combining FileFilters - and, or and not
The FileFilterUtils class has methods and, or and not which allows us to combine FileFilters or to negate them. In the below code, we use and hence the built FileFilter returns true only if the file starts with ‘My’ and ends with ‘.txt’.
// using earlier built prefixFileFilter and suffixFileFilter
IOFileFilter prefixAndSuffixFileFilter = FileFilterUtils.and(prefixFileFilter, suffixFileFilter);
System.out.println(prefixAndSuffixFileFilter.accept(new File("MyClass.java"))); //false
System.out.println(prefixAndSuffixFileFilter.accept(new File("MyFile.txt"))); //true
System.out.println(prefixAndSuffixFileFilter.accept(new File("YourFile.txt"))); //false
Using or, it returns true if the file starts with ‘My’ or ends with ‘.txt’.
// using earlier built prefixFileFilter and suffixFileFilter
IOFileFilter prefixOrSuffixFileFilter = FileFilterUtils.or(prefixFileFilter, suffixFileFilter);
System.out.println(prefixOrSuffixFileFilter.accept(new File("MyClass.java"))); //true
System.out.println(prefixOrSuffixFileFilter.accept(new File("MyFile.txt"))); //true
System.out.println(prefixOrSuffixFileFilter.accept(new File("YourFile.txt"))); //true
Finally, using notFileFilter, we can negate the check made by the file filter. Hence, the below file filter would return a true if the file doesn’t end with ‘.txt’.
// using earlier built prefixFileFilter
IOFileFilter notPrefixFileFilter = FileFilterUtils.notFileFilter(prefixFileFilter);
System.out.println(notPrefixFileFilter.accept(new File("MyClass.java"))); //false
System.out.println(notPrefixFileFilter.accept(new File("MyFile.txt"))); //false
System.out.println(notPrefixFileFilter.accept(new File("YourFile.txt"))); //true
Other FileAlterationObserver constructors
Passing a FileFilter
Let us now see the other overloaded constructors present in the FileAlterationObserver class. For each of the variations discussed here, it would have another similar overload where we can pass the root directory as a File (not a String). I’ll skip those overloaded constructors.
First variation, we can pass a FileFilter. Then, only the files and directories which pass the FileFilter predicate check are eligible to be notified.
IOFileFilter foldersAndTxtFiles = FileFilterUtils.or(
FileFilterUtils.directoryFileFilter(),
FileFilterUtils.suffixFileFilter(".txt"));
We have a FileFilter which allows only directories and .txt files. We pass this as the second argument to the FileAlterationObserver constructor. (The rest of the code is same and hence skipped).
//rest of code same as before
FileAlterationObserver fileAlterationObserver = new FileAlterationObserver(directory, foldersAndTxtFiles);
//rest of code same as before
FileAlterationObserverRunner fileAlterationObserverRunner = new FileAlterationObserverRunner(fileAlterationObserver);
Thread observer = new Thread(fileAlterationObserverRunner);
observer.start();
Now, as before, the thread will call the checkAndNotify() once every three seconds. But this time, the observer only considers directories and files ending with .txt.
touch newFile
Since this is neither a directory nor a text file, it filters it out (we don’t get anything on the listener).
mkdir newDir
Since this is a directory, we get,
onStart called for observer FileAlterationObserver[file='/tmp', OrFileFilter(DirectoryFileFilter,SuffixFileFilter(.txt)), listeners=1]
Directory created /tmp/newDir
onStop called for observer FileAlterationObserver[file='/tmp', OrFileFilter(DirectoryFileFilter,SuffixFileFilter(.txt)), listeners=1]
Let us create a file inside the newDir.
touch newDir/file1
We get the Directory change event only and no file created event as the file doesn’t end with .txt.
onStart called for observer FileAlterationObserver[file='/tmp', OrFileFilter(DirectoryFileFilter,SuffixFileFilter(.txt)), listeners=1]
Directory changed /tmp/newDir
onStop called for observer FileAlterationObserver[file='/tmp', OrFileFilter(DirectoryFileFilter,SuffixFileFilter(.txt)), listeners=1]
Let us create a .txt file inside the newDir directory.
touch newDir/file2.txt
onStart called for observer FileAlterationObserver[file='/tmp', OrFileFilter(DirectoryFileFilter,SuffixFileFilter(.txt)), listeners=1]
Directory changed /tmp/newDir
File created /tmp/newDir/file2.txt
onStop called for observer FileAlterationObserver[file='/tmp', OrFileFilter(DirectoryFileFilter,SuffixFileFilter(.txt)), listeners=1]
We got,
- Directory change notification.
- File created notification for the file file2.txt
Removing the directory will call,
- onFileDelete only for the file2.txt and
- onDirectoryDelete for the newDir folder
rm -r newDir
onStart called for observer FileAlterationObserver[file='/tmp', OrFileFilter(DirectoryFileFilter,SuffixFileFilter(.txt)), listeners=1]
File deleted /tmp/newDir/file2.txt
Directory deleted /tmp/newDir
onStop called for observer FileAlterationObserver[file='/tmp', OrFileFilter(DirectoryFileFilter,SuffixFileFilter(.txt)), listeners=1]
Setting the IO case sensitivity
In addition to the FileFilter argument, we can pass an IOCase as well. It is an enum which allows us to control how the file name checks are to be performed. It has three values:
INSENSITIVE
SENSITIVE
SYSTEM - to be determined by the OS
Setting to SYSTEM will leave the determination to the underlying operating system (Windows is case-insensitive whereas Unix is case-sensitive).
Adding and Removing listeners dynamically
Since the FileAlterationObserver has a addListener method we can add listeners dynamically. It also has a removeListener method to remove listeners. If we no longer want a listener to be notified, we can remove it as:
// fileAlterationListener was registered by calling addListener before
fileAlterationObserver.removeListener(fileAlterationListener);
Terminating the observation
The FileAlterationObserver has a destroy method which we are supposed to call when we are done with the observing (after calling checkAndNotify any number of times). But the current implementation of the destroy() method is empty (a no-op) and does nothing and hence I didn’t call it.
Conclusion
In this post we learnt about the Apache Commons IO FileAlterationObserver class and the FileAlterationListener interface. We can use these to get notifications of file system modifications (file or directory creation/changes/deletions).
And check out the other apache-commons posts as well.