/* * opsu! - an open-source osu! client * Copyright (C) 2014, 2015 Jeffrey Han * * opsu! is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * opsu! is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with opsu!. If not, see . */ package itdelatrisu.opsu.beatmap; import itdelatrisu.opsu.ErrorHandler; import itdelatrisu.opsu.Options; import java.io.IOException; import java.nio.file.ClosedWatchServiceException; import java.nio.file.FileSystems; import java.nio.file.FileVisitResult; import java.nio.file.Files; import java.nio.file.LinkOption; import java.nio.file.Path; import java.nio.file.SimpleFileVisitor; import java.nio.file.StandardWatchEventKinds; import java.nio.file.WatchEvent; import java.nio.file.WatchKey; import java.nio.file.WatchService; import java.nio.file.attribute.BasicFileAttributes; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import org.newdawn.slick.util.Log; /* * Copyright (c) 2008, 2010, Oracle and/or its affiliates. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * - Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * - Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * - Neither the name of Oracle nor the names of its * contributors may be used to endorse or promote products derived * from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ /** * Watches the beatmap directory tree for changes. * * @author The Java Tutorials (http://docs.oracle.com/javase/tutorial/essential/io/examples/WatchDir.java) (base) */ public class BeatmapWatchService { /** Beatmap watcher service instance. */ private static BeatmapWatchService ws; /** * Creates a new watch service instance (overwriting any previous instance), * registers the beatmap directory, and starts processing events. */ public static void create() { // close the existing watch service destroy(); // create a new watch service try { ws = new BeatmapWatchService(); ws.register(Options.getBeatmapDir().toPath()); } catch (IOException e) { ErrorHandler.error("An I/O exception occurred while creating the watch service.", e, true); return; } // start processing events ws.start(); } /** * Destroys the watch service instance, if any. * Subsequent calls to {@link #get()} will return {@code null}. */ public static void destroy() { if (ws == null) return; try { ws.watcher.close(); ws.service.shutdownNow(); ws = null; } catch (IOException e) { ws = null; ErrorHandler.error("An I/O exception occurred while closing the previous watch service.", e, true); } } /** * Returns the single instance of this class. */ public static BeatmapWatchService get() { return ws; } /** Watch service listener interface. */ public interface BeatmapWatchServiceListener { /** Indication that an event was received. */ public void eventReceived(WatchEvent.Kind kind, Path child); } /** The list of listeners. */ private static final List listeners = new ArrayList(); /** * Adds a listener. * @param listener the listener to add */ public static void addListener(BeatmapWatchServiceListener listener) { listeners.add(listener); } /** * Removes a listener. * @param listener the listener to remove */ public static void removeListener(BeatmapWatchServiceListener listener) { listeners.remove(listener); } /** * Removes all listeners. */ public static void removeListeners() { listeners.clear(); } /** The watch service. */ private final WatchService watcher; /** The WatchKey -> Path mapping for registered directories. */ private final Map keys; /** The Executor. */ private ExecutorService service; /** * Creates the WatchService. * @throws IOException if an I/O error occurs */ private BeatmapWatchService() throws IOException { this.watcher = FileSystems.getDefault().newWatchService(); this.keys = new ConcurrentHashMap(); } /** * Register the given directory with the WatchService. * @param dir the directory to register * @throws IOException if an I/O error occurs */ private void register(Path dir) throws IOException { WatchKey key = dir.register(watcher, StandardWatchEventKinds.ENTRY_CREATE, StandardWatchEventKinds.ENTRY_DELETE, StandardWatchEventKinds.ENTRY_MODIFY); keys.put(key, dir); } /** * Register the given directory, and all its sub-directories, with the WatchService. * @param start the root directory to register */ public void registerAll(final Path start) { try { Files.walkFileTree(start, new SimpleFileVisitor() { @Override public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException { try { register(dir); } catch (IOException e) { Log.warn(String.format("Failed to register path '%s' with the watch service.", dir.toString()), e); } return FileVisitResult.CONTINUE; } }); } catch (IOException e) { Log.warn(String.format("Failed to register paths from root directory '%s' with the watch service.", start.toString()), e); } } @SuppressWarnings("unchecked") private static WatchEvent cast(WatchEvent event) { return (WatchEvent) event; } /** * Start processing events in a new thread. */ private void start() { if (service != null) return; this.service = Executors.newCachedThreadPool(); service.submit(new Runnable() { @Override public void run() { ws.processEvents(); } }); } /** * Process all events for keys queued to the watcher */ private void processEvents() { while (true) { // wait for key to be signaled WatchKey key; try { key = watcher.take(); } catch (InterruptedException | ClosedWatchServiceException e) { return; } Path dir = keys.get(key); if (dir == null) continue; for (WatchEvent event : key.pollEvents()) { WatchEvent.Kind kind = event.kind(); if (kind == StandardWatchEventKinds.OVERFLOW) continue; // context for directory entry event is the file name of entry WatchEvent ev = cast(event); Path name = ev.context(); Path child = dir.resolve(name); //System.out.printf("%s: %s\n", kind.name(), child); // fire listeners for (BeatmapWatchServiceListener listener : listeners) listener.eventReceived(kind, child); // if directory is created, then register it and its sub-directories if (kind == StandardWatchEventKinds.ENTRY_CREATE) { if (Files.isDirectory(child, LinkOption.NOFOLLOW_LINKS)) registerAll(child); } } // reset key and remove from set if directory no longer accessible if (!key.reset()) { keys.remove(key); if (keys.isEmpty()) break; // all directories are inaccessible } } } }