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 }
181
182 @Override
183 public void run() {
184 try {
185 while (!Thread.currentThread().isInterrupted()) {
186
187
188 final WatchKey key;
189 try {
190 key = watcher.take();
191 } catch (InterruptedException x) {
192 Thread.currentThread().interrupt();
193 return;
194 }
195
196 final Path dir = keys.get(key);
197 if (dir == null) {
198 log.debug("WatchKey not recognized!!");
199 continue;
200 }
201
202
203 final List<WatchEvent<?>> watchEvents = key.pollEvents();
204 log.debug(" {} events", watchEvents.size());
205
206 for (WatchEvent<?> event : watchEvents) {
207 final WatchEvent.Kind<?> kind = event.kind();
208 log.debug("event: {}", event);
209 log.debug("kind: {}", kind);
210
211
212 if (kind == OVERFLOW) {
213 continue;
214 }
215
216 final Path completePath = getCompletePath(key, event);
217 log.debug("--> {} : {}: {}", Thread.currentThread(), event.kind().name(), completePath);
218
219
220 if (Files.isDirectory(completePath, this.followSymLinks ? new LinkOption[0] : new LinkOption[] {NOFOLLOW_LINKS})) {
221 if (recursive && (kind == ENTRY_CREATE)) {
222
223
224 try {
225 for (WatcherRegistration registration : registrations) {
226 if (!completePath.startsWith(registration.getRootPath())) {
227 continue;
228 }
229 Predicate<Path> filterPredicate = registration.getFilterPredicate();
230 registerRecursively(completePath, filterPredicate);
231 }
232 } catch (IOException x) {
233 log.warn("Unable to register all subdirectories because of the following exception:", x);
234 }
235 }
236
237 log.debug(" --> watch keys {}", keys.values());
238 }
239
240
241 try {
242 processEvent(kind, completePath, event);
243 } catch (Throwable t) {
244 log.error("Exception when executing callback for {}: {}", event.context(), ExceptionUtil.exceptionToWords(t), t);
245
246 }
247 }
248
249
250 boolean valid = key.reset();
251 if (!valid) {
252 keys.remove(key);
253
254
255 if (keys.isEmpty()) {
256 break;
257 }
258 }
259 }
260
261 } catch (Throwable t) {
262 log.error("Exception occurred in DirectoryWatcher: {}", t, t);
263 throw t;
264 }
265 }
266
267
268
269
270
271
272
273
274 protected void processEvent(final WatchEvent.Kind<?> kind, final Path completePath, final WatchEvent<?> watchEvent) {
275 logEvent(kind, completePath);
276
277 if (kind != ENTRY_CREATE && kind != ENTRY_MODIFY && kind != ENTRY_DELETE) {
278 throw new RuntimeException("Unknown event type " + kind + " for " + completePath.toAbsolutePath().toString());
279 }
280
281 for (WatcherRegistration registration : registrations) {
282 if (!completePath.startsWith(registration.getRootPath())) {
283 continue;
284 }
285
286 WatcherCallback callback = registration.getCallback();
287 if (kind == ENTRY_CREATE) {
288 callback.added(completePath);
289 } else if (kind == ENTRY_DELETE) {
290 callback.removed(completePath);
291 } else if (kind == ENTRY_MODIFY) {
292 callback.modified(completePath);
293 }
294 }
295 }
296
297
298
299
300
301
302
303
304 private void logEvent(WatchEvent.Kind<?> kind, Path completePath) {
305 String message;
306 final String resourceType;
307 if (!Files.exists(completePath)) {
308 resourceType = "File resource(s)";
309 } else {
310 resourceType = Files.isDirectory(completePath) ? "Directory" : "File resource";
311 }
312
313 if (kind == ENTRY_CREATE) {
314 message = "{} added at [{}]";
315 } else if (kind == ENTRY_DELETE) {
316 message = "{} deleted at [{}]";
317 } else if (kind == ENTRY_MODIFY) {
318 message = "{} modified at [{}]";
319 } else {
320 message = String.format("{} event of unhandled type (%s) occurred at [{}]", kind);
321 }
322
323 if (devMode) {
324 log.info(message, resourceType, completePath);
325 } else {
326 log.debug(message, resourceType, completePath);
327 }
328 }
329
330
331
332
333 private Path getCompletePath(WatchKey key, WatchEvent<?> watchEvent) {
334
335 final Path path = cast(watchEvent).context();
336 final Path parent = (Path) key.watchable();
337 return parent.resolve(path);
338 }
339
340 private String getWatcherSensitivity(String property, String defaultValue) {
341 if (StringUtils.isBlank(property)) {
342 return defaultValue;
343 }
344 if (StringUtils.equalsIgnoreCase(property, "high") || StringUtils.equalsIgnoreCase(property, "medium") || StringUtils.equalsIgnoreCase(property, "low")) {
345 return property;
346 }
347 return defaultValue;
348 }
349
350 @SuppressWarnings("unchecked")
351 private WatchEvent<Path> cast(WatchEvent<?> event) {
352 return (WatchEvent<Path>) event;
353 }
354
355 private static class WatcherRegistration {
356
357 private final Path rootPath;
358 private final Predicate<Path> filterPredicate;
359 private final WatcherCallback callback;
360
361 public WatcherRegistration(Path rootPath, Predicate<Path> filterPredicate, WatcherCallback callback) {
362 this.rootPath = rootPath;
363 this.filterPredicate = filterPredicate;
364 this.callback = callback;
365 }
366
367 public Path getRootPath() {
368 return rootPath;
369 }
370
371 public Predicate<Path> getFilterPredicate() {
372 return filterPredicate;
373 }
374
375 public WatcherCallback getCallback() {
376 return callback;
377 }
378 }
379 }