View Javadoc
1   /**
2    * This file Copyright (c) 2014-2016 Magnolia International
3    * Ltd.  (http://www.magnolia-cms.com). All rights reserved.
4    *
5    *
6    * This file is dual-licensed under both the Magnolia
7    * Network Agreement and the GNU General Public License.
8    * You may elect to use one or the other of these licenses.
9    *
10   * This file is distributed in the hope that it will be
11   * useful, but AS-IS and WITHOUT ANY WARRANTY; without even the
12   * implied warranty of MERCHANTABILITY or FITNESS FOR A
13   * PARTICULAR PURPOSE, TITLE, or NONINFRINGEMENT.
14   * Redistribution, except as permitted by whichever of the GPL
15   * or MNA you select, is prohibited.
16   *
17   * 1. For the GPL license (GPL), you can redistribute and/or
18   * modify this file under the terms of the GNU General
19   * Public License, Version 3, as published by the Free Software
20   * Foundation.  You should have received a copy of the GNU
21   * General Public License, Version 3 along with this program;
22   * if not, write to the Free Software Foundation, Inc., 51
23   * Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
24   *
25   * 2. For the Magnolia Network Agreement (MNA), this file
26   * and the accompanying materials are made available under the
27   * terms of the MNA which accompanies this distribution, and
28   * is available at http://www.magnolia-cms.com/mna.html
29   *
30   * Any modifications to this file must keep this entire header
31   * intact.
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.slf4j.Logger;
60  import org.slf4j.LoggerFactory;
61  
62  import com.google.common.base.Predicate;
63  
64  /**
65   * The {@link DirectoryWatcher} covers implementation of the low level WatchService API.
66   * <p>
67   * Highly inspired by http://docs.oracle.com/javase/tutorial/displayCode.html?code=http://docs.oracle.com/javase/tutorial/essential/io/examples/WatchDir.java
68   * Since java.nio.file.Watch* is not guaranteed to be using native events, we'll probably want to extract an interface for this. (non-native implementations use polling and can be quite slow)
69   * JDK on OSX doesn't have a native impl for 7 or 8: https://bugs.openjdk.java.net/browse/JDK-7133447
70   * See http://wiki.netbeans.org/NativeFileNotifications
71   * https://code.google.com/p/barbarywatchservice/
72   */
73  public class DirectoryWatcher implements Runnable {
74  
75      private static final Logger log = LoggerFactory.getLogger(DirectoryWatcher.class);
76  
77      private final WatchService watcher;
78      private final Map<WatchKey, Path> keys = new HashMap<>();
79  
80      private final boolean recursive;
81      private final LinkOption[] visitorOptions;
82      private final List<WatcherRegistration> registrations = new ArrayList<>();
83      private final boolean devMode;
84  
85      public DirectoryWatcher(boolean recursive, boolean followLinks, MagnoliaConfigurationProperties properties) throws IOException {
86          this.recursive = recursive;
87          this.visitorOptions = followLinks ? new LinkOption[]{NOFOLLOW_LINKS} : new LinkOption[0];
88          this.watcher = FileSystems.getDefault().newWatchService();
89          this.devMode = properties.getBooleanProperty("magnolia.develop");
90      }
91  
92      /**
93       * @deprecated Since 5.4.5, Please use {@link #DirectoryWatcher(boolean, boolean, MagnoliaConfigurationProperties)} instead.
94       */
95      @Deprecated
96      public DirectoryWatcher(boolean recursive, boolean followLinks) throws IOException {
97          this(recursive, followLinks, Components.getComponent(MagnoliaConfigurationProperties.class));
98      }
99  
100     public void register(Path dir, Predicate<Path> filterPredicate, WatcherCallback callback) throws IOException {
101         if (recursive) {
102             log.debug("Scanning {}", dir);
103             registerRecursively(dir, filterPredicate);
104             log.debug("Done scanning {}", dir);
105         } else {
106             registerDirectory(dir);
107         }
108         registrations.add(new WatcherRegistration(dir, filterPredicate, callback));
109     }
110 
111     /**
112      * Register the given directory with the WatchService.
113      */
114     protected void registerDirectory(Path dir) throws IOException {
115         final WatchKey key = dir.register(watcher, new WatchEvent.Kind[]{ENTRY_CREATE, ENTRY_DELETE, ENTRY_MODIFY}, getWatchEventModifiers(watcher));
116         // Would have used ExtendedWatchEventModifier.FILE_TREE, but it's not supported on osx nor linux;
117         // AFAIK, there is no way to query the WatchService to see if it supports a given modifier without passing it and getting an UnsupportedOperationException thrown at us, so...
118         // Presumably, this is why we do recursion ourselves in here
119 
120         if (log.isDebugEnabled()) {
121             Path prev = keys.get(key);
122             if (prev == null) {
123                 log.debug("* register: {}", dir);
124             } else {
125                 log.debug("* update: {} -> {}", prev, dir);
126             }
127         }
128         keys.put(key, dir);
129     }
130 
131     protected WatchEvent.Modifier[] getWatchEventModifiers(WatchService watchService) {
132         return new WatchEvent.Modifier[0];
133     }
134 
135     /**
136      * Register the given directory, and all its sub-directories, with the WatchService.
137      */
138     protected void registerRecursively(final Path start, final Predicate<Path> filterPredicate) throws IOException {
139 
140         // register directory and sub-directories
141         Files.walkFileTree(start, new SimpleFileVisitor<Path>() {
142 
143             @Override
144             public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
145                 if (!filterPredicate.apply(dir)) {
146                     return FileVisitResult.SKIP_SUBTREE;
147                 }
148 
149                 registerDirectory(dir);
150                 return FileVisitResult.CONTINUE;
151             }
152         });
153     }
154 
155     @Override
156     public void run() {
157         try {
158             while (!Thread.currentThread().isInterrupted()) {
159 
160                 // 1. wait for key to be signaled
161                 final WatchKey key;
162                 try {
163                     key = watcher.take();
164                 } catch (InterruptedException x) {
165                     Thread.currentThread().interrupt();
166                     return;
167                 }
168 
169                 final Path dir = keys.get(key);
170                 if (dir == null) {
171                     log.debug("WatchKey not recognized!!");
172                     continue;
173                 }
174 
175                 // 2. retrieve each pending event for the key
176                 final List<WatchEvent<?>> watchEvents = key.pollEvents();
177                 log.debug("   {} events", watchEvents.size());
178 
179                 for (WatchEvent<?> event : watchEvents) {
180                     final WatchEvent.Kind<?> kind = event.kind();
181                     log.debug("event: {}", event);
182                     log.debug("kind: {}", kind);
183 
184                     // TODO OVERFLOW indicates that events may been lost or discarded.
185                     if (kind == OVERFLOW) {
186                         continue;
187                     }
188 
189                     final Path completePath = getCompletePath(key, event);
190                     log.debug("--> {} : {}: {}", Thread.currentThread(), event.kind().name(), completePath);
191 
192                     // 2a. register new directories
193                     if (Files.isDirectory(completePath, visitorOptions)) {
194                         if (recursive && (kind == ENTRY_CREATE)) {
195 
196                             // Loop over registrations to re-register accordingly
197                             try {
198                                 for (WatcherRegistration registration : registrations) {
199                                     if (!completePath.startsWith(registration.getRootPath())) {
200                                         continue;
201                                     }
202                                     Predicate<Path> filterPredicate = registration.getFilterPredicate();
203                                     registerRecursively(completePath, filterPredicate);
204                                 }
205                             } catch (IOException x) {
206                                 log.warn("Unable to register all subdirectories because of the following exception:", x);
207                             }
208                         }
209                         // TODO shouldn't we unregister deleted directories ?
210                         log.debug(" --> watch keys {}", keys.values());
211                     }
212 
213                     // 2b. process event
214                     try {
215                         processEvent(kind, completePath, event);
216                     } catch (Throwable t) {
217                         log.error("Exception when executing callback for {}: {}", event.context(), ExceptionUtil.exceptionToWords(t), t);
218                         // if we throw here we kill the thread
219                     }
220                 }
221 
222                 // 3. reset key and remove from set if directory no longer accessible
223                 boolean valid = key.reset();
224                 if (!valid) {
225                     keys.remove(key);
226 
227                     // all directories are inaccessible
228                     if (keys.isEmpty()) {
229                         break;
230                     }
231                 }
232             }
233 
234         } catch (Throwable t) {
235             log.error("Exception occurred in DirectoryWatcher: {}", t, t);
236             throw t; // This kills the thread
237         }
238     }
239 
240     /**
241      * Process given pending {@link WatchEvent} as needed.
242      *
243      * @param kind the event {@link WatchEvent.Kind kind}
244      * @param completePath an absolute {@link Path} where the event occurred
245      * @param watchEvent the raw event for further custom processing
246      */
247     protected void processEvent(final WatchEvent.Kind<?> kind, final Path completePath, final WatchEvent<?> watchEvent) {
248         logEvent(kind, completePath);
249 
250         if (kind != ENTRY_CREATE && kind != ENTRY_MODIFY && kind != ENTRY_DELETE) {
251             throw new RuntimeException("Unknown event type " + kind + " for " + completePath.toAbsolutePath().toString());
252         }
253 
254         for (WatcherRegistration registration : registrations) {
255             if (!completePath.startsWith(registration.getRootPath())) {
256                 continue;
257             }
258 
259             WatcherCallback callback = registration.getCallback();
260             if (kind == ENTRY_CREATE) {
261                 callback.added(completePath);
262             } else if (kind == ENTRY_DELETE) {
263                 callback.removed(completePath);
264             } else if (kind == ENTRY_MODIFY) {
265                 callback.modified(completePath);
266             }
267         }
268     }
269 
270     /**
271      * Logs file system events, the severity of log statements depends on the state of {@link #devMode dev mode flag}:
272      * <ul>
273      * <li>if it is on - severity level is {@code INFO};</li>
274      * <li>otherwise - severity level is {@code DEBUG}.</li>
275      * </ul>
276      */
277     private void logEvent(WatchEvent.Kind<?> kind, Path completePath) {
278         String message;
279         final String resourceType;
280         if (!Files.exists(completePath)) {
281             resourceType = "File resource(s)";
282         } else {
283             resourceType = Files.isDirectory(completePath) ? "Directory" : "File resource";
284         }
285 
286         if (kind == ENTRY_CREATE) {
287             message = "{} added at [{}]";
288         } else if (kind == ENTRY_DELETE) {
289             message = "{} deleted at [{}]";
290         } else if (kind == ENTRY_MODIFY) {
291             message = "{} modified at [{}]";
292         } else {
293             message = String.format("{} event of unhandled type (%s) occurred at [{}]", kind);
294         }
295 
296         if (devMode) {
297             log.info(message, resourceType, completePath);
298         } else {
299             log.debug(message, resourceType, completePath);
300         }
301     }
302 
303     /**
304      * WatchEvent.context() returns a Path relative to the watchKey it was created for; we reconstruct it here.
305      */
306     private Path getCompletePath(WatchKey key, WatchEvent<?> watchEvent) {
307         // Theoretically, there could be events with other types than Path. Practically, not so sure.
308         final Path path = cast(watchEvent).context();
309         final Path parent = (Path) key.watchable();
310         return parent.resolve(path);
311     }
312 
313     @SuppressWarnings("unchecked")
314     private WatchEvent<Path> cast(WatchEvent<?> event) {
315         return (WatchEvent<Path>) event;
316     }
317 
318     private static class WatcherRegistration {
319 
320         private final Path rootPath;
321         private final Predicate<Path> filterPredicate;
322         private final WatcherCallback callback;
323 
324         public WatcherRegistration(Path rootPath, Predicate<Path> filterPredicate, WatcherCallback callback) {
325             this.rootPath = rootPath;
326             this.filterPredicate = filterPredicate;
327             this.callback = callback;
328         }
329 
330         public Path getRootPath() {
331             return rootPath;
332         }
333 
334         public Predicate<Path> getFilterPredicate() {
335             return filterPredicate;
336         }
337 
338         public WatcherCallback getCallback() {
339             return callback;
340         }
341     }
342 }