blob: e5537f7769c2fae80e07346740921c0a2f8e5b30 [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.sling.ide.cli.impl;
import static java.nio.file.StandardWatchEventKinds.ENTRY_CREATE;
import static java.nio.file.StandardWatchEventKinds.ENTRY_DELETE;
import static java.nio.file.StandardWatchEventKinds.ENTRY_MODIFY;
import java.io.IOException;
import java.nio.file.ClosedWatchServiceException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardWatchEventKinds;
import java.nio.file.WatchEvent;
import java.nio.file.WatchEvent.Kind;
import java.nio.file.WatchKey;
import java.nio.file.WatchService;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
* Watches a whole directory tree for changes
*
* <p>This class works on top of the standard {@link WatchService} API by generating
* events for all changes below a given directory.</p>
*
* <p>This implementation works within the same constraints as the {@link WatchService} so
* consumers are advised to read the documentation, particularly those related to platform limitations.
* It is recommended to allow for minimal
*/
public class DirWatcher implements AutoCloseable {
private final Path root;
private final WatchService ws;
private final DualMap watched = new DualMap();
private final Thread poller;
private final BlockingQueue<DirWatcher.Event> queue = new LinkedBlockingQueue<>();
public DirWatcher(Path path) throws IOException {
this.root = path;
ws = path.getFileSystem().newWatchService();
poller = new Thread(() -> {
while ( !Thread.currentThread().isInterrupted() ) {
try {
queue.addAll(pollInternal());
} catch ( InterruptedException e) {
Thread.currentThread().interrupt();
break;
} catch ( ClosedWatchServiceException e) {
break;
}
}
}, getClass().getSimpleName() +"-Poller");
Stream.concat(
Stream.of(root),
Files.walk(root).filter(p -> p.toFile().isDirectory())
).forEach( this::register);
poller.start();
}
public void close() throws IOException {
if ( poller != null )
poller.interrupt();
if ( ws != null)
ws.close();
}
/**
* Takes a single event from the queue, blocking if none are available
*
* @return the event
* @throws InterruptedException interrupted
*/
public Event poll() throws InterruptedException {
return queue.take();
}
// visible for testing
int queueSize() {
return queue.size();
}
private void register(Path path) {
try {
WatchKey key = path.register(ws, ENTRY_CREATE, ENTRY_DELETE, ENTRY_MODIFY);
watched.put(key, path);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
private void unregister(Path path) {
WatchKey key = watched.remove(path);
if ( key != null )
key.cancel();
}
private List<DirWatcher.Event> pollInternal() throws InterruptedException {
final WatchKey key = ws.take();
List<DirWatcher.Event> result = key.pollEvents().stream()
.filter( e -> e.context() instanceof Path )
.map( Event::new )
.map( e -> updateTracked(e, key) )
.map( e -> adjust(e, key) )
.collect( Collectors.toList() );
key.reset();
return result;
}
private DirWatcher.Event adjust(Event e, WatchKey key) {
Path keyPath = watched.get(key);
e.path = root.relativize(keyPath.resolve(e.path));
return e;
}
private DirWatcher.Event updateTracked(DirWatcher.Event evt, WatchKey key) {
if ( evt.getKind() == StandardWatchEventKinds.ENTRY_CREATE ) {
Path fullPath = watched.get(key).resolve(evt.getPath());
if ( fullPath.toFile().isDirectory())
register(fullPath);
} else if ( evt.getKind() == StandardWatchEventKinds.ENTRY_DELETE ) {
Path fullPath = watched.get(key).resolve(evt.getPath());
// we can't check if the path pointed to a directory since it is already deleted
unregister(fullPath);
}
return evt;
}
public static class Event {
public Event(WatchEvent<?> wrapper) {
kind = wrapper.kind();
path = (Path) wrapper.context();
count = wrapper.count();
}
private Kind<?> kind;
private Path path;
private int count;
public Kind<?> getKind() {
return kind;
}
public Path getPath() {
return path;
}
@Override
public String toString() {
return getClass().getSimpleName() + "[ kind: " + kind +", path: " + path + ", count: " + count +"]";
}
}
static class DualMap {
private final Map<WatchKey, Path> forward = new HashMap<>();
private final Map<Path, WatchKey> reverse = new HashMap<>();
private final Object sync = new Object();
public void put(WatchKey key, Path path) {
synchronized (sync) {
forward.put(key, path);
reverse.put(path, key);
}
}
public Path get(WatchKey key) {
synchronized (sync) {
return forward.get(key);
}
}
public WatchKey get(Path path) {
synchronized (sync) {
return reverse.get(path);
}
}
public WatchKey remove(Path path) {
synchronized (sync) {
WatchKey key = reverse.get(path);
if ( key != null )
forward.remove(key);
return key;
}
}
}
}