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