View Javadoc
1   /**
2    * This file Copyright (c) 2014-2015 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  
41  import java.io.IOException;
42  import java.nio.file.FileSystems;
43  import java.nio.file.FileVisitResult;
44  import java.nio.file.Files;
45  import java.nio.file.LinkOption;
46  import java.nio.file.Path;
47  import java.nio.file.SimpleFileVisitor;
48  import java.nio.file.WatchEvent;
49  import java.nio.file.WatchKey;
50  import java.nio.file.WatchService;
51  import java.nio.file.attribute.BasicFileAttributes;
52  import java.util.ArrayList;
53  import java.util.HashMap;
54  import java.util.List;
55  import java.util.Map;
56  
57  import org.slf4j.Logger;
58  import org.slf4j.LoggerFactory;
59  
60  import com.google.common.base.Predicate;
61  
62  /**
63   * The {@link DirectoryWatcher} covers implementation of the low level WatchService API.
64   * <p>
65   * Highly inspired by http://docs.oracle.com/javase/tutorial/displayCode.html?code=http://docs.oracle.com/javase/tutorial/essential/io/examples/WatchDir.java
66   * 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)
67   * JDK on OSX doesn't have a native impl for 7 or 8: https://bugs.openjdk.java.net/browse/JDK-7133447
68   * See http://wiki.netbeans.org/NativeFileNotifications
69   * https://code.google.com/p/barbarywatchservice/
70   */
71  public class DirectoryWatcher implements Runnable {
72  
73      private static final Logger log = LoggerFactory.getLogger(DirectoryWatcher.class);
74  
75      private final WatchService watcher;
76      private final Map<WatchKey, Path> keys = new HashMap<>();
77  
78      private final boolean recursive;
79      private final LinkOption[] visitorOptions;
80      private final List<WatcherRegistration> registrations = new ArrayList<>();
81  
82      public DirectoryWatcher(boolean recursive, boolean followLinks) throws IOException {
83          this.recursive = recursive;
84          this.visitorOptions = followLinks ? new LinkOption[] { NOFOLLOW_LINKS } : new LinkOption[0];
85          this.watcher = FileSystems.getDefault().newWatchService();
86      }
87  
88      public void register(Path dir, Predicate<Path> filterPredicate, WatcherCallback callback) throws IOException {
89          if (recursive) {
90              log.debug("Scanning {}", dir);
91              registerRecursively(dir, filterPredicate);
92              log.debug("Done scanning {}", dir);
93          } else {
94              registerDirectory(dir);
95          }
96          registrations.add(new WatcherRegistration(dir, filterPredicate, callback));
97      }
98  
99      /**
100      * Register the given directory with the WatchService.
101      */
102     protected void registerDirectory(Path dir) throws IOException {
103         final WatchKey key = dir.register(watcher, ENTRY_CREATE, ENTRY_DELETE, ENTRY_MODIFY);
104         // Would have used ExtendedWatchEventModifier.FILE_TREE, but it's not supported on osx nor linux;
105         // 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...
106         // Presumably, this is why we do recursion ourselves in here
107 
108         if (log.isDebugEnabled()) {
109             Path prev = keys.get(key);
110             if (prev == null) {
111                 log.debug("* register: {}", dir);
112             } else {
113                 log.debug("* update: {} -> {}", prev, dir);
114             }
115         }
116         keys.put(key, dir);
117     }
118 
119     /**
120      * Register the given directory, and all its sub-directories, with the WatchService.
121      */
122     protected void registerRecursively(final Path start, final Predicate<Path> filterPredicate) throws IOException {
123 
124         // register directory and sub-directories
125         Files.walkFileTree(start, new SimpleFileVisitor<Path>() {
126 
127             @Override
128             public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
129                 if (!filterPredicate.apply(dir)) {
130                     return FileVisitResult.SKIP_SUBTREE;
131                 }
132 
133                 registerDirectory(dir);
134                 return FileVisitResult.CONTINUE;
135             }
136         });
137     }
138 
139     @Override
140     public void run() {
141         try {
142             while (!Thread.currentThread().isInterrupted()) {
143 
144                 // 1. wait for key to be signaled
145                 final WatchKey key;
146                 try {
147                     key = watcher.take();
148                 } catch (InterruptedException x) {
149                     Thread.currentThread().interrupt();
150                     return;
151                 }
152 
153                 final Path dir = keys.get(key);
154                 if (dir == null) {
155                     log.debug("WatchKey not recognized!!");
156                     continue;
157                 }
158 
159                 // 2. retrieve each pending event for the key
160                 final List<WatchEvent<?>> watchEvents = key.pollEvents();
161                 log.debug("   {} events", watchEvents.size());
162 
163                 for (WatchEvent<?> event : watchEvents) {
164                     final WatchEvent.Kind<?> kind = event.kind();
165                     log.debug("event: {}", event);
166                     log.debug("kind: {}", kind);
167 
168                     // TODO OVERFLOW indicates that events may been lost or discarded.
169                     if (kind == OVERFLOW) {
170                         continue;
171                     }
172 
173                     final Path completePath = getCompletePath(key, event);
174                     log.debug("--> {} : {}: {}", Thread.currentThread(), event.kind().name(), completePath);
175 
176                     // 2a. register new directories
177                     if (Files.isDirectory(completePath, visitorOptions)) {
178                         if (recursive && (kind == ENTRY_CREATE)) {
179 
180                             // Loop over registrations to re-register accordingly
181                             try {
182                                 for (WatcherRegistration registration : registrations) {
183                                     if (!completePath.startsWith(registration.getRootPath())) {
184                                         continue;
185                                     }
186                                     Predicate<Path> filterPredicate = registration.getFilterPredicate();
187                                     registerRecursively(completePath, filterPredicate);
188                                 }
189                             } catch (IOException x) {
190                                 log.warn("Unable to register all subdirectories because of the following exception:", x);
191                             }
192                         }
193                         // TODO shouldn't we unregister deleted directories ?
194                         log.debug(" --> watch keys {}", keys.values());
195                     }
196 
197                     // 2b. process event
198                     try {
199                         processEvent(kind, completePath, event);
200                     } catch (Throwable t) {
201                         log.error("Exception when executing callback for {}: {}", event.context(), ExceptionUtil.exceptionToWords(t), t);
202                         // if we throw here we kill the thread
203                     }
204                 }
205 
206                 // 3. reset key and remove from set if directory no longer accessible
207                 boolean valid = key.reset();
208                 if (!valid) {
209                     keys.remove(key);
210 
211                     // all directories are inaccessible
212                     if (keys.isEmpty()) {
213                         break;
214                     }
215                 }
216             }
217 
218         } catch (Throwable t) {
219             log.error("Exception occurred in DirectoryWatcher: {}", t, t);
220             throw t; // This kills the thread
221         }
222     }
223 
224     /**
225      * Process given pending {@link WatchEvent} as needed.
226      *
227      * @param kind the event {@link WatchEvent.Kind kind}
228      * @param completePath an absolute {@link Path} where the event occurred
229      * @param watchEvent the raw event for further custom processing
230      */
231     protected void processEvent(final WatchEvent.Kind<?> kind, final Path completePath, final WatchEvent<?> watchEvent) {
232         if (kind != ENTRY_CREATE && kind != ENTRY_MODIFY && kind != ENTRY_DELETE) {
233             throw new RuntimeException("Unknown event type " + kind + " for " + completePath.toAbsolutePath().toString());
234         }
235 
236         for (WatcherRegistration registration : registrations) {
237             if (!completePath.startsWith(registration.getRootPath())) {
238                 continue;
239             }
240 
241             WatcherCallback callback = registration.getCallback();
242             if (kind == ENTRY_CREATE) {
243                 callback.added(completePath);
244             } else if (kind == ENTRY_DELETE) {
245                 callback.removed(completePath);
246             } else if (kind == ENTRY_MODIFY) {
247                 callback.modified(completePath);
248             }
249         }
250     }
251 
252     /**
253      * WatchEvent.context() returns a Path relative to the watchKey it was created for; we reconstruct it here.
254      */
255     private Path getCompletePath(WatchKey key, WatchEvent<?> watchEvent) {
256         // Theoretically, there could be events with other types than Path. Practically, not so sure.
257         final Path path = cast(watchEvent).context();
258         final Path parent = (Path) key.watchable();
259         return parent.resolve(path);
260     }
261 
262     @SuppressWarnings("unchecked")
263     private WatchEvent<Path> cast(WatchEvent<?> event) {
264         return (WatchEvent<Path>) event;
265     }
266 
267     private static class WatcherRegistration {
268 
269         private final Path rootPath;
270         private final Predicate<Path> filterPredicate;
271         private final WatcherCallback callback;
272 
273         public WatcherRegistration(Path rootPath, Predicate<Path> filterPredicate, WatcherCallback callback) {
274             this.rootPath = rootPath;
275             this.filterPredicate = filterPredicate;
276             this.callback = callback;
277         }
278 
279         public Path getRootPath() {
280             return rootPath;
281         }
282 
283         public Predicate<Path> getFilterPredicate() {
284             return filterPredicate;
285         }
286 
287         public WatcherCallback getCallback() {
288             return callback;
289         }
290     }
291 }