blob: 220061ead9f4ef28cdaf52a114cc661c502bfd0b [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.openejb.util;
import java.io.File;
import java.io.Serializable;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Timer;
import java.util.TimerTask;
/**
* @version $Rev: 1153797 $ $Date: 2011-08-04 12:09:44 +0300 (Thu, 04 Aug 2011) $
*/
public class DirectoryMonitor {
public static final Logger logger = Logger.getInstance(LogCategory.OPENEJB_DEPLOY, DirectoryMonitor.class.getPackage().getName());
private final int pollIntervalMillis;
private final File target;
private final Listener listener;
private final Map files = new HashMap();
private final Timer timer;
public DirectoryMonitor(final File target, final Listener listener, final int pollIntervalMillis) {
assert listener == null : "No listener specified";
assert target.isDirectory() : "File specified is not a directory. " + target.getAbsolutePath();
assert target.canRead() : "Directory specified cannot be read. " + target.getAbsolutePath();
assert pollIntervalMillis > 0 : "Poll Interval must be above zero.";
this.target = target;
this.listener = listener;
this.pollIntervalMillis = pollIntervalMillis;
this.timer = new Timer(this.getClass().getSimpleName());
}
private Logger getLogger() {
return logger;
}
public int getPollIntervalMillis() {
return pollIntervalMillis;
}
public File getTarget() {
return target;
}
public Listener getListener() {
return listener;
}
public synchronized void stop() {
timer.cancel();
}
public void start() {
initialize();
getLogger().debug("Scanner running. Polling every " + pollIntervalMillis + " milliseconds.");
timer.scheduleAtFixedRate(new TimerTask(){
public void run() {
try {
scan();
}
catch (Exception e) {
getLogger().error("Scan failed.", e);
}
}
}, pollIntervalMillis, pollIntervalMillis);
}
private void initialize() {
getLogger().debug("Doing initial scan of " + target.getAbsolutePath());
File[] files = (target.isDirectory()) ? target.listFiles(): new File[]{target};
for (File file : files) {
if (!file.canRead()) {
continue;
}
FileInfo now = newInfo(file);
now.setChanging(false);
}
}
private FileInfo newInfo(File child) {
FileInfo fileInfo = child.isDirectory() ? new DirectoryInfo(child) : new FileInfo(child);
files.put(fileInfo.getPath(), fileInfo);
return fileInfo;
}
/**
* Looks for changes to the immediate contents of the directory we're watching.
*/
public void scan() {
File[] files = (target.isDirectory()) ? target.listFiles(): new File[]{target};
HashSet<String> missingFilesList = new HashSet(this.files.keySet());
for (File file : files) {
missingFilesList.remove(file.getAbsolutePath());
if (!file.canRead()) {
getLogger().debug("not readable " + file.getName());
continue;
}
FileInfo oldStatus = oldInfo(file);
FileInfo newStatus = newInfo(file);
newStatus.diff(oldStatus);
if (oldStatus == null) {
// Brand new, but assume it's changing and
// wait a bit to make sure it's not still changing
getLogger().debug("File Discovered: " + newStatus);
} else if (newStatus.isChanging()) {
// The two records are different -- record the latest as a file that's changing
// and later when it stops changing we'll do the add or update as appropriate.
getLogger().debug("File Changing: " + newStatus);
} else if (oldStatus.isNewFile()) {
// Used to be changing, now in (hopefully) its final state
getLogger().info("New File: " + newStatus);
newStatus.setNewFile(!listener.fileAdded(file));
} else if (oldStatus.isChanging()) {
getLogger().info("Updated File: " + newStatus);
listener.fileUpdated(file);
missingFilesList.remove(oldStatus.getPath());
}
// else it's just totally unchanged and we ignore it this pass
}
// Look for any files we used to know about but didn't find in this pass
for (String path : missingFilesList) {
getLogger().info("File removed: " + path);
if (listener.fileRemoved(new File(path))) {
this.files.remove(path);
}
}
}
private FileInfo oldInfo(File file) {
return (FileInfo) files.get(file.getAbsolutePath());
}
/**
* Allows custom behavior to be hooked up to process file state changes.
*/
public interface Listener {
boolean fileAdded(File file);
boolean fileRemoved(File file);
void fileUpdated(File file);
}
/**
* Provides details about a directory.
*/
private static class DirectoryInfo extends FileInfo {
public DirectoryInfo(final File dir) {
//
// We don't pay attention to the size of the directory or files in the
// directory, only the highest last modified time of anything in the
// directory. Hopefully this is good enough.
//
super(dir.getAbsolutePath(), 0, getLastModifiedInDir(dir));
}
private static long getLastModifiedInDir(final File dir) {
assert dir != null;
long value = dir.lastModified();
File[] children = dir.listFiles();
long test;
for (int i = 0; i < children.length; i++) {
File child = children[i];
if (!child.canRead()) {
continue;
}
if (child.isDirectory()) {
test = getLastModifiedInDir(child);
} else {
test = child.lastModified();
}
if (test > value) {
value = test;
}
}
return value;
}
}
/**
* Provides details about a file.
*/
private static class FileInfo implements Serializable {
private String path;
private long size;
private long modified;
private boolean newFile;
private boolean changing;
public FileInfo(final File file) {
this(file.getAbsolutePath(), file.length(), file.lastModified());
}
public FileInfo(final String path, final long size, final long modified) {
assert path != null;
this.path = path;
this.size = size;
this.modified = modified;
this.newFile = true;
this.changing = true;
}
public String getPath() {
return path;
}
public long getSize() {
return size;
}
public void setSize(final long size) {
this.size = size;
}
public long getModified() {
return modified;
}
public void setModified(final long modified) {
this.modified = modified;
}
public boolean isNewFile() {
return newFile;
}
public void setNewFile(final boolean newFile) {
this.newFile = newFile;
}
public boolean isChanging() {
return changing;
}
public void setChanging(final boolean changing) {
this.changing = changing;
}
public boolean isSame(final FileInfo info) {
assert info != null;
if (!path.equals(info.path)) {
throw new IllegalArgumentException("Should only be used to compare two files representing the same path!");
}
return size == info.size && modified == info.modified;
}
public String toString() {
return path;
}
public void diff(final FileInfo old) {
if (old != null) {
this.changing = !isSame(old);
this.newFile = old.newFile;
}
}
}
}