source: josm/trunk/src/org/openstreetmap/josm/io/FileWatcher.java@ 14145

Last change on this file since 14145 was 14128, checked in by Don-vip, 6 years ago

see #15229 - deprecate Main.fileWatcher

  • Property svn:eol-style set to native
File size: 6.5 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.io;
3
4import java.io.File;
5import java.io.IOException;
6import java.nio.file.FileSystems;
7import java.nio.file.Files;
8import java.nio.file.Path;
9import java.nio.file.StandardWatchEventKinds;
10import java.nio.file.WatchEvent;
11import java.nio.file.WatchEvent.Kind;
12import java.nio.file.WatchKey;
13import java.nio.file.WatchService;
14import java.util.EnumMap;
15import java.util.HashMap;
16import java.util.Map;
17import java.util.Objects;
18import java.util.function.Consumer;
19
20import org.openstreetmap.josm.data.preferences.sources.SourceEntry;
21import org.openstreetmap.josm.data.preferences.sources.SourceType;
22import org.openstreetmap.josm.tools.CheckParameterUtil;
23import org.openstreetmap.josm.tools.Logging;
24
25/**
26 * Background thread that monitors certain files and perform relevant actions when they change.
27 * @since 7185
28 */
29public class FileWatcher {
30
31 private WatchService watcher;
32 private Thread thread;
33
34 private static final Map<SourceType, Consumer<SourceEntry>> loaderMap = new EnumMap<>(SourceType.class);
35 private final Map<Path, SourceEntry> sourceMap = new HashMap<>();
36
37 private static class InstanceHolder {
38 static final FileWatcher INSTANCE = new FileWatcher();
39 }
40
41 /**
42 * Returns the default instance.
43 * @return the default instance
44 * @since 14128
45 */
46 public static FileWatcher getDefaultInstance() {
47 return InstanceHolder.INSTANCE;
48 }
49
50 /**
51 * Constructs a new {@code FileWatcher}.
52 */
53 public FileWatcher() {
54 try {
55 watcher = FileSystems.getDefault().newWatchService();
56 thread = new Thread((Runnable) this::processEvents, "File Watcher");
57 } catch (IOException e) {
58 Logging.error(e);
59 }
60 }
61
62 /**
63 * Starts the File Watcher thread.
64 */
65 public final void start() {
66 if (thread != null && !thread.isAlive()) {
67 thread.start();
68 }
69 }
70
71 /**
72 * Registers a source for local file changes, allowing dynamic reloading.
73 * @param src The source to watch
74 * @throws IllegalArgumentException if {@code rule} is null or if it does not provide a local file
75 * @throws IllegalStateException if the watcher service failed to start
76 * @throws IOException if an I/O error occurs
77 * @since 12825
78 */
79 public void registerSource(SourceEntry src) throws IOException {
80 CheckParameterUtil.ensureParameterNotNull(src, "src");
81 if (watcher == null) {
82 throw new IllegalStateException("File watcher is not available");
83 }
84 // Get local file, as this method is only called for local style sources
85 File file = new File(src.url);
86 // Get parent directory as WatchService allows only to monitor directories, not single files
87 File dir = file.getParentFile();
88 if (dir == null) {
89 throw new IllegalArgumentException("Resource "+src+" does not have a parent directory");
90 }
91 synchronized (this) {
92 // Register directory. Can be called several times for a same directory without problem
93 // (it returns the same key so it should not send events several times)
94 dir.toPath().register(watcher, StandardWatchEventKinds.ENTRY_MODIFY, StandardWatchEventKinds.ENTRY_CREATE);
95 sourceMap.put(file.toPath(), src);
96 }
97 }
98
99 /**
100 * Registers a source loader, allowing dynamic reloading when an entry changes.
101 * @param type the source type for which the loader operates
102 * @param loader the loader in charge of reloading any source of given type when it changes
103 * @return the previous loader registered for this source type, if any
104 * @since 12825
105 */
106 public static Consumer<SourceEntry> registerLoader(SourceType type, Consumer<SourceEntry> loader) {
107 return loaderMap.put(Objects.requireNonNull(type, "type"), Objects.requireNonNull(loader, "loader"));
108 }
109
110 /**
111 * Process all events for the key queued to the watcher.
112 */
113 private void processEvents() {
114 Logging.debug("File watcher thread started");
115 while (true) {
116
117 // wait for key to be signaled
118 WatchKey key;
119 try {
120 key = watcher.take();
121 } catch (InterruptedException ex) {
122 Thread.currentThread().interrupt();
123 return;
124 }
125
126 for (WatchEvent<?> event: key.pollEvents()) {
127 Kind<?> kind = event.kind();
128
129 if (StandardWatchEventKinds.OVERFLOW.equals(kind)) {
130 continue;
131 }
132
133 // The filename is the context of the event.
134 @SuppressWarnings("unchecked")
135 WatchEvent<Path> ev = (WatchEvent<Path>) event;
136 Path filename = ev.context();
137 if (filename == null) {
138 continue;
139 }
140
141 // Only way to get full path (http://stackoverflow.com/a/7802029/2257172)
142 Path fullPath = ((Path) key.watchable()).resolve(filename);
143
144 try {
145 // Some filesystems fire two events when a file is modified. Skip first event (file is empty)
146 if (Files.size(fullPath) == 0) {
147 continue;
148 }
149 } catch (IOException ex) {
150 Logging.trace(ex);
151 continue;
152 }
153
154 synchronized (this) {
155 SourceEntry source = sourceMap.get(fullPath);
156 if (source != null) {
157 Consumer<SourceEntry> loader = loaderMap.get(source.type);
158 if (loader != null) {
159 Logging.info("Source "+source.getDisplayString()+" has been modified. Reloading it...");
160 loader.accept(source);
161 } else {
162 Logging.warn("Received {0} event for unregistered source type: {1}", kind.name(), source.type);
163 }
164 } else if (Logging.isDebugEnabled()) {
165 Logging.debug("Received {0} event for unregistered file: {1}", kind.name(), fullPath);
166 }
167 }
168 }
169
170 // Reset the key -- this step is critical to receive
171 // further watch events. If the key is no longer valid, the directory
172 // is inaccessible so exit the loop.
173 if (!key.reset()) {
174 break;
175 }
176 }
177 }
178}
Note: See TracBrowser for help on using the repository browser.