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 log.warn("WatchEvent OVERFLOWN. Some file system events are lost and won't be processed. Check your 'fs.inotify.max_user_watches' and 'fs.inotify.max_user_instances' system parameter values and consider to raise them.");
210 continue;
211 }
212
213 final Path completePath = getCompletePath(dir, event);
214 log.debug("--> {} : {}: {}", Thread.currentThread(), event.kind().name(), completePath);
215
216
217 if (Files.isDirectory(completePath, this.followSymLinks ? new LinkOption[0] : new LinkOption[] {NOFOLLOW_LINKS})) {
218 if (recursive && (kind == ENTRY_CREATE || kind == ENTRY_MODIFY)) {
219
220
221 synchronized (registrations) {
222
223 try {
224 for (WatcherRegistration registration : registrations) {
225 if (!completePath.startsWith(registration.getRootPath())) {
226 continue;
227 }
228 Predicate<Path> filterPredicate = registration.getFilterPredicate();
229
230 if (!filterPredicate.test(dir)) {
231 continue;
232 }
233
234 registerRecursively(completePath, filterPredicate);
235 }
236 } catch (IOException x) {
237 log.warn("Unable to register all subdirectories because of the following exception:", x);
238 }
239 }
240 }
241
242 log.debug(" --> watch keys {}", keys.values());
243 }
244
245
246 try {
247 processEvent(kind, completePath, event);
248 } catch (Throwable t) {
249 log.error("Exception when executing callback for {}: {}", event.context(), ExceptionUtil.exceptionToWords(t), t);
250
251 }
252 }
253
254
255 boolean valid = key.reset();
256 if (!valid) {
257 keys.remove(key);
258
259
260 if (keys.isEmpty()) {
261 break;
262 }
263 }
264 }
265
266 } catch (Throwable t) {
267 log.error("Exception occurred in DirectoryWatcher: {}", t, t);
268 throw t;
269 }
270 }
271
272
273
274
275
276
277
278
279 protected void processEvent(final WatchEvent.Kind<?> kind, final Path completePath, final WatchEvent<?> watchEvent) {
280 logEvent(kind, completePath);
281
282 if (kind != ENTRY_CREATE && kind != ENTRY_MODIFY && kind != ENTRY_DELETE) {
283 throw new RuntimeException("Unknown event type " + kind + " for " + completePath.toAbsolutePath().toString());
284 }
285
286
287 synchronized (registrations) {
288 for (WatcherRegistration registration : registrations) {
289 if (!completePath.startsWith(registration.getRootPath())) {
290 continue;
291 }
292
293 if (!registration.getFilterPredicate().test(completePath)) {
294 continue;
295 }
296
297 WatcherCallback callback = registration.getCallback();
298 if (kind == ENTRY_CREATE) {
299 callback.added(completePath);
300 } else if (kind == ENTRY_DELETE) {
301 callback.removed(completePath);
302 } else if (kind == ENTRY_MODIFY) {
303 callback.modified(completePath);
304 }
305 }
306 }
307 }
308
309
310
311
312
313
314
315
316 private void logEvent(WatchEvent.Kind<?> kind, Path completePath) {
317 String message;
318 final String resourceType;
319 if (!Files.exists(completePath)) {
320 resourceType = "File resource(s)";
321 } else {
322 resourceType = Files.isDirectory(completePath) ? "Directory" : "File resource";
323 }
324
325 if (kind == ENTRY_CREATE) {
326 message = "{} added at [{}]";
327 } else if (kind == ENTRY_DELETE) {
328 message = "{} deleted at [{}]";
329 } else if (kind == ENTRY_MODIFY) {
330 message = "{} modified at [{}]";
331 } else {
332 message = String.format("{} event of unhandled type (%s) occurred at [{}]", kind);
333 }
334
335 if (devMode) {
336 log.info(message, resourceType, completePath);
337 } else {
338 log.debug(message, resourceType, completePath);
339 }
340 }
341
342
343
344
345 private Path getCompletePath(Path parent, WatchEvent<?> watchEvent) {
346
347 final Path path = cast(watchEvent).context();
348 return parent.resolve(path);
349 }
350
351 private String getWatcherSensitivity(String property, String defaultValue) {
352 if (StringUtils.isBlank(property)) {
353 return defaultValue;
354 }
355 if (StringUtils.equalsIgnoreCase(property, "high") || StringUtils.equalsIgnoreCase(property, "medium") || StringUtils.equalsIgnoreCase(property, "low")) {
356 return property;
357 }
358 return defaultValue;
359 }
360
361 @SuppressWarnings("unchecked")
362 private WatchEvent<Path> cast(WatchEvent<?> event) {
363 return (WatchEvent<Path>) event;
364 }
365
366 private static class WatcherRegistration {
367
368 private final Path rootPath;
369 private final Predicate<Path> filterPredicate;
370 private final WatcherCallback callback;
371
372 public WatcherRegistration(Path rootPath, Predicate<Path> filterPredicate, WatcherCallback callback) {
373 this.rootPath = rootPath;
374 this.filterPredicate = filterPredicate;
375 this.callback = callback;
376 }
377
378 public Path getRootPath() {
379 return rootPath;
380 }
381
382 public Predicate<Path> getFilterPredicate() {
383 return filterPredicate;
384 }
385
386 public WatcherCallback getCallback() {
387 return callback;
388 }
389 }
390 }