Monday, August 23, 2010

Monitor Changes to a File

There are undoubtedly many options out there for file-system change monitors - i.e. a component that will notify your Java object when a given file or directory has changed, among many others I'm sure: jnotify and jpathwatch, the latter of which is based on upcoming Java 7 NIO enhancements.

I gave jpathwatch a try recently, at a time when I was under a tight deadline and didn't want to reinvent a wheel. This presents a perfectly fine API that worked out quite well and quite quickly for me in my Windows environment, but alas when I deployed to Linux, I bumped into an unsatisfied link error. The problem was around libc.so.6 and GLIBC_2.4, and I gave it a reasonable first effort to try quickly finding the resolution - assuming I'd deployed incorrectly, or my Linux box was out of date, etc. It was neither of these - OK, in fairness, it could be an out-of-date Linux box, but my experiment was to just try it out on our customers' target system - and the same problem occurred. So out-of-date becomes a moot point.

As I mentioned, I was under a tight deadline, so I began some quick prototyping to see if I could reinvent something but without relying on native libraries (as jpathwatch did). That would give me the added advantage of a smaller runtime footprint, which was another customer requirement. Since we are really talking about an Observer pattern, here's how I started:

public interface FileChangeListener {

void fileModified(String file);

void fileDeleted(String file);

void fileCreated(String file);
}

That specifies the observer. Here's a simple monitor interface:

public interface AbstractFileChangeMonitor {

void watch(FileChangeListener listener, final String filename);
}

This one is a bit limited, since it supports just one listener (observer) for one file. But my goal is not (yet) to provide a full-featured framework - I just need to knock out the problem at hand without any gold-plating. I'm a firm believer in doing the least I have to for a given problem - first make it work, then make it fast, then extend it, ... etc., but only if subsequent steps are called for. In either event, here's an implementation of the monitor - this one polls the file in question to detect changes, running in a thread so the client process isn't blocked:

public class PollingFileChangeMonitor implements AbstractFileChangeMonitor
{
private final Logger logger = LoggerFactory.getLogger(getClass().getSimpleName());
private FileChangeListener listener;
private boolean done;
private Thread watch;
private int pollingInterval;

public PollingFileChangeMonitor(int interval) {
pollingInterval = interval;
}

public void watch(FileChangeListener theListener, final String filename)
{
if (watch == null) {
listener = theListener;
watch = new Thread() {
public void run() {
try {
init(filename);
} catch (Exception e) {
throw new IllegalStateException(e);
}
}
};
watch.start();
}
}

private void init(String filename) throws Exception
{
boolean exists = false;
long modTime = -1;
File file = new File(filename);
if (file.exists())
{
exists = true;
modTime = file.lastModified();
logger.info("====> File '" + filename + "' exists; change monitor is running...");
} else
{
logger.info("====> File '" + filename + "' does NOT exist; change monitor is running...");
}

while (!done) {
try {
watch.sleep(pollingInterval);
} catch (InterruptedException e) {
// ignore for now
}

if (!exists && file.exists()) {
exists = true;
logger.info("====> File '" + filename + "' has been created; notify listener...");
listener.fileCreated(filename);
} else if (exists && !file.exists()) {
exists = false;
logger.info("====> File '" + filename + "' has been deleted; notify listener...");
listener.fileDeleted(filename);
} else if (exists && file.exists()) {
long timestamp = file.lastModified();
if (timestamp > modTime) {
modTime = timestamp;
logger.info("====> File '" + filename + "' has been modified; notify listener...");
listener.fileModified(filename);
}
}
}
}
}

What the listener does when notified is not really important in the context of this post; it can be anything. While I've to a certain extent "reinvented" something here, I've gotten away from reliance on native code and the potential deployment headaches around that, I've reduced my runtime footprint, and for that matter I've solved the problem with about the same amount of code I needed for the boiler-plate suggested by jpathwatch.

1 comment:

  1. Thanks for a sweet article,very informative.. we will sure be back for more.It is wonderful right here. good research. I've been searched this kind of information for quite a while. thanks

    ReplyDelete