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

Last change on this file since 12891 was 12827, checked in by Don-vip, 7 years ago

fix #15293 - silence exceptions with temp/empty files

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