FileWatcher.java
/*
* @copyright defined in LICENSE.txt
*/
package ship.util;
import static hera.util.ObjectUtils.nvl;
import static hera.util.ThreadUtils.trySleep;
import static java.util.Arrays.asList;
import static java.util.Arrays.stream;
import static java.util.Collections.unmodifiableCollection;
import static java.util.stream.Collectors.toList;
import static java.util.stream.Collectors.toSet;
import hera.server.ServerEvent;
import hera.server.ThreadServer;
import hera.util.Pair;
import java.io.File;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.Queue;
import java.util.Set;
import lombok.Getter;
import lombok.Setter;
/**
* Monitor file changes.
*/
public class FileWatcher extends ThreadServer implements Runnable {
/**
* Reset.
*/
public static final int RESET = 0x11;
/**
* File add event type.
*/
public static final int FILE_ADDED = 0x12;
/**
* File remove event type.
*/
public static final int FILE_REMOVED = 0x13;
/**
* File modification event type.
*/
public static final int FILE_CHANGED = 0x14;
/**
* Any change event type.
*/
public static final int ANY_CHANGED = 0x18;
/**
* Interval to check.
*/
@Getter
@Setter
protected long interval = 300;
/**
* File's last changed time.
*/
protected final HashMap<File, Long> base2lastModified1 = new HashMap<>();
protected final File base;
protected long lastModified = 0;
protected Set<File> previouslyChecked = new HashSet<>();
protected Set<String> ignores = new HashSet<>();
/**
* Constructor with watch service and base path.
*
* @param basePath path to monitor
*/
public FileWatcher(final File basePath) {
this.base = basePath;
}
public void addIgnore(final String name) {
ignores.add(name);
}
/**
* Rake file changes.
*
* <p>
* Save files to be checked into {@code checked} and files to be changed into {@code changed}.
* </p>
*
* @param checked container to save checked files
* @param changed container to save changed files
*/
protected void rake(final Set<File> checked, final Set<File> changed) {
logger.trace("Base: {}", base);
logger.trace("Base's last modified: {}", lastModified);
long max = 0;
final Queue<File> files = new LinkedList<>(asList(base));
while (!files.isEmpty()) {
final File file = files.remove();
logger.trace("File: {}", file);
final long lastModifiedInFile = file.lastModified();
max = Math.max(lastModifiedInFile, max);
if (lastModified < lastModifiedInFile) {
changed.add(file);
}
checked.add(file);
final File[] children = nvl(file.listFiles(), new File[0]);
logger.trace("Find {} files", children.length);
files.addAll(stream(children)
.filter(child -> !ignores.contains(child.getName())).collect(toList()));
}
lastModified = max;
}
@Override
@SuppressWarnings({"unchecked", "unsafe"})
protected void process() throws Exception {
trySleep(getInterval());
final HashSet<File> checkedFiles = new HashSet<>();
final HashSet<File> changed = new HashSet<>();
rake(checkedFiles, changed);
logger.debug("Changed: {}", changed);
final BeforeAndAfter<File> beforeAndAfter =
new BeforeAndAfter<>(previouslyChecked, checkedFiles);
previouslyChecked = checkedFiles;
final Set<File> added = beforeAndAfter.getAddedItems();
final Set<File> removed = beforeAndAfter.getRemovedItems();
final Set<File> any = asList(added, removed, changed).stream()
.flatMap(Collection::stream).collect(toSet());
asList(
new Pair<>(added, FILE_ADDED),
new Pair<>(removed, FILE_REMOVED),
new Pair<>(changed, FILE_CHANGED),
new Pair<>(any, ANY_CHANGED)
).stream().filter(pair -> !pair.v1.isEmpty())
.map(pair -> new ServerEvent(this, pair.v2, unmodifiableCollection(pair.v1)))
.forEach(this::fireEvent);
}
}