View Javadoc

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