/*
* Copyright 2012 Jason Miller
*
* 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 jj.resource;
import java.io.IOException;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.HashSet;
import java.util.concurrent.atomic.AtomicBoolean;
import javax.inject.Inject;
import javax.inject.Singleton;
import jj.ServerStarting;
import jj.ServerStopping;
import jj.configuration.ConfigurationLoaded;
import jj.event.Listener;
import jj.event.Publisher;
import jj.event.Subscriber;
import jj.execution.ServerTask;
import jj.execution.TaskRunner;
import jj.logging.Emergency;
@Singleton
@Subscriber
class ResourceWatchServiceLoop extends ServerTask {
private final ResourceWatchSwitch resourceWatchSwitch;
private final FileWatcher watcher;
private final ResourceCache resourceCache;
private final ResourceLoader resourceLoader;
private final Publisher publisher;
private final TaskRunner taskRunner;
private final AtomicBoolean run = new AtomicBoolean();
@Inject
ResourceWatchServiceLoop(
ResourceWatchSwitch resourceWatchSwitch,
ResourceCache resourceCache,
ResourceLoader resourceLoader,
FileWatcher watcher,
Publisher publisher,
TaskRunner taskRunner
) throws IOException {
super(ResourceWatchServiceLoop.class.getSimpleName());
this.resourceWatchSwitch = resourceWatchSwitch;
this.watcher = watcher;
this.resourceCache = resourceCache;
this.resourceLoader = resourceLoader;
this.publisher = publisher;
this.taskRunner = taskRunner;
}
private void watch(FileSystemResource resource) {
if (resource != null) {
Path path = resource.path();
if (path.getFileSystem() == FileSystems.getDefault()) {
watcher.watch(resource.isDirectory() ? path : path.getParent());
}
}
}
@Listener
void on(ResourceLoaded event) {
if (run.get() && FileSystemResource.class.isAssignableFrom(event.type())) {
watch((FileSystemResource)event.resourceReference.get());
}
}
@Listener
void on(ServerStarting ignored) {
if (resourceWatchSwitch.runFileWatcher() && run.compareAndSet(false, watcher.start()) && run.get()) {
start();
}
}
@Listener
void on(ServerStopping ignored) {
if (run.getAndSet(false)) {
stop();
}
}
@Listener
void on(ConfigurationLoaded ignored) {
if (resourceWatchSwitch.runFileWatcher() && !run.get() && !run.getAndSet(watcher.start())) {
start();
} else if (!resourceWatchSwitch.runFileWatcher() && run.getAndSet(false)) {
stop();
}
}
private void start() {
taskRunner.execute(this);
}
private void stop() {
watcher.stop();
interrupt();
}
@Override
protected void run() throws Exception {
while (run.get()) {
watcher.awaitChangedPaths().forEach((path, action) -> {
switch (action) {
case Create:
publisher.publish(
Files.isDirectory(path) ?
new DirectoryCreation(path) :
new FileCreation(path)
);
break;
case Delete:
case Modify:
resourceCache.findAllByPath(path).forEach(resource -> {
ResourceReloadOrganizer rro = new ResourceReloadOrganizer(resource, action);
rro.deletions.forEach(this::remove);
rro.reloads.forEach(this::reload);
});
break;
// the error case!
case Unknown:
default:
publisher.publish(new Emergency("unknown file operation on {}", path));
}
});
}
}
private void remove(AbstractResource<?> resource) {
if (resourceCache.remove(resource)) {
resource.kill();
}
}
private <T> void reload(final AbstractResource<T> resource) {
remove(resource);
resourceLoader.loadResource(resource.identifier());
}
/**
* A helper to organize the reloading behavior - we just want to delete things, for the most part,
* and only trigger reloads on objects that self-identify as roots
* @author jason
*
*/
private static class ResourceReloadOrganizer {
final HashSet<AbstractResource<?>> deletions = new HashSet<>();
final HashSet<AbstractResource<?>> reloads = new HashSet<>();
ResourceReloadOrganizer(Resource<?> base, FileWatcher.Action action) {
placeResource((AbstractResource<?>)base, action == FileWatcher.Action.Delete);
}
private void placeResource(AbstractResource<?> resource, boolean delete) {
// sorry for the javascripty java but the test passes!
if ((delete || resource.removeOnReload() ? deletions : reloads).add(resource)) {
// if we haven't seen this one before, traverse it
for (AbstractResource<?> dependent : resource.dependents()) {
placeResource(dependent, false);
}
}
}
@Override
public String toString() {
return "Removals: " + deletions + "\n" + "Reloads" + reloads + "\n";
}
}
}