package act.util; /*- * #%L * ACT Framework * %% * Copyright (C) 2014 - 2017 ActFramework * %% * Licensed 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. * #L% */ import act.exception.ActException; import org.osgl.$; import org.osgl.exception.NotAppliedException; import org.osgl.logging.L; import org.osgl.logging.Logger; import org.osgl.util.C; import java.io.File; import java.util.Map; import java.util.Set; /** * {@code FsChangeDetector} detects changes files in a folder and all sub folders. * The changes includes delete/add/update events */ public class FsChangeDetector { protected static Logger logger = L.get(FsChangeDetector.class); private C.List<FsEventListener> listeners = C.newList(); private final File dir; private final $.Predicate<String> fileNameFilter; private final Map<String, Long> timestamps = C.newMap(); private final int contextLen; private final String context; private final $.Var<Long> lastChecksum = $.var(0L); public FsChangeDetector(File file, $.Predicate<String> fileNameFilter) { this.dir = file; this.fileNameFilter = fileNameFilter; this.context = file.getAbsolutePath(); this.contextLen = context.length(); initialWalkThrough(); } public FsChangeDetector(File file, $.Predicate<String> fileNameFilter, FsEventListener... listeners) { this(file, fileNameFilter); this.listeners.append(C.listOf(listeners)); } public void registerListener(FsEventListener listener) { listeners.append(listener); } public void detectChanges() { $.Var<Long> checksum = $.var(0L); Map<String, Long> newTimestamps = walkThrough(dir, checksum); if (!checksum.get().equals(lastChecksum.get())) { if (dir.isDirectory()) { check(newTimestamps); timestamps.clear(); timestamps.putAll(newTimestamps); } else { trigger(new FsEvent(FsEvent.Kind.MODIFY, dir.getAbsolutePath())); } lastChecksum.set(checksum); } } private void initialWalkThrough() { walkThrough(dir, timestamps, lastChecksum); } private Map<String, Long> walkThrough(File file, $.Var<Long> checksum) { Map<String, Long> map = C.newMap(); walkThrough(file, map, checksum); return map; } private void check(Map<String, Long> newTimestamps) { C.List<FsEvent> events = C.newSizedList(3); C.Set<String> set0 = C.set(timestamps.keySet()); C.Set<String> set1 = C.set(newTimestamps.keySet()); C.Set<String> added = set1.without(set0); if (!added.isEmpty()) { events.add(createEvent(FsEvent.Kind.CREATE, added)); } C.Set<String> removed = set0.without(set1); if (!removed.isEmpty()) { events.add(createEvent(FsEvent.Kind.DELETE, removed)); } C.Set<String> retained = set1.withIn(set0); C.Set<String> modified = modified(retained, newTimestamps); if (!modified.isEmpty()) { events.add(createEvent(FsEvent.Kind.MODIFY, modified)); } if (!events.isEmpty()) { trigger(events.toArray(new FsEvent[events.size()])); } } private FsEvent createEvent(FsEvent.Kind kind, C.Set<String> paths) { return new FsEvent(kind, prependContext(paths)); } private C.Set<String> modified(C.Set<String> retained, Map<String, Long> newTimestamps) { C.Set<String> modified = C.newSet(); for (String path : retained) { long ts0 = timestamps.get(path); long ts1 = newTimestamps.get(path); if (ts0 != ts1) { modified.add(path); } } return modified; } private Set<String> prependContext(C.Set<String> paths) { return C.set(paths.map(new $.F1<String, String>() { @Override public String apply(String s) throws NotAppliedException, $.Break { return context + s; } })); } private void walkThrough(File file, Map<String, Long> timestamps, $.Var<Long> checksum) { if (file.isDirectory()) { Files.filter(file, fileNameFilter, visitor(timestamps, checksum)); } else { if (!file.exists()) { throw new ActException("File deleted: %s", dir); } checksum.set(file.lastModified()); } } private $.Visitor<File> visitor(final Map<String, Long> timestamps, final $.Var<Long> checksum) { return new $.Visitor<File>() { @Override public void visit(File file) throws $.Break { long ts = file.lastModified(); String path = file.getAbsolutePath().substring(contextLen); checksum.set(checksum.get() + path.hashCode() + ts); timestamps.put(file.getAbsolutePath().substring(contextLen), file.lastModified()); } }; } private void trigger(final FsEvent... events) { int n = listeners.size(); for (int i = 0; i < n; ++i) { FsEventListener l = listeners.get(i); l.on(events); } } }