package restx.common.watch;
import com.google.common.eventbus.EventBus;
import java.nio.file.Path;
import java.nio.file.StandardWatchEventKinds;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.HashMap;
import java.util.concurrent.TimeUnit;
/**
* Used to coalesce {@link restx.common.watch.FileWatchEvent} in a short period of time.
*
* <p>
* There is some cases where events will be discarded:
* <ul>
* <li>If the same event is posted multiple times, only the first occurrence will be kept.</li>
* <li>If a create event follow a delete event, for a same file, it will be transformed into a modified event.</li>
* </ul>
*
* @author apeyrard
*/
public class FileWatchEventCoalescor extends EventCoalescor<FileWatchEvent> {
/**
* Create a new {@link EventCoalescor} to coalesce {@link FileWatchEvent}.
*
* @param eventBus the event bus where to post processed events
* @param coalescePeriod the coalesce period
* @return the generic event coalescor
*/
public static FileWatchEventCoalescor create(EventBus eventBus, long coalescePeriod) {
return new FileWatchEventCoalescor(eventBus, coalescePeriod);
}
private final HashMap<FileWatchEventKey, Deque<EventReference>> queue = new HashMap<>();
FileWatchEventCoalescor(EventBus eventBus, long coalescePeriod) {
super(eventBus, coalescePeriod);
}
/**
* Posts a {@link restx.common.watch.FileWatchEvent}, the post will be delayed, or even discarded, if
* the event might be merged, with a previous one.
*
* @param event the event to try to post
*/
public void post(final FileWatchEvent event) {
synchronized (queue) {
final FileWatchEventKey key = FileWatchEventKey.fromEvent(event);
Deque<EventReference> fileEvents;
if ((fileEvents = queue.get(key)) == null) {
// easy case, first event for a file, just queue it and schedule a post
fileEvents = new ArrayDeque<>();
queue.put(key, fileEvents);
EventReference reference = EventReference.of(key, event);
fileEvents.add(reference);
schedulePost(reference);
return;
}
// more complex case, we need to analyze the last saved event for this file
EventReference last = fileEvents.getLast();
if (!merge(last, event)) {
// event has not been merged, so try to add it
EventReference reference = EventReference.of(key, event);
fileEvents.add(reference);
schedulePost(reference);
}
}
}
/**
* tries to merge the current event into the current one
*/
private boolean merge(EventReference previous, FileWatchEvent current) {
if (!previous.isPresent()) {
return false;
}
if (previous.getReference().getKind() == current.getKind()) {
return true; // duplicate events, keep only one
}
if (previous.getReference().getKind() == StandardWatchEventKinds.ENTRY_DELETE) {
if (current.getKind() == StandardWatchEventKinds.ENTRY_CREATE) {
// DELETE, then CREATE, so merge into a MODIFY
previous.updateReference(
FileWatchEvent.fromWithKind(previous.getReference(), StandardWatchEventKinds.ENTRY_MODIFY));
return true;
}
}
if (previous.getReference().getKind() == StandardWatchEventKinds.ENTRY_CREATE) {
if (current.getKind() == StandardWatchEventKinds.ENTRY_MODIFY) {
// skip modify
return true;
}
}
if (previous.getReference().getKind() == StandardWatchEventKinds.ENTRY_CREATE) {
if (current.getKind() == StandardWatchEventKinds.ENTRY_DELETE) {
// CREATE then DELETE, so nothing to notify
previous.clearReference();
return true;
}
}
return false;
}
/**
* postpones the post of the specified event, when it will be time to post,
* the reference might have been cleaned up
*
* (package-private for test purposes)
*/
void schedulePost(final EventReference event) {
executor.schedule(new Runnable() {
@Override
public void run() {
synchronized (queue) {
try {
if (event.isPresent()) {
eventBus.post(event.getReference());
}
} finally {
dequeue(event.getKey(), event);
}
}
}
}, coalescePeriod, TimeUnit.MILLISECONDS);
}
/**
* remove the specified event from the queue
*
* (package-private for test purposes)
*/
void dequeue(FileWatchEventKey key, EventReference event) {
Deque<EventReference> fileEvents;
if ((fileEvents = queue.get(key)) != null) {
if (fileEvents.remove(event) && fileEvents.isEmpty()) {
queue.remove(key); // no more events for this key, remove the stack
}
}
}
/**
* clear all events
*
* (package-private for test purposes)
*/
void clear() {
synchronized (queue) {
queue.clear();
}
}
/**
* key used for the storage of an event, composed by file paths, two event with same keys, are for the same physical file
*/
static class FileWatchEventKey {
static FileWatchEventKey fromEvent(FileWatchEvent event) {
return new FileWatchEventKey(event.getDir(), event.getPath());
}
private final Path dir;
private final Path path;
private FileWatchEventKey(Path dir, Path path) {
this.dir = dir;
this.path = path;
}
@Override
public boolean equals(Object o) {
if (this == o)
return true;
if (!(o instanceof FileWatchEventKey))
return false;
FileWatchEventKey that = (FileWatchEventKey) o;
return dir.equals(that.dir) && path.equals(that.path);
}
@Override
public int hashCode() {
int result = dir.hashCode();
result = 31 * result + path.hashCode();
return result;
}
}
/**
* this is a reference holder, the reference might have been cleaned up, and be null
*
* it also stores the key of the event, in order to avoid key recalculation
*/
static class EventReference {
static EventReference of(FileWatchEventKey key, FileWatchEvent reference) {
return new EventReference(key, reference);
}
private final FileWatchEventKey key;
private FileWatchEvent reference;
private EventReference(FileWatchEventKey key, FileWatchEvent reference) {
this.key = key;
this.reference = reference;
}
public void updateReference(FileWatchEvent newEvent) {
reference = newEvent;
}
public void clearReference() {
reference = null;
}
public boolean isPresent() {
return reference != null;
}
public FileWatchEventKey getKey() {
return key;
}
public FileWatchEvent getReference() {
return reference;
}
}
}