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