blob: d51c35b6c8b636e726fb049b65534bf0615059d9 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2008, 2010 VMware Inc.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* VMware Inc. - initial contribution
*******************************************************************************/
package org.eclipse.virgo.util.io;
import java.io.File;
import java.io.FilenameFilter;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.regex.Pattern;
import org.eclipse.virgo.util.common.Assert;
import org.eclipse.virgo.util.math.Sets;
import org.slf4j.Logger;
/**
* Checks a directory on the file system for modifications. Maintains a known state of the files to determine if the
* changes are new files, modified files or deleted files.
* <p/>
*
* Modification notifications ({@link FileSystemEvent}s) are published to a set of configured {@link FileSystemListener
* FileSystemListeners}. New listeners can be safely added at runtime.
* <p/>
*
* By default, all files in the directory are monitored. Files can be excluded from monitoring using a regex pattern.
* <p/>
* <strong>Concurrent Semantics</strong><br/>
* Thread-safe.
*
*/
public final class FileSystemChecker {
private final File checkDir;
private final Logger logger;
private final Object checkLock = new Object();
/**
* The files we know about -- with their last modified date (as a Long) are in <code>fileState</code>.<br/>
* Files that are changing or are new are monitored in <code>monitorRecords</code>.
* <p/>
* <strong>Invariant:</strong> all monitored files are in <code>fileState</code>.<br/>
* As soon as a file is notified (to listeners) it is no longer monitored. Thus files in <code>fileState</code> and
* not monitored can be assumed to have been notified about already.
*/
private final Map<String, Long> fileState = new HashMap<String, Long>(32);
private final Map<String, MonitorRecord> monitorRecords = new HashMap<String, MonitorRecord>(16);
private final List<FileSystemListener> listeners = new CopyOnWriteArrayList<FileSystemListener>();
private final FilenameFilter includeFilter;
private static boolean WINDOWS = System.getProperty("os.name").startsWith("Windows");
/**
* Creates a new <code>FileSystemChecker</code>. Identifies changes on all files in <code>checkDir</code>.
*
* @param checkDir the directory to check.
*/
public FileSystemChecker(File checkDir) {
this(checkDir, null, null);
}
/**
* Creates a new <code>FileSystemChecker</code>. Identifies changes on all files in <code>checkDir</code>, except those that match
* <code>excludePattern</code>. No diagnostics logging.
*
* @param checkDir the directory to check.
* @param excludePattern regular expression for files to exclude.
*/
public FileSystemChecker(File checkDir, String excludePattern) {
this(checkDir, excludePattern, null);
}
/**
* Creates a new <code>FileSystemChecker</code>. Identifies changes on all files in <code>checkDir</code>. Diagnostics to logger.
*
* @param checkDir the directory to check.
* @param logger where to log diagnostics -- can be null
*/
public FileSystemChecker(File checkDir, Logger logger) {
this(checkDir, null, logger);
}
/**
* Creates a new <code>FileSystemChecker</code>. Identifies changes to on all files, except those that match
* <code>excludePattern</code>.
*
* @param checkDir the directory to check -- {@link File} must exist and be a directory
* @param excludePattern regular expression for files to exclude.
* @param logger where to log diagnostics -- can be null
*/
public FileSystemChecker(File checkDir, String excludePattern, Logger logger) {
Assert.isTrue(checkDir.isDirectory(), "Check directory '%s' must exist and must be a directory.", checkDir.getAbsolutePath());
this.checkDir = checkDir;
this.logger = logger;
final Pattern compiledExcludePattern = (excludePattern==null) ? null : Pattern.compile(excludePattern);
this.includeFilter = new FilenameFilter() {
public boolean accept(File dir, String name) {
return compiledExcludePattern == null || !compiledExcludePattern.matcher(name).matches();
}
};
populateInitialState(); // no notifications made yet
}
/**
* Add a new {@link FileSystemListener} to this <code>FileSystemChecker</code>.
*
* @param listener the listener to add.
*/
public void addListener(FileSystemListener listener) {
this.listeners.add(listener);
}
/**
* Instructs this <code>FileSystemChecker</code> to check the configured directory and notifies any registered
* listeners of changes to the directory files.
*/
public void check() {
synchronized (this.checkLock) {
try {
File[] currentFiles;
try {
currentFiles = listCurrentDirFiles();
} catch (Exception e) {
if (logger!=null) logger.warn("FileSystemChecker caught exception from listFiles()", e);
throw e;
}
debugState("before check:", currentFiles);
Set<String> currentFileKeys = new HashSet<String>(currentFiles.length);
for (File file : currentFiles) {
// remember seen files to allow comparison for delete
String keyFile = this.key(file);
currentFileKeys.add(keyFile);
if (!isKnown(file)) {
// not seen it before -- start monitoring it -- a potential newly created file
monitorRecords.put(keyFile, new MonitorRecord(file.length(), FileSystemEvent.CREATED));
setKnownFileState(file);
} else if (monitorRecords.containsKey(keyFile)) {
// we are monitoring this file
MonitorRecord monitorRecord = monitorRecords.get(keyFile);
long size = file.length();
if (size > monitorRecord.getSize()) {
// still being written? continue to track it
monitorRecord.setSize(size);
} else if (isUnlocked(file)){
// not changing anymore so if we can rename it we can announce it:
notifyListeners(this.key(file), monitorRecord.getEvent());
// do not monitor it anymore
monitorRecords.remove(keyFile);
}
setKnownFileState(file);
} else if (file.lastModified() > knownLastModified(file)) {
// we know about this file, we are not monitoring it, but it has changed
// start monitoring it until it stabilises
monitorRecords.put(keyFile, new MonitorRecord(file.length(), FileSystemEvent.MODIFIED));
setKnownFileState(file);
}
}
Set<String> deletedFiles = Sets.difference(this.fileState.keySet(), currentFileKeys);
for (String deletedFile : deletedFiles) {
if (monitorRecords.containsKey(deletedFile)) {
// we were monitoring it when it disappeared
MonitorRecord monitorRecord = monitorRecords.get(deletedFile);
if (monitorRecord.getEvent().equals(FileSystemEvent.MODIFIED)) {
notifyListeners(deletedFile, FileSystemEvent.DELETED);
}
} else {
notifyListeners(deletedFile, FileSystemEvent.DELETED);
}
this.fileState.remove(deletedFile);
this.monitorRecords.remove(deletedFile);
}
} catch (Exception _) {
// FatalIOException can arise from listCurrentDirFiles() which means that we cannot determine the list.
// In this case we have already retried the list, and we can ignore this check().
// The check() then becomes a no-op which is better than assuming the directory is empty.
} finally {
debugState("after check:", null);
}
}
}
public boolean isUnlocked(File file) {
// Heuristic check for the file not being locked on Windows. On *ix, assume the file is unlocked since we can't tell.
return !WINDOWS || file.renameTo(file);
}
private void debugState(final String heading, File[] files) {
if (logger!=null && logger.isDebugEnabled()) {
StringBuilder sb = new StringBuilder().append(this.checkDir).append(" - ").append(heading);
if (files != null) {
sb.append("\n\tFileList(): [");
boolean first = true;
for (File f : files) {
if (!first) sb.append(", ");
sb.append(f.getName());
first = false;
}
sb.append("]");
}
if (this.fileState != null) {
sb.append("\n\tKnown files: [");
boolean first = true;
for (String s : this.fileState.keySet()) {
if (!first) sb.append(", ");
sb.append(s);
first = false;
}
sb.append("]");
}
if (this.monitorRecords != null) {
sb.append("\n\tMonitored: [");
boolean first = true;
for (String s : this.monitorRecords.keySet()) {
if (!first) sb.append(", ");
sb.append(s);
first = false;
}
sb.append("]");
}
logger.debug(sb.toString());
}
}
private void notifyListeners(String file, FileSystemEvent event) {
for (FileSystemListener listener : this.listeners) {
try {
listener.onChange(file, event);
} catch (Throwable e) {
if (logger!=null) {
logger.warn("Listener threw exception for event " + event, e);
}
}
}
}
/**
* Initialises known files (<code>fileState</code>) from the check directory and starts monitoring them.
* @throws Exception
*/
private void populateInitialState() throws RuntimeException {
File[] initialList;
try {
initialList = listCurrentDirFiles();
} catch (RuntimeException e) {
if (logger!=null) logger.warn("FileSystemChecker caught exception from listFiles()", e);
throw e;
}
for (File file : initialList) {
String keyFile = key(file);
monitorRecords.put(keyFile, new MonitorRecord(file.length(), FileSystemEvent.INITIAL));
setKnownFileState(file);
}
debugState("initial state:", initialList);
}
/**
* Lists the {@link File Files} currently in the check directory.
*
* @return the <code>Files</code> that are in the check directory.
*/
private File[] listCurrentDirFiles() {
return FileSystemUtils.listFiles(this.checkDir, this.includeFilter, this.logger);
}
/**
* Sets the state of the supplied {@link File} into our known files map (<code>fileState</code>).
*
* @param the <code>File</code> to record state for.
*/
private void setKnownFileState(File file) {
String key = key(file);
long lastModified = file.lastModified();
this.fileState.put(key, lastModified);
}
/**
* Gets the recorded last modified timestamp for the supplied {@link File}.
*
* @param file the <code>File</code> to check for.
* @return the last modified timestamp, or <code>null</code> if no timestamp is recorded.
*/
private Long knownLastModified(File file) {
return this.fileState.get(key(file));
}
/**
* Is file known to us? (In <code>fileState</code>.)
*
* @param file the <code>File</code> to check for.
* @return <code>true</code> if in the map (domain), <code>false</code> otherwise.
*/
private boolean isKnown(File file) {
return this.fileState.containsKey(key(file));
}
/**
* Gets the record key for the supplied {@link File}.
*
* @param file the <code>File</code> to get the key for.
* @return the record key.
*/
private String key(File file) {
String key = file.getAbsolutePath();
if (file.isDirectory()) {
key += File.separator;
}
return key;
}
private static class MonitorRecord {
private final FileSystemEvent event;
private long size;
public MonitorRecord(long size, FileSystemEvent event) {
this.size = size;
this.event = event;
}
public long getSize() {
return size;
}
public void setSize(long size) {
this.size = size;
}
public FileSystemEvent getEvent() {
return event;
}
}
}