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