Java Class for Monitoring File Changes

Java class to monitor a file for changes and notify a listener

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.

Written by KB3HHA on 08/14/2020