/** * 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.jooby.filewatcher; import java.nio.file.FileSystems; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.StandardWatchEventKinds; import java.nio.file.WatchService; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.function.Consumer; import java.util.function.Function; import org.jooby.Env; import org.jooby.Jooby.Module; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.common.collect.ImmutableList; import com.google.inject.Binder; import com.google.inject.multibindings.Multibinder; import com.typesafe.config.Config; import com.typesafe.config.ConfigFactory; import javaslang.CheckedFunction1; import javaslang.CheckedFunction2; import javaslang.control.Try.CheckedConsumer; /** * <h1>file watcher</h1> * <p> * Watches for file system changes or event. It uses a watch service to monitor a directory for * changes so that it can update its display of the list of files when files are created or deleted. * </p> * * <h2>usage</h2> * <p> * Watch <code>mydir</code> and listen for create, modify or delete event on all files under it: * </p> * * <pre>{@code * { * use(new FileWatcher() * .register(Paths.get("mydir"), (kind, path) -> { * log.info("found {} on {}", kind, path); * }); * ); * } * }</pre> * * <h2>file handler</h2> * <p> * You can specify a {@link FileEventHandler} instance or class while registering a path: * </p> * * <p> * Instance: * </p> * <pre>{@code * { * use(new FileWatcher() * .register(Paths.get("mydir"), (kind, path) -> { * log.info("found {} on {}", kind, path); * }); * ); * } * }</pre> * * <p> * Class reference: * </p> * * <pre>{@code * { * use(new FileWatcher() * .register(Paths.get("mydir"), MyFileEventHandler.class) * ); * } * }</pre> * * <p> * Worth to mention that <code>MyFileEventHandler</code> will be provided by Guice. * </p> * * <h2>options</h2> * <p> * You can specify a couple of options at registration time: * </p> * <pre>{@code * { * use(new FileWatcher( * .register(Paths.get("mydir"), MyFileEventHandler.class, options -> { * options.kind(StandardWatchEventKinds.ENTRY_MODIFY) * .recursive(false) * .includes("*.java"); * }); * )); * } * }</pre> * * <p> * 1. Here we listen for {@link StandardWatchEventKinds#ENTRY_MODIFY} (we don't care about create or * delete). * </p> * <p> * 2. We turn off recursive watching, only direct files are detected. * </p> * <p> * 3. We want <code>.java</code> files and we ignore any other files. * </p> * * <h2>configuration file</h2> * <p> * In addition to do it programmatically, you can do it via configuration properties: * </p> * * <pre>{@code * { * use(new FileWatcher() * .register("mydirproperty", MyEventHandler.class) * ); * } * }</pre> * * <p> * The <code>mydirproperty</code> property must be present in your <code>.conf</code> file. * </p> * * <p> * But of course, you can entirely register paths from <code>.conf</code> file: * </p> * * <pre>{@code * filewatcher { * register { * path: "mydir" * handler: "org.example.MyFileEventHandler" * kind: "ENTRY_MODIFY" * includes: "*.java" * recursive: false * } * } * }</pre> * * <p> * Multiple paths are supported using array notation: * </p> * * <pre>{@code * filewatcher { * register: [{ * path: "mydir1" * handler: "org.example.MyFileEventHandler" * }, { * path: "mydir2" * handler: "org.example.MyFileEventHandler" * }] * } * }</pre> * * <p> * Now use the module: * </p> * <pre>{@code * { * use(new FileWatcher()); * } * }</pre> * * <p> * The {@link FileWatcher} module read the <code>filewatcher.register</code> property and setup * everything. * </p> * * @author edgar */ public class FileWatcher implements Module { private static final Consumer<FileEventOptions> EMPTY = it -> { }; /** * The logging system. */ private final Logger log = LoggerFactory.getLogger(getClass()); private final List<CheckedFunction2<Config, Binder, FileEventOptions>> bindings = new ArrayList<>(); private WatchService watcher; /** * Register the given directory tree and execute the given handler whenever a file/folder has been * created, modified or delete. * * @param path Directory to register. * @param handler Handler to execute. * @return This module. */ public FileWatcher register(final Path path, final Class<? extends FileEventHandler> handler) { return register(path, handler, EMPTY); } /** * Register the given directory tree and execute the given handler whenever a file/folder has been * created, modified or delete. The configurer callback allow you to customize default event * kinds, filter specific file types and restrict the watch scope lookup to direct files. * * @param path Directory to register. * @param handler Handler to execute. * @param configurer Configurer callback. * @return This module. * @see FileEventOptions */ public FileWatcher register(final Path path, final Class<? extends FileEventHandler> handler, final Consumer<FileEventOptions> configurer) { return register(c -> path, p -> new FileEventOptions(p, handler), configurer); } /** * Register the given directory tree and execute the given handler whenever a file/folder has been * created, modified or delete. * * @param path Directory to register. * @param handler Handler to execute. * @return This module. */ public FileWatcher register(final Path path, final FileEventHandler handler) { return register(c -> path, p -> new FileEventOptions(p, handler), EMPTY); } /** * Register the given directory tree and execute the given handler whenever a file/folder has been * created, modified or delete. The configurer callback allow you to customize default event * kinds, filter specific file types and restrict the watch scope lookup to direct files. * * @param path Directory to register. * @param handler Handler to execute. * @param configurer Configurer callback. * @return This module. * @see FileEventOptions */ public FileWatcher register(final Path path, final FileEventHandler handler, final Consumer<FileEventOptions> configurer) { return register(c -> path, p -> new FileEventOptions(p, handler), configurer); } /** * Register the given directory tree and execute the given handler whenever a file/folder has been * created, modified or delete. * * @param property Directory to register. * @param handler Handler to execute. * @return This module. */ public FileWatcher register(final String property, final FileEventHandler handler) { return register(c -> Paths.get(c.getString(property)), p -> new FileEventOptions(p, handler), EMPTY); } /** * Register the given directory tree and execute the given handler whenever a file/folder has been * created, modified or delete. The configurer callback allow you to customize default event * kinds, filter specific file types and restrict the watch scope lookup to direct files. * * @param property Directory to register. * @param handler Handler to execute. * @param configurer Configurer callback. * @return This module. * @see FileEventOptions */ public FileWatcher register(final String property, final FileEventHandler handler, final Consumer<FileEventOptions> configurer) { return register(c -> Paths.get(c.getString(property)), p -> new FileEventOptions(p, handler), configurer); } /** * Register the given directory tree and execute the given handler whenever a file/folder has been * created, modified or delete. * * @param property Directory to register. * @param handler Handler to execute. * @return This module. */ public FileWatcher register(final String property, final Class<? extends FileEventHandler> handler) { return register(property, handler, EMPTY); } /** * Register the given directory tree and execute the given handler whenever a file/folder has been * created, modified or delete. The configurer callback allow you to customize default event * kinds, filter specific file types and restrict the watch scope lookup to direct files. * * @param property Directory to register. * @param handler Handler to execute. * @param configurer Configurer callback. * @return This module. * @see FileEventOptions */ public FileWatcher register(final String property, final Class<? extends FileEventHandler> handler, final Consumer<FileEventOptions> configurer) { return register(c -> Paths.get(c.getString(property)), path -> new FileEventOptions(path, handler), configurer); } private FileWatcher register(final Function<Config, Path> provider, final CheckedFunction1<Path, FileEventOptions> handler, final Consumer<FileEventOptions> configurer) { bindings.add((conf, binder) -> { Path path = provider.apply(conf); FileEventOptions options = handler.apply(path); configurer.accept(options); return register(binder, options); }); return this; } private FileEventOptions register(final Binder binder, final FileEventOptions options) { Multibinder.newSetBinder(binder, FileEventOptions.class) .addBinding() .toInstance(options); return options; } @Override public void configure(final Env env, final Config conf, final Binder binder) throws Throwable { this.watcher = FileSystems.getDefault().newWatchService(); binder.bind(WatchService.class).toInstance(watcher); List<FileEventOptions> paths = new ArrayList<>(); paths(env.getClass().getClassLoader(), conf, "filewatcher.register", options -> { paths.add(register(binder, options)); }); for (CheckedFunction2<Config, Binder, FileEventOptions> binding : bindings) { paths.add(binding.apply(conf, binder)); } binder.bind(FileMonitor.class).asEagerSingleton(); paths.forEach(it -> log.info("Watching: {}", it)); } protected boolean empty() { return bindings.isEmpty(); } @SuppressWarnings({"unchecked", "rawtypes" }) private void paths(final ClassLoader loader, final Config conf, final String name, final Consumer<FileEventOptions> callback) throws Throwable { list(conf, name, value -> { Config coptions = ConfigFactory.parseMap((Map) value); Class handler = loader.loadClass(coptions.getString("handler")); Path path = Paths.get(coptions.getString("path")); FileEventOptions options = new FileEventOptions(path, handler); list(coptions, "kind", it -> options.kind(new WatchEventKind(it.toString()))); list(coptions, "modifier", it -> options.modifier(new WatchEventModifier(it.toString()))); list(coptions, "includes", it -> options.includes(it.toString())); list(coptions, "recursive", it -> options.recursive(Boolean.valueOf(it.toString()))); callback.accept(options); }); } @SuppressWarnings("rawtypes") private void list(final Config conf, final String name, final CheckedConsumer<Object> callback) throws Throwable { if (conf.hasPath(name)) { Object value = conf.getAnyRef(name); List values = value instanceof List ? (List) value : ImmutableList.of(value); for (Object it : values) { callback.accept(it); } } } }