1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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
60
61
62
63
64
65
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;
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
100
101
102
103
104 public void addExclude(PathMatcher matcher)
105 {
106 this.excludes.add(matcher);
107 }
108
109
110
111
112
113
114
115
116
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
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144 public void addExcludeGlobRelative(String pattern)
145 {
146 addExclude(toGlobPattern(dir,pattern));
147 }
148
149
150
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 + "\\..*$");
163 addExclude("regex:^.*" + PATTERN_SEP + "\\..*" + PATTERN_SEP + ".*$");
164 }
165 }
166
167
168
169
170
171
172
173
174 public void addExcludes(List<String> syntaxAndPatterns)
175 {
176 for (String syntaxAndPattern : syntaxAndPatterns)
177 {
178 addExclude(syntaxAndPattern);
179 }
180 }
181
182
183
184
185
186
187
188 public void addInclude(PathMatcher matcher)
189 {
190 this.includes.add(matcher);
191 }
192
193
194
195
196
197
198
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
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226 public void addIncludeGlobRelative(String pattern)
227 {
228 addInclude(toGlobPattern(dir,pattern));
229 }
230
231
232
233
234
235
236
237
238 public void addIncludes(List<String> syntaxAndPatterns)
239 {
240 for (String syntaxAndPattern : syntaxAndPatterns)
241 {
242 addInclude(syntaxAndPattern);
243 }
244 }
245
246
247
248
249
250
251
252
253
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;
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
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
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
362
363
364
365
366
367
368 public void setRecurseDepth(int depth)
369 {
370 this.recurseDepth = depth;
371 }
372
373
374
375
376
377
378
379
380
381
382 public boolean shouldRecurseDirectory(Path child)
383 {
384 if (!child.startsWith(dir))
385 {
386
387 return false;
388 }
389
390
391 if (isRecurseDepthUnlimited())
392 return true;
393
394
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
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
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
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
491
492
493
494
495
496
497
498
499
500
501
502 @Override
503 public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException
504 {
505
506
507
508
509
510
511
512
513
514
515
516 if (!base.isExcluded(dir))
517 {
518 if (base.isIncluded(dir))
519 {
520 if (watcher.isNotifiable())
521 {
522
523
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
534
535
536
537
538 if ((base.getPath().equals(dir) && (base.isRecurseDepthUnlimited() || base.getRecurseDepth() >= 0)) || base.shouldRecurseDirectory(dir))
539 watcher.register(dir,base);
540 }
541
542
543
544
545
546
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
557
558
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
577
578 public static interface Listener extends EventListener
579 {
580 void onPathWatchEvent(PathWatchEvent event);
581 }
582
583
584
585
586
587
588 public static interface EventListListener extends EventListener
589 {
590 void onPathWatchEvents(List<PathWatchEvent> events);
591 }
592
593
594
595
596
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
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
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
706
707 @Override
708 public String toString()
709 {
710 return String.format("PathWatchEvent[%s|%s]",type,path);
711 }
712 }
713
714
715
716
717
718
719
720
721
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
754
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
790
791
792
793
794
795
796
797
798
799
800
801
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();
810 boolean fileSizeChanged = (_lastFileSize != fileSize);
811 _lastFileSize = fileSize;
812
813 if ((now > pastdue) && (!fileSizeChanged
814 {
815
816
817
818 return true;
819 }
820
821 return false;
822 }
823
824 }
825
826
827
828
829
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
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<>();
872 private List<Config> configs = new ArrayList<>();
873
874
875
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
887
888 public PathWatcher()
889 {
890 }
891
892
893
894
895
896
897
898
899 public void watch (final Path file)
900 {
901
902
903
904 Path abs = file;
905 if (!abs.isAbsolute())
906 {
907 abs = file.toAbsolutePath();
908 }
909
910
911
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
924 if (config == null)
925 {
926 config = new Config(abs.getParent());
927
928 config.addIncludeGlobRelative("");
929
930 config.addIncludeGlobRelative(file.getFileName().toString());
931 watch(config);
932 }
933 else
934
935 config.addIncludeGlobRelative(file.getFileName().toString());
936 }
937
938
939
940
941
942
943
944 public void watch (final Config config)
945 {
946
947 configs.add(config);
948 }
949
950
951
952
953
954
955
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
970
971
972
973 public void addListener(EventListener listener)
974 {
975 listeners.add(listener);
976 }
977
978
979
980
981
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
1012
1013 @Override
1014 protected void doStart() throws Exception
1015 {
1016
1017 createWatchService();
1018
1019
1020 setUpdateQuietTime(getUpdateQuietTimeMillis(), TimeUnit.MILLISECONDS);
1021
1022
1023
1024 for (Config c:configs)
1025 prepareConfig(c);
1026
1027
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
1040
1041 @Override
1042 protected void doStop() throws Exception
1043 {
1044 if (watchService != null)
1045 watchService.close();
1046 watchService = null;
1047 thread = null;
1048 keys.clear();
1049 pendingEvents.clear();
1050 super.doStop();
1051 }
1052
1053
1054
1055
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
1069
1070
1071
1072
1073 private void createWatchService () throws IOException
1074 {
1075
1076 this.watchService = FileSystems.getDefault().newWatchService();
1077
1078 WatchEvent.Modifier modifiers[] = null;
1079 boolean nativeService = true;
1080
1081
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
1101 LOG.ignore(t);
1102 }
1103
1104 this.watchModifiers = modifiers;
1105 this.nativeWatchService = nativeService;
1106 }
1107
1108
1109
1110
1111
1112
1113
1114
1115 protected boolean isNotifiable ()
1116 {
1117 return (isStarted() || (!isStarted() && isNotifyExistingOnStart()));
1118 }
1119
1120
1121
1122
1123
1124
1125 public Iterator<EventListener> getListeners()
1126 {
1127 return listeners.iterator();
1128 }
1129
1130
1131
1132
1133
1134
1135 public long getUpdateQuietTimeMillis()
1136 {
1137 return TimeUnit.MILLISECONDS.convert(updateQuietTimeDuration,updateQuietTimeUnit);
1138 }
1139
1140
1141
1142
1143
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
1184
1185
1186
1187
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
1196 WatchKey key = dir.register(watchService,WATCH_EVENT_KINDS,watchModifiers);
1197 keys.put(key,root.asSubConfig(dir));
1198 } else
1199 {
1200
1201 WatchKey key = dir.register(watchService,WATCH_EVENT_KINDS);
1202 keys.put(key,root.asSubConfig(dir));
1203 }
1204 }
1205
1206
1207
1208
1209
1210
1211
1212 public boolean removeListener(Listener listener)
1213 {
1214 return listeners.remove(listener);
1215 }
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235 @Override
1236 public void run()
1237 {
1238
1239 List<PathWatchEvent> notifiableEvents = new ArrayList<PathWatchEvent>();
1240
1241
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
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
1263
1264 if (NOISY_LOG.isDebugEnabled())
1265 NOISY_LOG.debug("Waiting for poll({}, {})",updateQuietTimeDuration,updateQuietTimeUnit);
1266
1267 key = watchService.poll(updateQuietTimeDuration,updateQuietTimeUnit);
1268
1269
1270 if (key == null)
1271 {
1272 long now = System.currentTimeMillis();
1273
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
1280
1281 for (PathWatchEvent p:pending.getEvents())
1282 {
1283 notifiableEvents.add(p);
1284 }
1285
1286 pendingEvents.remove(path);
1287 }
1288 }
1289 }
1290 }
1291 }
1292 catch (ClosedWatchServiceException e)
1293 {
1294
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
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
1335
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
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;
1369 }
1370 }
1371 }
1372 }
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382 public void addToPendingList (Path path, PathWatchEvent event)
1383 {
1384 PathPendingEvents pending = pendingEvents.get(path);
1385
1386
1387 if (pending == null)
1388 {
1389
1390 pendingEvents.put(path,new PathPendingEvents(path, event));
1391 }
1392 else
1393 {
1394
1395 pending.addEvent(event);
1396 }
1397 }
1398
1399
1400
1401
1402
1403
1404
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
1418
1419
1420
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
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 }