1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34 package info.magnolia.dirwatch;
35
36 import static java.nio.file.LinkOption.NOFOLLOW_LINKS;
37 import static java.nio.file.StandardWatchEventKinds.*;
38
39 import info.magnolia.cms.util.ExceptionUtil;
40 import info.magnolia.init.MagnoliaConfigurationProperties;
41 import info.magnolia.objectfactory.Components;
42
43 import java.io.IOException;
44 import java.nio.file.FileSystems;
45 import java.nio.file.FileVisitResult;
46 import java.nio.file.Files;
47 import java.nio.file.LinkOption;
48 import java.nio.file.Path;
49 import java.nio.file.SimpleFileVisitor;
50 import java.nio.file.WatchEvent;
51 import java.nio.file.WatchKey;
52 import java.nio.file.WatchService;
53 import java.nio.file.attribute.BasicFileAttributes;
54 import java.util.ArrayList;
55 import java.util.HashMap;
56 import java.util.List;
57 import java.util.Map;
58
59 import org.slf4j.Logger;
60 import org.slf4j.LoggerFactory;
61
62 import com.google.common.base.Predicate;
63
64
65
66
67
68
69
70
71
72
73 public class DirectoryWatcher implements Runnable {
74
75 private static final Logger log = LoggerFactory.getLogger(DirectoryWatcher.class);
76
77 private final WatchService watcher;
78 private final Map<WatchKey, Path> keys = new HashMap<>();
79
80 private final boolean recursive;
81 private final LinkOption[] visitorOptions;
82 private final List<WatcherRegistration> registrations = new ArrayList<>();
83 private final boolean devMode;
84
85 public DirectoryWatcher(boolean recursive, boolean followLinks, MagnoliaConfigurationProperties properties) throws IOException {
86 this.recursive = recursive;
87 this.visitorOptions = followLinks ? new LinkOption[]{NOFOLLOW_LINKS} : new LinkOption[0];
88 this.watcher = FileSystems.getDefault().newWatchService();
89 this.devMode = properties.getBooleanProperty("magnolia.develop");
90 }
91
92
93
94
95 @Deprecated
96 public DirectoryWatcher(boolean recursive, boolean followLinks) throws IOException {
97 this(recursive, followLinks, Components.getComponent(MagnoliaConfigurationProperties.class));
98 }
99
100 public void register(Path dir, Predicate<Path> filterPredicate, WatcherCallback callback) throws IOException {
101 if (recursive) {
102 log.debug("Scanning {}", dir);
103 registerRecursively(dir, filterPredicate);
104 log.debug("Done scanning {}", dir);
105 } else {
106 registerDirectory(dir);
107 }
108 registrations.add(new WatcherRegistration(dir, filterPredicate, callback));
109 }
110
111
112
113
114 protected void registerDirectory(Path dir) throws IOException {
115 final WatchKey key = dir.register(watcher, new WatchEvent.Kind[]{ENTRY_CREATE, ENTRY_DELETE, ENTRY_MODIFY}, getWatchEventModifiers(watcher));
116
117
118
119
120 if (log.isDebugEnabled()) {
121 Path prev = keys.get(key);
122 if (prev == null) {
123 log.debug("* register: {}", dir);
124 } else {
125 log.debug("* update: {} -> {}", prev, dir);
126 }
127 }
128 keys.put(key, dir);
129 }
130
131 protected WatchEvent.Modifier[] getWatchEventModifiers(WatchService watchService) {
132 return new WatchEvent.Modifier[0];
133 }
134
135
136
137
138 protected void registerRecursively(final Path start, final Predicate<Path> filterPredicate) throws IOException {
139
140
141 Files.walkFileTree(start, new SimpleFileVisitor<Path>() {
142
143 @Override
144 public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
145 if (!filterPredicate.apply(dir)) {
146 return FileVisitResult.SKIP_SUBTREE;
147 }
148
149 registerDirectory(dir);
150 return FileVisitResult.CONTINUE;
151 }
152 });
153 }
154
155 @Override
156 public void run() {
157 try {
158 while (!Thread.currentThread().isInterrupted()) {
159
160
161 final WatchKey key;
162 try {
163 key = watcher.take();
164 } catch (InterruptedException x) {
165 Thread.currentThread().interrupt();
166 return;
167 }
168
169 final Path dir = keys.get(key);
170 if (dir == null) {
171 log.debug("WatchKey not recognized!!");
172 continue;
173 }
174
175
176 final List<WatchEvent<?>> watchEvents = key.pollEvents();
177 log.debug(" {} events", watchEvents.size());
178
179 for (WatchEvent<?> event : watchEvents) {
180 final WatchEvent.Kind<?> kind = event.kind();
181 log.debug("event: {}", event);
182 log.debug("kind: {}", kind);
183
184
185 if (kind == OVERFLOW) {
186 continue;
187 }
188
189 final Path completePath = getCompletePath(key, event);
190 log.debug("--> {} : {}: {}", Thread.currentThread(), event.kind().name(), completePath);
191
192
193 if (Files.isDirectory(completePath, visitorOptions)) {
194 if (recursive && (kind == ENTRY_CREATE)) {
195
196
197 try {
198 for (WatcherRegistration registration : registrations) {
199 if (!completePath.startsWith(registration.getRootPath())) {
200 continue;
201 }
202 Predicate<Path> filterPredicate = registration.getFilterPredicate();
203 registerRecursively(completePath, filterPredicate);
204 }
205 } catch (IOException x) {
206 log.warn("Unable to register all subdirectories because of the following exception:", x);
207 }
208 }
209
210 log.debug(" --> watch keys {}", keys.values());
211 }
212
213
214 try {
215 processEvent(kind, completePath, event);
216 } catch (Throwable t) {
217 log.error("Exception when executing callback for {}: {}", event.context(), ExceptionUtil.exceptionToWords(t), t);
218
219 }
220 }
221
222
223 boolean valid = key.reset();
224 if (!valid) {
225 keys.remove(key);
226
227
228 if (keys.isEmpty()) {
229 break;
230 }
231 }
232 }
233
234 } catch (Throwable t) {
235 log.error("Exception occurred in DirectoryWatcher: {}", t, t);
236 throw t;
237 }
238 }
239
240
241
242
243
244
245
246
247 protected void processEvent(final WatchEvent.Kind<?> kind, final Path completePath, final WatchEvent<?> watchEvent) {
248 logEvent(kind, completePath);
249
250 if (kind != ENTRY_CREATE && kind != ENTRY_MODIFY && kind != ENTRY_DELETE) {
251 throw new RuntimeException("Unknown event type " + kind + " for " + completePath.toAbsolutePath().toString());
252 }
253
254 for (WatcherRegistration registration : registrations) {
255 if (!completePath.startsWith(registration.getRootPath())) {
256 continue;
257 }
258
259 WatcherCallback callback = registration.getCallback();
260 if (kind == ENTRY_CREATE) {
261 callback.added(completePath);
262 } else if (kind == ENTRY_DELETE) {
263 callback.removed(completePath);
264 } else if (kind == ENTRY_MODIFY) {
265 callback.modified(completePath);
266 }
267 }
268 }
269
270
271
272
273
274
275
276
277 private void logEvent(WatchEvent.Kind<?> kind, Path completePath) {
278 String message;
279 final String resourceType;
280 if (!Files.exists(completePath)) {
281 resourceType = "File resource(s)";
282 } else {
283 resourceType = Files.isDirectory(completePath) ? "Directory" : "File resource";
284 }
285
286 if (kind == ENTRY_CREATE) {
287 message = "{} added at [{}]";
288 } else if (kind == ENTRY_DELETE) {
289 message = "{} deleted at [{}]";
290 } else if (kind == ENTRY_MODIFY) {
291 message = "{} modified at [{}]";
292 } else {
293 message = String.format("{} event of unhandled type (%s) occurred at [{}]", kind);
294 }
295
296 if (devMode) {
297 log.info(message, resourceType, completePath);
298 } else {
299 log.debug(message, resourceType, completePath);
300 }
301 }
302
303
304
305
306 private Path getCompletePath(WatchKey key, WatchEvent<?> watchEvent) {
307
308 final Path path = cast(watchEvent).context();
309 final Path parent = (Path) key.watchable();
310 return parent.resolve(path);
311 }
312
313 @SuppressWarnings("unchecked")
314 private WatchEvent<Path> cast(WatchEvent<?> event) {
315 return (WatchEvent<Path>) event;
316 }
317
318 private static class WatcherRegistration {
319
320 private final Path rootPath;
321 private final Predicate<Path> filterPredicate;
322 private final WatcherCallback callback;
323
324 public WatcherRegistration(Path rootPath, Predicate<Path> filterPredicate, WatcherCallback callback) {
325 this.rootPath = rootPath;
326 this.filterPredicate = filterPredicate;
327 this.callback = callback;
328 }
329
330 public Path getRootPath() {
331 return rootPath;
332 }
333
334 public Predicate<Path> getFilterPredicate() {
335 return filterPredicate;
336 }
337
338 public WatcherCallback getCallback() {
339 return callback;
340 }
341 }
342 }