/*
* Copyright 2015 MovingBlocks
*
* 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.
*/
package org.terasology.module.filesystem;
import com.google.common.base.Function;
import com.google.common.collect.Collections2;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import javax.annotation.Nullable;
import java.io.IOException;
import java.nio.file.FileSystems;
import java.nio.file.Path;
import java.nio.file.WatchEvent;
import java.nio.file.WatchKey;
import java.nio.file.WatchService;
import java.nio.file.Watchable;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
/**
* A watch service for ModuleFileSystems. Wraps a watch service from the default filesystem, and allows keys to be produced that encompass multiple real locations.
*
* @author Immortius
*/
class ModuleWatchService implements WatchService {
private final WatchService defaultWatchService;
private final ModuleFileSystem fileSystem;
private final Map<WatchKey, ModuleWatchKey> keyLookup = Maps.newHashMap();
ModuleWatchService(ModuleFileSystem fileSystem) throws IOException {
this.fileSystem = fileSystem;
this.defaultWatchService = FileSystems.getDefault().newWatchService();
}
@Override
public void close() throws IOException {
defaultWatchService.close();
keyLookup.clear();
}
@Override
public WatchKey poll() {
WatchKey polledKey = defaultWatchService.poll();
if (polledKey != null) {
synchronized (keyLookup) {
return keyLookup.get(polledKey);
}
}
return null;
}
@Override
public WatchKey poll(long timeout, TimeUnit unit) throws InterruptedException {
WatchKey polledKey = defaultWatchService.poll(timeout, unit);
if (polledKey != null) {
synchronized (keyLookup) {
return keyLookup.get(polledKey);
}
}
return null;
}
@Override
public WatchKey take() throws InterruptedException {
WatchKey polledKey = defaultWatchService.take();
if (polledKey != null) {
synchronized (keyLookup) {
return keyLookup.get(polledKey);
}
}
return null;
}
WatchKey register(ModulePath path, WatchEvent.Kind<?>[] events, WatchEvent.Modifier... modifiers) throws IOException {
if (!fileSystem.equals(path.getFileSystem())) {
throw new IllegalArgumentException("Path and WatchService belong to different file systems: '" + path.getFileSystem() + "' != '" + fileSystem + "'");
}
ModuleWatchKey result = new ModuleWatchKey(path, events, modifiers);
synchronized (keyLookup) {
for (WatchKey key : result.realKeys) {
keyLookup.put(key, result);
}
}
return result;
}
void clearInvalidKeys() {
synchronized (keyLookup) {
Iterator<Map.Entry<WatchKey, ModuleWatchKey>> iterator = keyLookup.entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry<WatchKey, ModuleWatchKey> entry = iterator.next();
if (!entry.getKey().isValid()) {
iterator.remove();
}
}
}
}
private class ModuleWatchKey implements WatchKey {
private List<WatchKey> realKeys = Lists.newArrayList();
private ModulePath path;
private WrapWatchEvent wrapper = new WrapWatchEvent<>();
public ModuleWatchKey(ModulePath path, WatchEvent.Kind<?>[] events, WatchEvent.Modifier[] modifiers) throws IOException {
this.path = path;
try {
for (Path realPath : path.getUnderlyingPaths()) {
if (realPath.getFileSystem().equals(FileSystems.getDefault())) {
WatchKey realKey = realPath.register(defaultWatchService, events, modifiers);
realKeys.add(realKey);
}
}
} catch (IOException e) {
throw new IOException("Error establishing WatchKey for " + path, e);
}
}
@Override
public boolean isValid() {
for (WatchKey key : realKeys) {
if (key.isValid()) {
return true;
}
}
return false;
}
@Override
@SuppressWarnings("unchecked")
public List<WatchEvent<?>> pollEvents() {
List<WatchEvent<?>> events = Lists.newArrayList();
for (WatchKey key : realKeys) {
if (key.isValid()) {
events.addAll(Collections2.transform(key.pollEvents(), wrapper));
}
}
return events;
}
@Override
public boolean reset() {
if (isValid()) {
for (WatchKey key : realKeys) {
key.reset();
}
return true;
}
return false;
}
@Override
public void cancel() {
Iterator<WatchKey> iterator = realKeys.iterator();
while (iterator.hasNext()) {
WatchKey next = iterator.next();
next.cancel();
iterator.remove();
}
clearInvalidKeys();
}
@Override
public Watchable watchable() {
return path;
}
private class WrapWatchEvent<T> implements Function<WatchEvent<T>, WatchEvent<T>> {
@Nullable
@Override
public WatchEvent<T> apply(@Nullable WatchEvent<T> input) {
return new ModuleWatchEvent<>(path, input);
}
}
}
private static class ModuleWatchEvent<T> implements WatchEvent<T> {
private ModulePath modulePath;
private WatchEvent<T> event;
public ModuleWatchEvent(ModulePath modulePath, WatchEvent<T> event) {
this.modulePath = modulePath;
this.event = event;
}
@Override
public Kind<T> kind() {
return event.kind();
}
@Override
public int count() {
return event.count();
}
@Override
@SuppressWarnings("unchecked")
public T context() {
T innerContext = event.context();
if (innerContext instanceof Path) {
Path result = modulePath;
for (Path part : ((Path) innerContext)) {
result = result.resolve(part.toString());
}
return (T) result;
} else {
return innerContext;
}
}
}
}