Java Class for Monitoring File Changes
My weather station software has a configuration file for managing its'
settings such as which devices are connected, where to send weather data,
and whether any of these items are disabled or have debugging turned on. I
wanted the weather server to be able to monitor changes to this
configuration file so that I would not need to restart the server every
time I made a change to the configuration file. Ideally the weather server
would either reload the settings or restart whenever the configuration
file changed. That way I could enable or disable devices, or turn
debugging on and off just by editing the configuration file and the server
would respond to the changes with no further action by me. This led me to
develop a FileChangeMonitor class. I created this quite a few years ago
and probably got the basic idea from an example I found in either a book
or through an Internet search. I would like to credit the original source
for the idea but unfortunately I no longer remember where the original
came from.
The FileChangeMonitor class maintains a list of listeners to be notified
when the monitored file has changed. In order to receive notifications the
listener must implement the FileChangeListener interface. This interface
has a single method that will be called by the FileChangeMonitor when it
detects a change in the monitored file. The FileChangeListener interface
looks like this:
public interface FileChangeListener {
public void fileChanged(String fileName);
}
The listener registers with the FileChangeMonitor by telling it what file
it wishes to monitor and how frequently to check for changes. It does this
by calling the addFileChangeListener() method of the FileChangeMonitor
class. This method schedules a background task to monitor the file and
notify the listener on any file changes. There is also a corresponding
removeFileChangeListener() method that should be called when the listener
no longer wishes to be notified of changes to the monitored file. This
method stops the background task and removes it from the list of monitor
tasks.
Here are the addFileChangeListener() and removeFileChangeListener()
methods.
/** Add a monitored file with a FileChangeListener.
* @param listener listener to notify when the file changed.
* @param fileName name of the file to monitor.
* @param period polling period in milliseconds.
*/
public void addFileChangeListener(FileChangeListener listener, String fileName, long period) throws FileNotFoundException {
removeFileChangeListener(listener, fileName);
FileMonitorTask task = new FileMonitorTask(listener, fileName);
timerEntries.put(fileName + listener.hashCode(), task);
timer.schedule(task, period, period);
}
/** Remove the listener from the notification list.
* @param listener the listener to be removed.
* @param fileName name of the file that was being monitored.
*/
public void removeFileChangeListener(FileChangeListener listener, String fileName) {
FileMonitorTask task = (FileMonitorTask) timerEntries.remove(fileName + listener.hashCode());
if (task != null) {
task.cancel();
}
}
That handles the listener portion of the code. The monitor task is pretty
straightforward. It saves off the last modified date and time for the
monitored file and then periodically compares the saved off date and time
to the current values. If the current date and time is more recent the
listener is notified. Here's the code for that:
protected void fireFileChangeEvent(FileChangeListener listener, String fileName) {
listener.fileChanged(fileName);
}
class FileMonitorTask extends TimerTask {
FileChangeListener listener;
String fileName;
File monitoredFile;
long lastModified;
public FileMonitorTask(FileChangeListener listener, String fileName) throws FileNotFoundException {
this.listener = listener;
this.fileName = fileName;
this.lastModified = 0;
monitoredFile = new File(fileName);
if (!monitoredFile.exists()) { // but is it on CLASSPATH?
URL fileURL = listener.getClass().getClassLoader().getResource(fileName);
if (fileURL != null) {
monitoredFile = new File(fileURL.getFile());
}
else {
throw new FileNotFoundException("File Not Found: "
+ fileName);
}
}
this.lastModified = monitoredFile.lastModified();
}
public void run() {
long lastModified = monitoredFile.lastModified();
if (lastModified != this.lastModified) {
this.lastModified = lastModified;
fireFileChangeEvent(this.listener, this.fileName);
}
}
}
The last thing to take care of is to ensure we only have one
FileChangeMonitor instance to control all of the background monitoring
tasks. I accomplished this by using the Singleton design pattern. This
pattern is well documented in the book Design Patterns Elements of
Reusable Object-Oriented Software by Erich Gamma, Richard Helm, Ralph
Johnson, and John Vlissides. I highly recommend this book if you
have any interest in design patterns. Wikipedia has an article on the
Singleton design pattern as well.
Here is the Singleton implementation:
private static final FileChangeMonitor instance = new FileChangeMonitor();
public static FileChangeMonitor getInstance() {
return instance;
}
protected FileChangeMonitor() {
// Create timer, run timer thread as daemon.
timer = new Timer(true);
timerEntries = new Hashtable<string, FileMonitorTask>();
}
We first create an instance of the FileChangeMonitor class, We implement
the method getInstance() which will return the instance we initially
created. Then we make the constructor protected so that the only way to
get the FileChangeMonitor instance is through the getInstance() method
which will always return the one instance we created.
That sums up all of the bits and pieces. So here is the complete
FileChangeMonitor class:
public class FileChangeMonitor {
private static final FileChangeMonitor instance = new FileChangeMonitor();
private Timer timer;
private Hashtable<string, FileMonitorTask> timerEntries;
public static FileChangeMonitor getInstance() {
return instance;
}
protected FileChangeMonitor() {
// Create timer, run timer thread as daemon.
timer = new Timer(true);
timerEntries = new Hashtable<string, FileMonitorTask>();
}
/** Add a monitored file with a FileChangeListener.
* @param listener listener to notify when the file changed.
* @param fileName name of the file to monitor.
* @param period polling period in milliseconds.
*/
public void addFileChangeListener(FileChangeListener listener, String fileName, long period) throws FileNotFoundException {
removeFileChangeListener(listener, fileName);
FileMonitorTask task = new FileMonitorTask(listener, fileName);
timerEntries.put(fileName + listener.hashCode(), task);
timer.schedule(task, period, period);
}
/** Remove the listener from the notification list.
* @param listener the listener to be removed.
* @param fileName name of the file that was being monitored.
*/
public void removeFileChangeListener(FileChangeListener listener, String fileName) {
FileMonitorTask task = (FileMonitorTask) timerEntries.remove(fileName + listener.hashCode());
if (task != null) {
task.cancel();
}
}
protected void fireFileChangeEvent(FileChangeListener listener, String fileName) {
listener.fileChanged(fileName);
}
class FileMonitorTask extends TimerTask {
FileChangeListener listener;
String fileName;
File monitoredFile;
long lastModified;
public FileMonitorTask(FileChangeListener listener, String fileName) throws FileNotFoundException {
this.listener = listener;
this.fileName = fileName;
this.lastModified = 0;
monitoredFile = new File(fileName);
if (!monitoredFile.exists()) { // but is it on CLASSPATH?
URL fileURL = listener.getClass().getClassLoader().getResource(fileName);
if (fileURL != null) {
monitoredFile = new File(fileURL.getFile());
}
else {
throw new FileNotFoundException("File Not Found: "
+ fileName);
}
}
this.lastModified = monitoredFile.lastModified();
}
public void run() {
long lastModified = monitoredFile.lastModified();
if (lastModified != this.lastModified) {
this.lastModified = lastModified;
fireFileChangeEvent(this.listener, this.fileName);
}
}
}
}
Here is an example of how to use the FileChangeMonitor:
public class MonitorTest implements FileChangeListener {
public MonitorTest() {
FileChangeMonitor fileChangeMonitor = FileChangeMonitor.getInstance();
try
{
fileChangeMonitor.addFileChangeListener(this, "config.file", 10000);
}
catch (FileNotFoundException e)
{
System.out.println("Configuration file not found by monitor: " + e.getMessage());
}
}
void fileChanged(String fileName) {
System.out.println("File changed: " + fileName);
}
}
I hope you find this useful. Happy coding.