1+ /*
2+ * WorldEdit, a Minecraft world manipulation toolkit
3+ * Copyright (C) sk89q <http://www.sk89q.com>
4+ * Copyright (C) WorldEdit team and contributors
5+ *
6+ * This program is free software: you can redistribute it and/or modify
7+ * it under the terms of the GNU General Public License as published by
8+ * the Free Software Foundation, either version 3 of the License, or
9+ * (at your option) any later version.
10+ *
11+ * This program is distributed in the hope that it will be useful,
12+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
13+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14+ * GNU General Public License for more details.
15+ *
16+ * You should have received a copy of the GNU General Public License
17+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
18+ */
19+
20+ package com .sk89q .worldedit .util .io .file ;
21+
22+ import com .google .common .collect .BiMap ;
23+ import com .google .common .collect .HashBiMap ;
24+ import com .sk89q .worldedit .internal .util .LogManagerCompat ;
25+ import org .apache .logging .log4j .Logger ;
26+
27+ import java .io .IOException ;
28+ import java .nio .file .ClosedWatchServiceException ;
29+ import java .nio .file .Files ;
30+ import java .nio .file .Path ;
31+ import java .nio .file .StandardWatchEventKinds ;
32+ import java .nio .file .WatchEvent ;
33+ import java .nio .file .WatchKey ;
34+ import java .nio .file .WatchService ;
35+ import java .util .function .Consumer ;
36+
37+ /**
38+ * Helper class that allows to recursively monitor a directory for changed (created / deleted) files / folders.
39+ *
40+ * @warning File- and Folder events might be sent multiple times. Users of this class need to employ their own
41+ * deduplication!
42+ */
43+ public class RecursiveDirectoryWatcher {
44+
45+ /**
46+ * Base-Class for all DirEntry change events.
47+ */
48+ public static class DirEntryChangeEvent {
49+ private Path path ;
50+
51+ public DirEntryChangeEvent (Path path ) {
52+ this .path = path ;
53+ }
54+
55+ public Path getPath () {
56+ return path ;
57+ }
58+ }
59+
60+ /**
61+ * Event signaling the creation of a new file.
62+ */
63+ public static class FileCreatedEvent extends DirEntryChangeEvent {
64+ public FileCreatedEvent (Path path ) {
65+ super (path );
66+ }
67+ }
68+
69+ /**
70+ * Event signaling the deletion of a file.
71+ */
72+ public static class FileDeletedEvent extends DirEntryChangeEvent {
73+ public FileDeletedEvent (Path path ) {
74+ super (path );
75+ }
76+ }
77+
78+ /**
79+ * Event signaling the creation of a new directory.
80+ */
81+ public static class DirectoryCreatedEvent extends DirEntryChangeEvent {
82+ public DirectoryCreatedEvent (Path path ) {
83+ super (path );
84+ }
85+ }
86+
87+ /**
88+ * Event signaling the deletion of a directory.
89+ */
90+ public static class DirectoryDeletedEvent extends DirEntryChangeEvent {
91+ public DirectoryDeletedEvent (Path path ) {
92+ super (path );
93+ }
94+ }
95+
96+
97+ private static final Logger LOGGER = LogManagerCompat .getLogger ();
98+
99+ private final Path root ;
100+ private final WatchService watchService ;
101+ private Thread watchThread ;
102+ private Consumer <DirEntryChangeEvent > eventConsumer ;
103+ private BiMap <WatchKey , Path > watchRootMap = HashBiMap .create ();
104+
105+ private RecursiveDirectoryWatcher (Path root , WatchService watchService ) {
106+ this .root = root ;
107+ this .watchService = watchService ;
108+ }
109+
110+ /**
111+ * Create a new recursive directory watcher for the given root folder.
112+ * You have to call @see start() before the instance starts monitoring.
113+ *
114+ * @param root Folder to watch for changed files recursively.
115+ * @return A new RecursiveDirectoryWatcher instance, monitoring the given root folder.
116+ * @throws IOException If creating the watcher failed, e.g. due to root not existing.
117+ */
118+ public static RecursiveDirectoryWatcher create (Path root ) throws IOException {
119+ WatchService watchService = root .getFileSystem ().newWatchService ();
120+ return new RecursiveDirectoryWatcher (root , watchService );
121+ }
122+
123+ private void registerFolderWatchAndScanInitially (Path root ) throws IOException {
124+ WatchKey watchKey = root .register (watchService , StandardWatchEventKinds .ENTRY_CREATE , StandardWatchEventKinds .ENTRY_DELETE );
125+ LOGGER .debug ("Watch registered: " + root );
126+ watchRootMap .put (watchKey , root );
127+ eventConsumer .accept (new DirectoryCreatedEvent (root ));
128+ for (Path path : Files .newDirectoryStream (root )) {
129+ if (Files .isDirectory (path )) {
130+ registerFolderWatchAndScanInitially (path );
131+ } else {
132+ eventConsumer .accept (new FileCreatedEvent (path ));
133+ }
134+ }
135+ }
136+
137+ /**
138+ * Make this RecursiveDirectoryWatcher instance start monitoring the root folder it was created on.
139+ * When this is called, RecursiveDirectoryWatcher will send initial notifications for the entire
140+ * file structure in the configured root.
141+ * @param eventConsumer The lambda that's fired for every file event.
142+ */
143+ public void start (Consumer <DirEntryChangeEvent > eventConsumer ) {
144+ this .eventConsumer = eventConsumer ;
145+ watchThread = new Thread (() -> {
146+ LOGGER .debug ("RecursiveDirectoryWatcher::EventConsumer started" );
147+
148+ try {
149+ registerFolderWatchAndScanInitially (root );
150+ } catch (IOException e ) { e .printStackTrace (); }
151+
152+ try {
153+ WatchKey watchKey ;
154+ while (true ) {
155+ try {
156+ watchKey = watchService .take ();
157+ } catch (InterruptedException e ) { break ; }
158+
159+ for (WatchEvent <?> event : watchKey .pollEvents ()) {
160+ WatchEvent .Kind <?> kind = event .kind ();
161+ if (kind .equals (StandardWatchEventKinds .OVERFLOW )) {
162+ LOGGER .warn ("RecursiveDirectoryWatcher Seems like we can't keep up with updates" );
163+ continue ;
164+ }
165+ // make sure to work with an absolute path
166+ Path path = (Path ) event .context ();
167+ Path parentPath = watchRootMap .get (watchKey );
168+ path = parentPath .resolve (path );
169+
170+ if (kind .equals (StandardWatchEventKinds .ENTRY_CREATE )) {
171+ if (Files .isDirectory (path )) { // new subfolder created, create watch for it
172+ try {
173+ registerFolderWatchAndScanInitially (path );
174+ } catch (IOException e ) { e .printStackTrace (); }
175+ } else { // new file created
176+ eventConsumer .accept (new FileCreatedEvent (path ));
177+ }
178+ } else if (kind .equals (StandardWatchEventKinds .ENTRY_DELETE )) {
179+ // When we are notified about a deleted entry, we can't simply ask the filesystem
180+ // whether the entry is a file or a folder. But we have our watchRootMap, that stores
181+ // one WatchKey per (sub)folder, so we can just ask it.
182+ if (watchRootMap .containsValue (path )) { // was a folder
183+ LOGGER .debug ("Watch unregistered: " + path );
184+ WatchKey obsoleteSubfolderWatchKey = watchRootMap .inverse ().get (path );
185+ // stop listening to changes from deleted dir
186+ obsoleteSubfolderWatchKey .cancel ();
187+ watchRootMap .remove (obsoleteSubfolderWatchKey );
188+ eventConsumer .accept (new DirectoryDeletedEvent (path ));
189+ } else { // was a file
190+ eventConsumer .accept (new FileDeletedEvent (path ));
191+ }
192+ }
193+ }
194+
195+ if (!watchKey .reset ()) {
196+ watchRootMap .remove (watchKey );
197+ if (watchRootMap .isEmpty ()) {
198+ break ; // nothing left to watch
199+ }
200+ }
201+ }
202+ } catch (ClosedWatchServiceException ignored ) { }
203+ LOGGER .debug ("RecursiveDirectoryWatcher::EventConsumer exited" );
204+ });
205+ watchThread .setName ("RecursiveDirectoryWatcher" );
206+ watchThread .start ();
207+ }
208+
209+ /**
210+ * Stop this RecursiveDirectoryWatcher instance and wait for it to be completely shut down.
211+ * @warning RecursiveDirectoryWatcher is not reusable!
212+ */
213+ public void stop () {
214+ try {
215+ watchService .close ();
216+ } catch (IOException e ) { e .printStackTrace (); }
217+ if (watchThread != null ) {
218+ try {
219+ watchThread .join ();
220+ } catch (InterruptedException e ) { e .printStackTrace (); }
221+ watchThread = null ;
222+ }
223+ eventConsumer = null ;
224+ }
225+
226+ }
0 commit comments