View Javadoc

1   //
2   //  ========================================================================
3   //  Copyright (c) 1995-2016 Mort Bay Consulting Pty. Ltd.
4   //  ------------------------------------------------------------------------
5   //  All rights reserved. This program and the accompanying materials
6   //  are made available under the terms of the Eclipse Public License v1.0
7   //  and Apache License v2.0 which accompanies this distribution.
8   //
9   //      The Eclipse Public License is available at
10  //      http://www.eclipse.org/legal/epl-v10.html
11  //
12  //      The Apache License v2.0 is available at
13  //      http://www.opensource.org/licenses/apache2.0.php
14  //
15  //  You may elect to redistribute this code under either of these licenses.
16  //  ========================================================================
17  //
18  
19  package org.eclipse.jetty.util;
20  
21  import static java.nio.file.StandardWatchEventKinds.ENTRY_CREATE;
22  import static java.nio.file.StandardWatchEventKinds.ENTRY_DELETE;
23  import static java.nio.file.StandardWatchEventKinds.ENTRY_MODIFY;
24  
25  import java.io.File;
26  import java.io.IOException;
27  import java.lang.reflect.Field;
28  import java.nio.file.ClosedWatchServiceException;
29  import java.nio.file.FileSystem;
30  import java.nio.file.FileSystems;
31  import java.nio.file.FileVisitResult;
32  import java.nio.file.Files;
33  import java.nio.file.LinkOption;
34  import java.nio.file.Path;
35  import java.nio.file.PathMatcher;
36  import java.nio.file.SimpleFileVisitor;
37  import java.nio.file.WatchEvent;
38  import java.nio.file.WatchEvent.Kind;
39  import java.nio.file.WatchKey;
40  import java.nio.file.WatchService;
41  import java.nio.file.attribute.BasicFileAttributes;
42  import java.util.ArrayList;
43  import java.util.Collections;
44  import java.util.EventListener;
45  import java.util.HashMap;
46  import java.util.HashSet;
47  import java.util.Iterator;
48  import java.util.LinkedHashMap;
49  import java.util.List;
50  import java.util.Locale;
51  import java.util.Map;
52  import java.util.Scanner;
53  import java.util.concurrent.CopyOnWriteArrayList;
54  import java.util.concurrent.TimeUnit;
55  
56  import org.eclipse.jetty.util.component.AbstractLifeCycle;
57  import org.eclipse.jetty.util.log.Log;
58  import org.eclipse.jetty.util.log.Logger;
59  
60  /**
61   * Watch a Path (and sub directories) for Path changes.
62   * <p>
63   * Suitable replacement for the old {@link Scanner} implementation.
64   * <p>
65   * Allows for configured Excludes and Includes using {@link FileSystem#getPathMatcher(String)} syntax.
66   * <p>
67   * Reports activity via registered {@link Listener}s
68   */
69  public class PathWatcher extends AbstractLifeCycle implements Runnable
70  {
71      public static class Config
72      {
73          public static final int UNLIMITED_DEPTH = -9999;
74          
75          private static final String PATTERN_SEP;
76  
77          static
78          {
79              String sep = File.separator;
80              if (File.separatorChar == '\\')
81              {
82                  sep = "\\\\";
83              }
84              PATTERN_SEP = sep;
85          }
86  
87          protected final Path dir;
88          protected int recurseDepth = 0; // 0 means no sub-directories are scanned
89          protected List<PathMatcher> includes;
90          protected List<PathMatcher> excludes;
91          protected boolean excludeHidden = false;
92  
93          public Config(Path path)
94          {
95              this.dir = path;
96              includes = new ArrayList<>();
97              excludes = new ArrayList<>();
98          }
99  
100         /**
101          * Add an exclude PathMatcher
102          *
103          * @param matcher
104          *            the path matcher for this exclude
105          */
106         public void addExclude(PathMatcher matcher)
107         {
108             this.excludes.add(matcher);
109         }
110 
111         /**
112          * Add an exclude PathMatcher.
113          * <p>
114          * Note: this pattern is FileSystem specific (so use "/" for Linux and OSX, and "\\" for Windows)
115          *
116          * @param syntaxAndPattern
117          *            the PathMatcher syntax and pattern to use
118          * @see FileSystem#getPathMatcher(String) for detail on syntax and pattern
119          */
120         public void addExclude(final String syntaxAndPattern)
121         {
122             if (LOG.isDebugEnabled())
123             {
124                 LOG.debug("Adding exclude: [{}]",syntaxAndPattern);
125             }
126             addExclude(dir.getFileSystem().getPathMatcher(syntaxAndPattern));
127         }
128 
129         /**
130          * Add a <code>glob:</code> syntax pattern exclude reference in a directory relative, os neutral, pattern.
131          *
132          * <pre>
133          *    On Linux:
134          *    Config config = new Config(Path("/home/user/example"));
135          *    config.addExcludeGlobRelative("*.war") =&gt; "glob:/home/user/example/*.war"
136          * 
137          *    On Windows
138          *    Config config = new Config(Path("D:/code/examples"));
139          *    config.addExcludeGlobRelative("*.war") =&gt; "glob:D:\\code\\examples\\*.war"
140          *
141          * </pre>
142          *
143          * @param pattern
144          *            the pattern, in unixy format, relative to config.dir
145          */
146         public void addExcludeGlobRelative(String pattern)
147         {
148             addExclude(toGlobPattern(dir,pattern));
149         }
150 
151         /**
152          * Exclude hidden files and hidden directories
153          */
154         public void addExcludeHidden()
155         {
156             if (!excludeHidden)
157             {
158                 if (LOG.isDebugEnabled())
159                 {
160                     LOG.debug("Adding hidden files and directories to exclusions");
161                 }
162                 excludeHidden = true;
163 
164                 addExclude("regex:^.*" + PATTERN_SEP + "\\..*$"); // ignore hidden files
165                 addExclude("regex:^.*" + PATTERN_SEP + "\\..*" + PATTERN_SEP + ".*$"); // ignore files in hidden directories
166             }
167         }
168 
169         /**
170          * Add multiple exclude PathMatchers
171          *
172          * @param syntaxAndPatterns
173          *            the list of PathMatcher syntax and patterns to use
174          * @see FileSystem#getPathMatcher(String) for detail on syntax and pattern
175          */
176         public void addExcludes(List<String> syntaxAndPatterns)
177         {
178             for (String syntaxAndPattern : syntaxAndPatterns)
179             {
180                 addExclude(syntaxAndPattern);
181             }
182         }
183 
184         /**
185          * Add an include PathMatcher
186          *
187          * @param matcher
188          *            the path matcher for this include
189          */
190         public void addInclude(PathMatcher matcher)
191         {
192             this.includes.add(matcher);
193         }
194 
195         /**
196          * Add an include PathMatcher
197          *
198          * @param syntaxAndPattern
199          *            the PathMatcher syntax and pattern to use
200          * @see FileSystem#getPathMatcher(String) for detail on syntax and pattern
201          */
202         public void addInclude(String syntaxAndPattern)
203         {
204             if (LOG.isDebugEnabled())
205             {
206                 LOG.debug("Adding include: [{}]",syntaxAndPattern);
207             }
208             addInclude(dir.getFileSystem().getPathMatcher(syntaxAndPattern));
209         }
210 
211         /**
212          * Add a <code>glob:</code> syntax pattern reference in a directory relative, os neutral, pattern.
213          *
214          * <pre>
215          *    On Linux:
216          *    Config config = new Config(Path("/home/user/example"));
217          *    config.addIncludeGlobRelative("*.war") =&gt; "glob:/home/user/example/*.war"
218          * 
219          *    On Windows
220          *    Config config = new Config(Path("D:/code/examples"));
221          *    config.addIncludeGlobRelative("*.war") =&gt; "glob:D:\\code\\examples\\*.war"
222          *
223          * </pre>
224          *
225          * @param pattern
226          *            the pattern, in unixy format, relative to config.dir
227          */
228         public void addIncludeGlobRelative(String pattern)
229         {
230             addInclude(toGlobPattern(dir,pattern));
231         }
232 
233         /**
234          * Add multiple include PathMatchers
235          *
236          * @param syntaxAndPatterns
237          *            the list of PathMatcher syntax and patterns to use
238          * @see FileSystem#getPathMatcher(String) for detail on syntax and pattern
239          */
240         public void addIncludes(List<String> syntaxAndPatterns)
241         {
242             for (String syntaxAndPattern : syntaxAndPatterns)
243             {
244                 addInclude(syntaxAndPattern);
245             }
246         }
247 
248         /**
249          * Build a new config from a this configuration.
250          * <p>
251          * Useful for working with sub-directories that also need to be watched.
252          *
253          * @param dir
254          *            the directory to build new Config from (using this config as source of includes/excludes)
255          * @return the new Config
256          */
257         public Config asSubConfig(Path dir)
258         {
259             Config subconfig = new Config(dir);
260             subconfig.includes = this.includes;
261             subconfig.excludes = this.excludes;
262             if (dir == this.dir)
263                 subconfig.recurseDepth = this.recurseDepth; // TODO shouldn't really do a subconfig for this
264             else
265             {
266                 if (this.recurseDepth == UNLIMITED_DEPTH)
267                     subconfig.recurseDepth = UNLIMITED_DEPTH;
268                 else
269                     subconfig.recurseDepth = this.recurseDepth - (dir.getNameCount() - this.dir.getNameCount());                
270             }
271             return subconfig;
272         }
273 
274         public int getRecurseDepth()
275         {
276             return recurseDepth;
277         }
278         
279         public boolean isRecurseDepthUnlimited ()
280         {
281             return this.recurseDepth == UNLIMITED_DEPTH;
282         }
283         
284         public Path getPath ()
285         {
286             return this.dir;
287         }
288 
289         private boolean hasMatch(Path path, List<PathMatcher> matchers)
290         {
291             for (PathMatcher matcher : matchers)
292             {
293                 if (matcher.matches(path))
294                 {
295                     return true;
296                 }
297             }
298             return false;
299         }
300 
301         public boolean isExcluded(Path dir) throws IOException
302         {
303             if (excludeHidden)
304             {
305                 if (Files.isHidden(dir))
306                 {
307                     if (NOISY_LOG.isDebugEnabled())
308                     {
309                         NOISY_LOG.debug("isExcluded [Hidden] on {}",dir);
310                     }
311                     return true;
312                 }
313             }
314 
315             if (excludes.isEmpty())
316             {
317                 // no excludes == everything allowed
318                 return false;
319             }
320 
321             boolean matched = hasMatch(dir,excludes);
322             if (NOISY_LOG.isDebugEnabled())
323             {
324                 NOISY_LOG.debug("isExcluded [{}] on {}",matched,dir);
325             }
326             return matched;
327         }
328 
329         public boolean isIncluded(Path dir)
330         {
331             if (includes.isEmpty())
332             {
333                 // no includes == everything allowed
334                 if (NOISY_LOG.isDebugEnabled())
335                 {
336                     NOISY_LOG.debug("isIncluded [All] on {}",dir);
337                 }
338                 return true;
339             }
340 
341             boolean matched = hasMatch(dir,includes);
342             if (NOISY_LOG.isDebugEnabled())
343             {
344                 NOISY_LOG.debug("isIncluded [{}] on {}",matched,dir);
345             }
346             return matched;
347         }
348 
349         public boolean matches(Path path)
350         {
351             try
352             {
353                 return !isExcluded(path) && isIncluded(path);
354             }
355             catch (IOException e)
356             {
357                 LOG.warn("Unable to match path: " + path,e);
358                 return false;
359             }
360         }
361 
362         /**
363          * Set the recurse depth for the directory scanning.
364          * <p>
365          * -999 indicates arbitrarily deep recursion, 0 indicates no recursion, 1 is only one directory deep, and so on.
366          *
367          * @param depth
368          *            the number of directories deep to recurse
369          */
370         public void setRecurseDepth(int depth)
371         {
372             this.recurseDepth = depth;
373         }
374         
375    
376 
377         /**
378          * Determine if the provided child directory should be recursed into based on the configured {@link #setRecurseDepth(int)}
379          *
380          * @param child
381          *            the child directory to test against
382          * @return true if recurse should occur, false otherwise
383          */
384         public boolean shouldRecurseDirectory(Path child)
385         {
386             if (!child.startsWith(dir))
387             {
388                 // not part of parent? don't recurse
389                 return false;
390             }
391 
392             //If not limiting depth, should recurse all
393             if (isRecurseDepthUnlimited())
394                 return true;
395             
396             //Depth limited, check it
397             int childDepth = dir.relativize(child).getNameCount();
398             return (childDepth <= recurseDepth);
399         }
400 
401         private String toGlobPattern(Path path, String subPattern)
402         {
403             StringBuilder s = new StringBuilder();
404             s.append("glob:");
405 
406             boolean needDelim = false;
407 
408             // Add root (aka "C:\" for Windows)
409             Path root = path.getRoot();
410             if (root != null)
411             {
412                 if (NOISY_LOG.isDebugEnabled())
413                 {
414                     NOISY_LOG.debug("Path: {} -> Root: {}",path,root);
415                 }
416                 for (char c : root.toString().toCharArray())
417                 {
418                     if (c == '\\')
419                     {
420                         s.append(PATTERN_SEP);
421                     }
422                     else
423                     {
424                         s.append(c);
425                     }
426                 }
427             }
428             else
429             {
430                 needDelim = true;
431             }
432 
433             // Add the individual path segments
434             for (Path segment : path)
435             {
436                 if (needDelim)
437                 {
438                     s.append(PATTERN_SEP);
439                 }
440                 s.append(segment);
441                 needDelim = true;
442             }
443 
444             // Add the sub pattern (if specified)
445             if ((subPattern != null) && (subPattern.length() > 0))
446             {
447                 if (needDelim)
448                 {
449                     s.append(PATTERN_SEP);
450                 }
451                 for (char c : subPattern.toCharArray())
452                 {
453                     if (c == '/')
454                     {
455                         s.append(PATTERN_SEP);
456                     }
457                     else
458                     {
459                         s.append(c);
460                     }
461                 }
462             }
463 
464             return s.toString();
465         }
466 
467         @Override
468         public String toString()
469         {
470             StringBuilder s = new StringBuilder();
471             s.append(dir);
472             if (recurseDepth > 0)
473             {
474                 s.append(" [depth=").append(recurseDepth).append("]");
475             }
476             return s.toString();
477         }
478     }
479     
480     public static class DepthLimitedFileVisitor extends SimpleFileVisitor<Path>
481     {
482         private Config base;
483         private PathWatcher watcher;
484         
485         public DepthLimitedFileVisitor (PathWatcher watcher, Config base)
486         {
487             this.base = base;
488             this.watcher = watcher;
489         }
490 
491         /*
492          * 2 situations:
493          * 
494          * 1. a subtree exists at the time a dir to watch is added (eg watching /tmp/xxx and it contains aaa/)
495          *  - will start with /tmp/xxx for which we want to register with the poller
496          *  - want to visit each child
497          *     - if child is file, gen add event
498          *     - if child is dir, gen add event but ONLY register it if inside depth limit and ONLY continue visit of child if inside depth limit
499          * 2. a subtree is added inside a watched dir (eg watching /tmp/xxx, add aaa/ to xxx/)
500          *  - will start with /tmp/xxx/aaa 
501          *    - gen add event but ONLY register it if inside depth limit and ONLY continue visit of children if inside depth limit
502          *    
503          */
504         @Override
505         public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException
506         {
507             //In a directory:
508             // 1. the dir is the base directory
509             //   - register it with the poll mechanism
510             //   - generate pending add event (iff notifiable and matches patterns)
511             //   - continue the visit (sibling dirs, sibling files)
512             // 2. the dir is a subdir at some depth in the basedir's tree
513             //   - if the level of the subdir less or equal to base's limit
514             //     - register it wih the poll mechanism
515             //     - generate pending add event (iff notifiable and matches patterns)
516             //   - else stop visiting this dir
517 
518             if (!base.isExcluded(dir))
519             {
520                 if (base.isIncluded(dir))
521                 {
522                     if (watcher.isNotifiable())
523                     {
524                         // Directory is specifically included in PathMatcher, then
525                         // it should be notified as such to interested listeners
526                         PathWatchEvent event = new PathWatchEvent(dir,PathWatchEventType.ADDED);
527                         if (LOG.isDebugEnabled())
528                         {
529                             LOG.debug("Pending {}",event);
530                         }
531                         watcher.addToPendingList(dir, event);
532                     }
533                 }
534 
535                 //Register the dir with the watcher if it is:
536                 // - the base dir and recursion is unlimited
537                 // - the base dir and its depth is 0 (meaning we want to capture events from it, but not necessarily its children)
538                 // - the base dir and we are recursing it and the depth is within the limit
539                 // - a child dir and its depth is within the limits
540                 if ((base.getPath().equals(dir) && (base.isRecurseDepthUnlimited() || base.getRecurseDepth() >= 0)) || base.shouldRecurseDirectory(dir))
541                     watcher.register(dir,base);
542             }
543 
544             //Continue walking the tree of this dir if it is:
545             // - the base dir and recursion is unlimited
546             // - the base dir and we're not recursing in it
547             // - the base dir and we are recursing it and the depth is within the limit
548             // - a child dir and its depth is within the limits
549             if ((base.getPath().equals(dir)&& (base.isRecurseDepthUnlimited() || base.getRecurseDepth() >= 0)) || base.shouldRecurseDirectory(dir))
550                 return FileVisitResult.CONTINUE;
551             else 
552                 return FileVisitResult.SKIP_SUBTREE;   
553         }
554 
555         @Override
556         public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException
557         {
558             // In a file:
559             //    - register with poll mechanism
560             //    - generate pending add event (iff notifiable and matches patterns)
561             
562             if (base.matches(file) && watcher.isNotifiable())
563             {
564                 PathWatchEvent event = new PathWatchEvent(file,PathWatchEventType.ADDED);
565                 if (LOG.isDebugEnabled())
566                 {
567                     LOG.debug("Pending {}",event);
568                 }
569                 watcher.addToPendingList(file, event);
570             }
571 
572             return FileVisitResult.CONTINUE;
573         }
574         
575     }
576 
577     /**
578      * Listener for path change events
579      */
580     public static interface Listener extends EventListener
581     {
582         void onPathWatchEvent(PathWatchEvent event);
583     }
584     
585     /**
586      * EventListListener
587      *
588      * Listener that reports accumulated events in one shot
589      */
590     public static interface EventListListener extends EventListener
591     {
592         void onPathWatchEvents(List<PathWatchEvent> events);
593     }
594 
595     /**
596      * PathWatchEvent
597      *
598      * Represents a file event. Reported to registered listeners.
599      */
600     public static class PathWatchEvent
601     {
602         private final Path path;
603         private final PathWatchEventType type;
604         private int count = 0;
605      
606         public PathWatchEvent(Path path, PathWatchEventType type)
607         {
608             this.path = path;
609             this.count = 1;
610             this.type = type;
611 
612         }
613 
614         public PathWatchEvent(Path path, WatchEvent<Path> event)
615         {
616             this.path = path;
617             this.count = event.count();
618             if (event.kind() == ENTRY_CREATE)
619             {
620                 this.type = PathWatchEventType.ADDED;
621             }
622             else if (event.kind() == ENTRY_DELETE)
623             {
624                 this.type = PathWatchEventType.DELETED;
625             }
626             else if (event.kind() == ENTRY_MODIFY)
627             {
628                 this.type = PathWatchEventType.MODIFIED;
629             }
630             else
631             {
632                 this.type = PathWatchEventType.UNKNOWN;
633             }
634         }
635 
636         /** 
637          * @see java.lang.Object#equals(java.lang.Object)
638          */
639         @Override
640         public boolean equals(Object obj)
641         {
642             if (this == obj)
643             {
644                 return true;
645             }
646             if (obj == null)
647             {
648                 return false;
649             }
650             if (getClass() != obj.getClass())
651             {
652                 return false;
653             }
654             PathWatchEvent other = (PathWatchEvent)obj;
655             if (path == null)
656             {
657                 if (other.path != null)
658                 {
659                     return false;
660                 }
661             }
662             else if (!path.equals(other.path))
663             {
664                 return false;
665             }
666             if (type != other.type)
667             {
668                 return false;
669             }
670             return true;
671         }
672 
673         public Path getPath()
674         {
675             return path;
676         }
677 
678         public PathWatchEventType getType()
679         {
680             return type;
681         }
682         
683         public void incrementCount(int num)
684         {
685             count += num;
686         }
687 
688         public int getCount()
689         {
690             return count;
691         }
692         
693         /** 
694          * @see java.lang.Object#hashCode()
695          */
696         @Override
697         public int hashCode()
698         {
699             final int prime = 31;
700             int result = 1;
701             result = (prime * result) + ((path == null)?0:path.hashCode());
702             result = (prime * result) + ((type == null)?0:type.hashCode());
703             return result;
704         }
705 
706         /** 
707          * @see java.lang.Object#toString()
708          */
709         @Override
710         public String toString()
711         {
712             return String.format("PathWatchEvent[%s|%s]",type,path);
713         }
714     }
715     
716     
717     
718     /**
719      * PathPendingEvents
720      *
721      * For a given path, a list of events that are awaiting the
722      * quiet time. The list is in the order that the event were
723      * received from the WatchService
724      */
725     public static class PathPendingEvents
726     {
727         private Path _path;
728         private List<PathWatchEvent> _events;
729         private long _timestamp;
730         private long _lastFileSize = -1;
731 
732         public PathPendingEvents (Path path)
733         {
734             _path = path;
735         }
736         
737         public PathPendingEvents (Path path, PathWatchEvent event)
738         {
739             this (path);
740             addEvent(event);
741         }
742         
743         public void addEvent (PathWatchEvent event)
744         {
745             long now = System.currentTimeMillis();
746             _timestamp = now;
747 
748             if (_events == null)
749             {
750                 _events = new ArrayList<PathWatchEvent>();
751                 _events.add(event);
752             }
753             else
754             {
755                 //Check if the same type of event is already present, in which case we
756                 //can increment its counter. Otherwise, add it
757                 PathWatchEvent existingType = null;
758                 for (PathWatchEvent e:_events)
759                 {
760                     if (e.getType() == event.getType())
761                     {
762                         existingType = e;
763                         break;
764                     }
765                 }
766 
767                 if (existingType == null)
768                 {
769                     _events.add(event);
770                 }
771                 else
772                 {
773                     existingType.incrementCount(event.getCount());
774                 }
775             }
776 
777         }
778         
779         public List<PathWatchEvent> getEvents()
780         {
781             return _events;
782         }
783 
784         public long getTimestamp()
785         {
786             return _timestamp;
787         }
788    
789         
790         /**
791          * Check to see if the file referenced by this Event is quiet.
792          * <p>
793          * Will validate the timestamp to see if it is expired, as well as if the file size hasn't changed within the quiet period.
794          * <p>
795          * Always updates timestamp to 'now' on use of this method.
796          * 
797          * @param now the time now 
798          *
799          * @param expiredDuration
800          *            the expired duration past the timestamp to be considered expired
801          * @param expiredUnit
802          *            the unit of time for the expired check
803          * @return true if expired, false if not
804          */
805         public boolean isQuiet(long now, long expiredDuration, TimeUnit expiredUnit)
806         {
807 
808             long pastdue = _timestamp + expiredUnit.toMillis(expiredDuration);
809             _timestamp = now;
810 
811             long fileSize = _path.toFile().length(); // File.length() returns 0 for non existant files
812             boolean fileSizeChanged = (_lastFileSize != fileSize);
813             _lastFileSize = fileSize;
814 
815             if ((now > pastdue) && (!fileSizeChanged /*|| fileSize == 0*/))
816             {
817                 // Quiet period timestamp has expired, and file size hasn't changed, or the file
818                 // has been deleted.
819                 // Consider this a quiet event now.
820                 return true;
821             }
822 
823             return false;
824         }
825 
826     }
827 
828     /**
829      * PathWatchEventType
830      *
831      * Type of an event
832      */
833     public static enum PathWatchEventType
834     {
835         ADDED, DELETED, MODIFIED, UNKNOWN;
836     }
837 
838     private static final boolean IS_WINDOWS;
839 
840     static
841     {
842         String os = System.getProperty("os.name");
843         if (os == null)
844         {
845             IS_WINDOWS = false;
846         }
847         else
848         {
849             String osl = os.toLowerCase(Locale.ENGLISH);
850             IS_WINDOWS = osl.contains("windows");
851         }
852     }
853 
854     private static final Logger LOG = Log.getLogger(PathWatcher.class);
855     /**
856      * super noisy debug logging
857      */
858     private static final Logger NOISY_LOG = Log.getLogger(PathWatcher.class.getName() + ".Noisy");
859 
860     @SuppressWarnings("unchecked")
861     protected static <T> WatchEvent<T> cast(WatchEvent<?> event)
862     {
863         return (WatchEvent<T>)event;
864     }
865 
866     private static final WatchEvent.Kind<?> WATCH_EVENT_KINDS[] = { ENTRY_CREATE, ENTRY_DELETE, ENTRY_MODIFY };
867     
868     private  WatchService watchService;
869     private  WatchEvent.Modifier watchModifiers[];
870     private  boolean nativeWatchService;
871     
872     private Map<WatchKey, Config> keys = new HashMap<>();
873     private List<EventListener> listeners = new CopyOnWriteArrayList<>(); //a listener may modify the listener list directly or by stopping the PathWatcher
874     private List<Config> configs = new ArrayList<>();
875 
876     /**
877      * Update Quiet Time - set to 1000 ms as default (a lower value in Windows is not supported)
878      */
879     private long updateQuietTimeDuration = 1000;
880     private TimeUnit updateQuietTimeUnit = TimeUnit.MILLISECONDS;
881     private Thread thread;
882     private boolean _notifyExistingOnStart = true;
883     private Map<Path, PathPendingEvents> pendingEvents = new LinkedHashMap<>();
884     
885     
886     
887     /**
888      * Construct new PathWatcher
889      */
890     public PathWatcher()
891     {
892     }
893     
894     /**
895      * Request watch on a the given path (either file or dir)
896      * using all Config defaults. In the case of a dir,
897      * the default is not to recurse into subdirs for watching.
898      * 
899      * @param file the path to watch
900      */
901     public void watch (final Path file)
902     {
903         //Make a config for the dir above it and
904         //include a match only for the given path
905         //using all defaults for the configuration
906         Path abs = file;
907         if (!abs.isAbsolute())
908         {
909             abs = file.toAbsolutePath();
910         }
911         
912         //Check we don't already have a config for the parent directory. 
913         //If we do, add in this filename.
914         Config config = null;
915         Path parent = abs.getParent();
916         for (Config c:configs)
917         {
918             if (c.getPath().equals(parent))
919             {
920                 config = c;
921                 break;
922             }
923         }
924         
925         //Make a new config
926         if (config == null)
927         {
928             config = new Config(abs.getParent());
929             // the include for the directory itself
930             config.addIncludeGlobRelative("");
931             //add the include for the file
932             config.addIncludeGlobRelative(file.getFileName().toString());
933             watch(config);
934         }
935         else
936             //add the include for the file
937             config.addIncludeGlobRelative(file.getFileName().toString());
938     }
939     
940     /**
941      * Request watch on a path with custom Config 
942      * provided.
943      * 
944      * @param config the configuration to watch
945      */
946     public void watch (final Config config)
947     {
948         //Add a custom config
949         configs.add(config);
950     }
951     
952     /**
953      * Register path in the config with the file watch service,
954      * walking the tree if it happens to be a directory.
955      * 
956      * @param baseDir the base directory configuration to watch
957      * @throws IOException if unable to walk the filesystem tree
958      */
959     protected void prepareConfig (final Config baseDir) throws IOException
960     {
961         if (LOG.isDebugEnabled())
962         {
963             LOG.debug("Watching directory {}",baseDir);
964         }
965         Files.walkFileTree(baseDir.getPath(), new DepthLimitedFileVisitor(this, baseDir));
966     }
967 
968     
969     
970     /**
971      * Add a listener for changes the watcher notices.
972      * 
973      * @param listener change listener
974      */
975     public void addListener(EventListener listener)
976     {
977         listeners.add(listener);
978     }
979 
980     /**
981      * Append some info on the paths that we are watching.
982      * 
983      * @param s
984      */
985     private void appendConfigId(StringBuilder s)
986     {
987         List<Path> dirs = new ArrayList<>();
988 
989         for (Config config : keys.values())
990         {
991             dirs.add(config.dir);
992         }
993 
994         Collections.sort(dirs);
995 
996         s.append("[");
997         if (dirs.size() > 0)
998         {
999             s.append(dirs.get(0));
1000             if (dirs.size() > 1)
1001             {
1002                 s.append(" (+").append(dirs.size() - 1).append(")");
1003             }
1004         }
1005         else
1006         {
1007             s.append("<null>");
1008         }
1009         s.append("]");
1010     }
1011 
1012     /** 
1013      * @see org.eclipse.jetty.util.component.AbstractLifeCycle#doStart()
1014      */
1015     @Override
1016     protected void doStart() throws Exception
1017     {
1018         //create a new watchservice
1019         createWatchService();
1020         
1021         //ensure setting of quiet time is appropriate now we have a watcher
1022         setUpdateQuietTime(getUpdateQuietTimeMillis(), TimeUnit.MILLISECONDS);
1023 
1024         // Register all watched paths, walking dir hierarchies as needed, possibly generating
1025         // fake add events if notifyExistingOnStart is true
1026         for (Config c:configs)
1027             prepareConfig(c);
1028         
1029         // Start Thread for watcher take/pollKeys loop
1030         StringBuilder threadId = new StringBuilder();
1031         threadId.append("PathWatcher-Thread");
1032         appendConfigId(threadId);
1033 
1034         thread = new Thread(this,threadId.toString());
1035         thread.setDaemon(true);
1036         thread.start();
1037         super.doStart();
1038     }
1039 
1040     /** 
1041      * @see org.eclipse.jetty.util.component.AbstractLifeCycle#doStop()
1042      */
1043     @Override
1044     protected void doStop() throws Exception
1045     {
1046         if (watchService != null)
1047             watchService.close(); //will invalidate registered watch keys, interrupt thread in take or poll
1048         watchService = null;
1049         thread = null;
1050         keys.clear();
1051         pendingEvents.clear();
1052         super.doStop();
1053     }
1054     
1055     
1056     /**
1057      * Remove all current configs and listeners.
1058      */
1059     public void reset ()
1060     {
1061         if (!isStopped())
1062             throw new IllegalStateException("PathWatcher must be stopped before reset.");
1063         
1064         configs.clear();
1065         listeners.clear();
1066     }
1067     
1068     
1069     /**
1070      * Create a fresh WatchService and determine if it is a 
1071      * native implementation or not.
1072      * 
1073      * @throws IOException
1074      */
1075     private void createWatchService () throws IOException
1076     {
1077         //create a watch service
1078         this.watchService = FileSystems.getDefault().newWatchService();
1079 
1080         WatchEvent.Modifier modifiers[] = null;
1081         boolean nativeService = true;
1082         // Try to determine native behavior
1083         // See http://stackoverflow.com/questions/9588737/is-java-7-watchservice-slow-for-anyone-else
1084         try
1085         {
1086             ClassLoader cl = Thread.currentThread().getContextClassLoader();
1087             Class<?> pollingWatchServiceClass = Class.forName("sun.nio.fs.PollingWatchService",false,cl);
1088             if (pollingWatchServiceClass.isAssignableFrom(this.watchService.getClass()))
1089             {
1090                 nativeService = false;
1091                 LOG.info("Using Non-Native Java {}",pollingWatchServiceClass.getName());
1092                 Class<?> c = Class.forName("com.sun.nio.file.SensitivityWatchEventModifier");
1093                 Field f = c.getField("HIGH");
1094                 modifiers = new WatchEvent.Modifier[]
1095                         {
1096                          (WatchEvent.Modifier)f.get(c)
1097                         };
1098             }
1099         }
1100         catch (Throwable t)
1101         {
1102             // Unknown JVM environment, assuming native.
1103             LOG.ignore(t);
1104         }
1105 
1106         this.watchModifiers = modifiers;
1107         this.nativeWatchService = nativeService;
1108     }
1109     
1110     /**
1111      * Check to see if the watcher is in a state where it should generate
1112      * watch events to the listeners. Used to determine if watcher should generate
1113      * events for existing files and dirs on startup.
1114      * 
1115      * @return true if the watcher should generate events to the listeners.
1116      */
1117     protected boolean isNotifiable ()
1118     {
1119         return (isStarted() || (!isStarted() && isNotifyExistingOnStart()));
1120     }
1121 
1122     /**
1123      * Get an iterator over the listeners.
1124      * 
1125      * @return iterator over the listeners.
1126      */
1127     public Iterator<EventListener> getListeners()
1128     {
1129         return listeners.iterator();
1130     }
1131 
1132     /**
1133      * Change the quiet time.
1134      * 
1135      * @return the quiet time in millis
1136      */
1137     public long getUpdateQuietTimeMillis()
1138     {
1139         return TimeUnit.MILLISECONDS.convert(updateQuietTimeDuration,updateQuietTimeUnit);
1140     }
1141 
1142     /**
1143      * Generate events to the listeners.
1144      * 
1145      * @param events the events captured
1146      */
1147     protected void notifyOnPathWatchEvents (List<PathWatchEvent> events)
1148     {
1149         if (events == null || events.isEmpty())
1150             return;
1151 
1152         for (EventListener listener : listeners)
1153         {
1154             if (listener instanceof EventListListener)
1155             {
1156                 try
1157                 {
1158                     ((EventListListener)listener).onPathWatchEvents(events);
1159                 }
1160                 catch (Throwable t)
1161                 {
1162                     LOG.warn(t);
1163                 }
1164             }
1165             else
1166             {
1167                 Listener l = (Listener)listener;
1168                 for (PathWatchEvent event:events)
1169                 {
1170                     try
1171                     {
1172                         l.onPathWatchEvent(event);
1173                     }
1174                     catch (Throwable t)
1175                     {
1176                         LOG.warn(t);
1177                     }
1178                 }
1179             }
1180         }
1181 
1182     }
1183 
1184     /**
1185      * Register a path (directory) with the WatchService.
1186      * 
1187      * @param dir the directory to register
1188      * @param root the configuration root
1189      * @throws IOException if unable to register the path with the watch service.
1190      */
1191     protected void register(Path dir, Config root) throws IOException
1192     {
1193        
1194         LOG.debug("Registering watch on {}",dir);
1195         if(watchModifiers != null) 
1196         {
1197             // Java Watcher
1198             WatchKey key = dir.register(watchService,WATCH_EVENT_KINDS,watchModifiers);
1199             keys.put(key,root.asSubConfig(dir));
1200         } else 
1201         {
1202             // Native Watcher
1203             WatchKey key = dir.register(watchService,WATCH_EVENT_KINDS);
1204             keys.put(key,root.asSubConfig(dir));
1205         }
1206     }
1207 
1208     
1209     /**
1210      * Delete a listener
1211      * @param listener the listener to remove
1212      * @return true if the listener existed and was removed
1213      */
1214     public boolean removeListener(Listener listener)
1215     {
1216         return listeners.remove(listener);
1217     }
1218 
1219     
1220     /** 
1221      * Forever loop.
1222      * 
1223      * Wait for the WatchService to report some filesystem events for the
1224      * watched paths.
1225      * 
1226      * When an event for a path first occurs, it is subjected to a quiet time.
1227      * Subsequent events that arrive for the same path during this quiet time are
1228      * accumulated and the timer reset. Only when the quiet time has expired are
1229      * the accumulated events sent. MODIFY events are handled slightly differently -
1230      * multiple MODIFY events arriving within a quiet time are coalesced into a
1231      * single MODIFY event. Both the accumulation of events and coalescing of MODIFY
1232      * events reduce the number and frequency of event reporting for "noisy" files (ie
1233      * those that are undergoing rapid change).
1234      * 
1235      * @see java.lang.Runnable#run()
1236      */
1237     @Override
1238     public void run()
1239     {
1240 
1241         List<PathWatchEvent> notifiableEvents = new ArrayList<PathWatchEvent>();
1242         
1243         // Start the java.nio watching
1244         if (LOG.isDebugEnabled())
1245         {
1246             LOG.debug("Starting java.nio file watching with {}",watchService);
1247         }
1248 
1249         while (watchService != null  && thread == Thread.currentThread())
1250         {
1251             WatchKey key = null;
1252 
1253             try
1254             {     
1255                 //If no pending events, wait forever for new events
1256                 if (pendingEvents.isEmpty())
1257                 {
1258                     if (NOISY_LOG.isDebugEnabled())
1259                         NOISY_LOG.debug("Waiting for take()");
1260                     key = watchService.take();
1261                 }
1262                 else
1263                 {
1264                     //There are existing events that might be ready to go,
1265                     //only wait as long as the quiet time for any new events
1266                     if (NOISY_LOG.isDebugEnabled())
1267                         NOISY_LOG.debug("Waiting for poll({}, {})",updateQuietTimeDuration,updateQuietTimeUnit);
1268 
1269                     key = watchService.poll(updateQuietTimeDuration,updateQuietTimeUnit);
1270                    
1271                     //If no new events its safe to process the pendings
1272                     if (key == null)
1273                     {
1274                         long now = System.currentTimeMillis();
1275                         // no new event encountered.
1276                         for (Path path : new HashSet<Path>(pendingEvents.keySet()))
1277                         {
1278                             PathPendingEvents pending = pendingEvents.get(path);
1279                             if (pending.isQuiet(now, updateQuietTimeDuration,updateQuietTimeUnit))
1280                             {
1281                                 //No fresh events received during quiet time for this path, 
1282                                 //so generate the events that were pent up
1283                                 for (PathWatchEvent p:pending.getEvents())
1284                                 {
1285                                     notifiableEvents.add(p);
1286                                 }
1287                                 // remove from pending list
1288                                 pendingEvents.remove(path);
1289                             }
1290                         }
1291                     }
1292                 }
1293             }
1294             catch (ClosedWatchServiceException e)
1295             {
1296                 // Normal shutdown of watcher
1297                 return;
1298             }
1299             catch (InterruptedException e)
1300             {
1301                 if (isRunning())
1302                 {
1303                     LOG.warn(e);
1304                 }
1305                 else
1306                 {
1307                     LOG.ignore(e);
1308                 }
1309                 return;
1310             }
1311 
1312             //If there was some new events to process
1313             if (key != null)
1314             {
1315 
1316                 Config config = keys.get(key);
1317                 if (config == null)
1318                 {
1319                     if (LOG.isDebugEnabled())
1320                     {
1321                         LOG.debug("WatchKey not recognized: {}",key);
1322                     }
1323                     continue;
1324                 }
1325 
1326                 for (WatchEvent<?> event : key.pollEvents())
1327                 {
1328                     @SuppressWarnings("unchecked")
1329                     WatchEvent.Kind<Path> kind = (Kind<Path>)event.kind();
1330                     WatchEvent<Path> ev = cast(event);
1331                     Path name = ev.context();
1332                     Path child = config.dir.resolve(name);
1333 
1334                     if (kind == ENTRY_CREATE)
1335                     {
1336                         // handle special case for registering new directories
1337                         // recursively
1338                         if (Files.isDirectory(child,LinkOption.NOFOLLOW_LINKS))
1339                         {
1340                             try
1341                             {
1342                                 prepareConfig(config.asSubConfig(child));
1343                             }
1344                             catch (IOException e)
1345                             {
1346                                 LOG.warn(e);
1347                             }
1348                         }
1349                         else if (config.matches(child))
1350                         {
1351                             addToPendingList(child, new PathWatchEvent(child,ev));
1352                         }
1353                     }
1354                     else if (config.matches(child))
1355                     {
1356                         addToPendingList(child, new PathWatchEvent(child,ev));      
1357                     }
1358                 }
1359             }
1360 
1361             //Send any notifications generated this pass
1362             notifyOnPathWatchEvents(notifiableEvents);
1363             notifiableEvents.clear();
1364             
1365             if (key != null && !key.reset())
1366             {
1367                 keys.remove(key);
1368                 if (keys.isEmpty())
1369                 {
1370                     return; // all done, no longer monitoring anything
1371                 }
1372             }
1373         }
1374     }
1375     
1376     
1377     /**
1378      * Add an event reported by the WatchService to list of pending events
1379      * that will be sent after their quiet time has expired.
1380      * 
1381      * @param path the path to add to the pending list
1382      * @param event the pending event
1383      */
1384     public void addToPendingList (Path path, PathWatchEvent event)
1385     {
1386         PathPendingEvents pending = pendingEvents.get(path);
1387         
1388         //Are there already pending events for this path?
1389         if (pending == null)
1390         {
1391             //No existing pending events, create pending list
1392             pendingEvents.put(path,new PathPendingEvents(path, event));
1393         }
1394         else
1395         {
1396             //There are already some events pending for this path
1397             pending.addEvent(event);
1398         }
1399     }
1400     
1401     
1402     /**
1403      * Whether or not to issue notifications for directories and files that
1404      * already exist when the watcher starts.
1405      * 
1406      * @param notify true if existing paths should be notified or not
1407      */
1408     public void setNotifyExistingOnStart (boolean notify)
1409     {
1410         _notifyExistingOnStart = notify;
1411     }
1412     
1413     public boolean isNotifyExistingOnStart ()
1414     {
1415         return _notifyExistingOnStart;
1416     }
1417 
1418     /**
1419      * Set the quiet time.
1420      * 
1421      * @param duration the quiet time duration
1422      * @param unit the quite time unit
1423      */
1424     public void setUpdateQuietTime(long duration, TimeUnit unit)
1425     {
1426         long desiredMillis = unit.toMillis(duration);
1427         
1428         if (watchService != null && !this.nativeWatchService && (desiredMillis < 5000))
1429         {
1430             LOG.warn("Quiet Time is too low for non-native WatchService [{}]: {} < 5000 ms (defaulting to 5000 ms)",watchService.getClass().getName(),desiredMillis);
1431             this.updateQuietTimeDuration = 5000;
1432             this.updateQuietTimeUnit = TimeUnit.MILLISECONDS;
1433             return;
1434         }
1435 
1436         if (IS_WINDOWS && (desiredMillis < 1000))
1437         {
1438             LOG.warn("Quiet Time is too low for Microsoft Windows: {} < 1000 ms (defaulting to 1000 ms)",desiredMillis);
1439             this.updateQuietTimeDuration = 1000;
1440             this.updateQuietTimeUnit = TimeUnit.MILLISECONDS;
1441             return;
1442         }
1443         
1444         // All other OS and watch service combinations can use desired setting
1445         this.updateQuietTimeDuration = duration;
1446         this.updateQuietTimeUnit = unit;
1447     }
1448 
1449     @Override
1450     public String toString()
1451     {
1452         StringBuilder s = new StringBuilder(this.getClass().getName());
1453         appendConfigId(s);
1454         return s.toString();
1455     }
1456 }