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
42 import java.io.IOException;
43 import java.nio.file.FileSystems;
44 import java.nio.file.FileVisitOption;
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.Collections;
56 import java.util.HashMap;
57 import java.util.List;
58 import java.util.Map;
59 import java.util.function.Predicate;
60
61 import org.apache.commons.lang3.EnumUtils;
62 import org.apache.commons.lang3.StringUtils;
63 import org.slf4j.Logger;
64 import org.slf4j.LoggerFactory;
65
66
67
68
69
70
71
72
73
74
75 public class DirectoryWatcher implements Runnable {
76
77 private static final Logger log = LoggerFactory.getLogger(DirectoryWatcher.class);
78
79 private static final String WATCHER_SENSITIVITY = "magnolia.resources.watcher.sensitivity";
80 private static final String DEFAULT_WATCHER_SENSITIVITY_VALUE = "high";
81
82 private final WatchService watcher;
83 private final Map<WatchKey, Path> keys = new HashMap<>();
84
85 private final boolean recursive;
86 private final boolean followSymLinks;
87 private final List<WatcherRegistration> registrations = Collections.synchronizedList(new ArrayList<>());
88 private final boolean devMode;
89 private final String watcherSensitivity;
90
91 public DirectoryWatcher(boolean recursive, boolean followLinks, MagnoliaConfigurationProperties properties) throws IOException {
92 this.recursive = recursive;
93 this.followSymLinks = followLinks;
94 this.watcher = FileSystems.getDefault().newWatchService();
95 this.devMode = properties.getBooleanProperty("magnolia.develop");
96 this.watcherSensitivity = getWatcherSensitivity(properties.getProperty(WATCHER_SENSITIVITY), DEFAULT_WATCHER_SENSITIVITY_VALUE);
97 }
98
99 public void register(Path dir, Predicate<Path> filterPredicate, WatcherCallback callback) throws IOException {
100 if (recursive) {
101 log.debug("Scanning {}", dir);
102 registerRecursively(dir, filterPredicate);
103 log.debug("Done scanning {}", dir);
104 } else {
105 registerDirectory(dir);
106 }
107 registrations.add(new WatcherRegistration(dir, filterPredicate, callback));
108 }
109
110
111
112
113 protected void registerDirectory(Path dir) throws IOException {
114 final WatchKey key = dir.register(watcher, new WatchEvent.Kind[]{ENTRY_CREATE, ENTRY_DELETE, ENTRY_MODIFY}, getWatchEventModifiers(watcher));
115
116
117
118
119 if (log.isDebugEnabled()) {
120 Path prev = keys.get(key);
121 if (prev == null) {
122 log.debug("* register: {}", dir);
123 } else {
124 log.debug("* update: {} -> {}", prev, dir);
125 }
126 }
127 keys.put(key, dir);
128 }
129
130 protected WatchEvent.Modifier[] getWatchEventModifiers(WatchService watchService) {
131
132 try {
133
134 Class<?> pollingWatchService = Class.forName("sun.nio.fs.PollingWatchService");
135 if (pollingWatchService.isInstance(watchService)) {
136
137
138 Class clazz = Class.forName("com.sun.nio.file.SensitivityWatchEventModifier");
139
140 WatchEvent.Modifier sensitivityModifier = (WatchEvent.Modifier) EnumUtils.getEnum(clazz, StringUtils.upperCase(watcherSensitivity));
141 if (sensitivityModifier == null) {
142 sensitivityModifier = (WatchEvent.Modifier) EnumUtils.getEnum(clazz, "HIGH");
143 }
144 return new WatchEvent.Modifier[] { sensitivityModifier };
145 }
146 } catch (ClassNotFoundException ignored) {
147
148 }
149 return new WatchEvent.Modifier[0];
150 }
151
152
153
154
155 protected void registerRecursively(final Path start, final Predicate<Path> filterPredicate) throws IOException {
156
157
158 Files.walkFileTree(start, this.followSymLinks ? Collections.singleton(FileVisitOption.FOLLOW_LINKS) : Collections.emptySet(), Integer.MAX_VALUE, new SimpleFileVisitor<Path>() {
159
160 @Override
161 public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
162 if (!filterPredicate.test(dir)) {
163 return FileVisitResult.SKIP_SUBTREE;
164 }
165
166 registerDirectory(dir);
167 return FileVisitResult.CONTINUE;
168 }
169
170 @Override
171 public FileVisitResult visitFileFailed(Path file, IOException e) throws IOException {
172 log.warn("Visiting failed for {}", file);
173 return FileVisitResult.SKIP_SUBTREE;
174 }
175 });
176 }
177
178 @Override
179 public void run() {
180 try {
181 while (!Thread.currentThread().isInterrupted()) {
182
183
184 final WatchKey key;
185 try {
186 key = watcher.take();
187 } catch (InterruptedException x) {
188 Thread.currentThread().interrupt();
189 return;
190 }
191
192 final Path dir = keys.get(key);
193 if (dir == null) {
194 log.debug("WatchKey not recognized!!");
195 continue;
196 }
197
198
199 final List<WatchEvent<?>> watchEvents = key.pollEvents();
200 log.debug(" {} events", watchEvents.size());
201
202 for (WatchEvent<?> event : watchEvents) {
203 final WatchEvent.Kind<?> kind = event.kind();
204 log.debug("event: {}", event);
205 log.debug("kind: {}", kind);
206
207
208 if (kind == OVERFLOW) {
209 continue;
210 }
211
212 final Path completePath = getCompletePath(dir, event);
213 log.debug("--> {} : {}: {}", Thread.currentThread(), event.kind().name(), completePath);
214
215
216 if (Files.isDirectory(completePath, this.followSymLinks ? new LinkOption[0] : new LinkOption[] {NOFOLLOW_LINKS})) {
217 if (recursive && (kind == ENTRY_CREATE || kind == ENTRY_MODIFY)) {
218
219
220 synchronized (registrations) {
221
222 try {
223 for (WatcherRegistration registration : registrations) {
224 if (!completePath.startsWith(registration.getRootPath())) {
225 continue;
226 }
227 Predicate<Path> filterPredicate = registration.getFilterPredicate();
228
229 if (!filterPredicate.test(dir)) {
230 continue;
231 }
232
233 registerRecursively(completePath, filterPredicate);
234 }
235 } catch (IOException x) {
236 log.warn("Unable to register all subdirectories because of the following exception:", x);
237 }
238 }
239 }
240
241 log.debug(" --> watch keys {}", keys.values());
242 }
243
244
245 try {
246 processEvent(kind, completePath, event);
247 } catch (Throwable t) {
248 log.error("Exception when executing callback for {}: {}", event.context(), ExceptionUtil.exceptionToWords(t), t);
249
250 }
251 }
252
253
254 boolean valid = key.reset();
255 if (!valid) {
256 keys.remove(key);
257
258
259 if (keys.isEmpty()) {
260 break;
261 }
262 }
263 }
264
265 } catch (Throwable t) {
266 log.error("Exception occurred in DirectoryWatcher: {}", t, t);
267 throw t;
268 }
269 }
270
271
272
273
274
275
276
277
278 protected void processEvent(final WatchEvent.Kind<?> kind, final Path completePath, final WatchEvent<?> watchEvent) {
279 logEvent(kind, completePath);
280
281 if (kind != ENTRY_CREATE && kind != ENTRY_MODIFY && kind != ENTRY_DELETE) {
282 throw new RuntimeException("Unknown event type " + kind + " for " + completePath.toAbsolutePath().toString());
283 }
284
285
286 synchronized (registrations) {
287 for (WatcherRegistration registration : registrations) {
288 if (!completePath.startsWith(registration.getRootPath())) {
289 continue;
290 }
291
292 if (!registration.getFilterPredicate().test(completePath)) {
293 continue;
294 }
295
296 WatcherCallback callback = registration.getCallback();
297 if (kind == ENTRY_CREATE) {
298 callback.added(completePath);
299 } else if (kind == ENTRY_DELETE) {
300 callback.removed(completePath);
301 } else if (kind == ENTRY_MODIFY) {
302 callback.modified(completePath);
303 }
304 }
305 }
306 }
307
308
309
310
311
312
313
314
315 private void logEvent(WatchEvent.Kind<?> kind, Path completePath) {
316 String message;
317 final String resourceType;
318 if (!Files.exists(completePath)) {
319 resourceType = "File resource(s)";
320 } else {
321 resourceType = Files.isDirectory(completePath) ? "Directory" : "File resource";
322 }
323
324 if (kind == ENTRY_CREATE) {
325 message = "{} added at [{}]";
326 } else if (kind == ENTRY_DELETE) {
327 message = "{} deleted at [{}]";
328 } else if (kind == ENTRY_MODIFY) {
329 message = "{} modified at [{}]";
330 } else {
331 message = String.format("{} event of unhandled type (%s) occurred at [{}]", kind);
332 }
333
334 if (devMode) {
335 log.info(message, resourceType, completePath);
336 } else {
337 log.debug(message, resourceType, completePath);
338 }
339 }
340
341
342
343
344 private Path getCompletePath(Path parent, WatchEvent<?> watchEvent) {
345
346 final Path path = cast(watchEvent).context();
347 return parent.resolve(path);
348 }
349
350 private String getWatcherSensitivity(String property, String defaultValue) {
351 if (StringUtils.isBlank(property)) {
352 return defaultValue;
353 }
354 if (StringUtils.equalsIgnoreCase(property, "high") || StringUtils.equalsIgnoreCase(property, "medium") || StringUtils.equalsIgnoreCase(property, "low")) {
355 return property;
356 }
357 return defaultValue;
358 }
359
360 @SuppressWarnings("unchecked")
361 private WatchEvent<Path> cast(WatchEvent<?> event) {
362 return (WatchEvent<Path>) event;
363 }
364
365 private static class WatcherRegistration {
366
367 private final Path rootPath;
368 private final Predicate<Path> filterPredicate;
369 private final WatcherCallback callback;
370
371 public WatcherRegistration(Path rootPath, Predicate<Path> filterPredicate, WatcherCallback callback) {
372 this.rootPath = rootPath;
373 this.filterPredicate = filterPredicate;
374 this.callback = callback;
375 }
376
377 public Path getRootPath() {
378 return rootPath;
379 }
380
381 public Predicate<Path> getFilterPredicate() {
382 return filterPredicate;
383 }
384
385 public WatcherCallback getCallback() {
386 return callback;
387 }
388 }
389 }