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