001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.io;
003
004import java.io.File;
005import java.io.IOException;
006import java.nio.file.FileSystems;
007import java.nio.file.Path;
008import java.nio.file.StandardWatchEventKinds;
009import java.nio.file.WatchEvent;
010import java.nio.file.WatchEvent.Kind;
011import java.nio.file.WatchKey;
012import java.nio.file.WatchService;
013import java.util.Collections;
014import java.util.HashMap;
015import java.util.Map;
016
017import org.openstreetmap.josm.Main;
018import org.openstreetmap.josm.data.validation.OsmValidator;
019import org.openstreetmap.josm.data.validation.tests.MapCSSTagChecker;
020import org.openstreetmap.josm.gui.mappaint.MapPaintStyles.MapPaintStyleLoader;
021import org.openstreetmap.josm.gui.mappaint.StyleSource;
022import org.openstreetmap.josm.gui.mappaint.mapcss.parsergen.ParseException;
023import org.openstreetmap.josm.gui.preferences.SourceEntry;
024import org.openstreetmap.josm.tools.CheckParameterUtil;
025
026/**
027 * Background thread that monitors certain files and perform relevant actions when they change.
028 * @since 7185
029 */
030public class FileWatcher {
031
032    private WatchService watcher;
033    private Thread thread;
034
035    private final Map<Path, StyleSource> styleMap = new HashMap<>();
036    private final Map<Path, SourceEntry> ruleMap = new HashMap<>();
037
038    /**
039     * Constructs a new {@code FileWatcher}.
040     */
041    public FileWatcher() {
042        try {
043            watcher = FileSystems.getDefault().newWatchService();
044            thread = new Thread(new Runnable() {
045                @Override
046                public void run() {
047                    processEvents();
048                }
049            }, "File Watcher");
050        } catch (IOException e) {
051            Main.error(e);
052        }
053    }
054
055    /**
056     * Starts the File Watcher thread.
057     */
058    public final void start() {
059        if (!thread.isAlive()) {
060            thread.start();
061        }
062    }
063
064    /**
065     * Registers a map paint style for local file changes, allowing dynamic reloading.
066     * @param style The style to watch
067     * @throws IllegalArgumentException if {@code style} is null or if it does not provide a local file
068     * @throws IllegalStateException if the watcher service failed to start
069     * @throws IOException if an I/O error occurs
070     */
071    public void registerStyleSource(StyleSource style) throws IOException {
072        register(style, styleMap);
073    }
074
075    /**
076     * Registers a validator rule for local file changes, allowing dynamic reloading.
077     * @param rule The rule to watch
078     * @throws IllegalArgumentException if {@code rule} is null or if it does not provide a local file
079     * @throws IllegalStateException if the watcher service failed to start
080     * @throws IOException if an I/O error occurs
081     * @since 7276
082     */
083    public void registerValidatorRule(SourceEntry rule) throws IOException {
084        register(rule, ruleMap);
085    }
086
087    private <T extends SourceEntry> void register(T obj, Map<Path, T> map) throws IOException {
088        CheckParameterUtil.ensureParameterNotNull(obj, "obj");
089        if (watcher == null) {
090            throw new IllegalStateException("File watcher is not available");
091        }
092        // Get local file, as this method is only called for local style sources
093        File file = new File(obj.url);
094        // Get parent directory as WatchService allows only to monitor directories, not single files
095        File dir = file.getParentFile();
096        if (dir == null) {
097            throw new IllegalArgumentException("Resource "+obj+" does not have a parent directory");
098        }
099        synchronized (this) {
100            // Register directory. Can be called several times for a same directory without problem
101            // (it returns the same key so it should not send events several times)
102            dir.toPath().register(watcher, StandardWatchEventKinds.ENTRY_MODIFY, StandardWatchEventKinds.ENTRY_CREATE);
103            map.put(file.toPath(), obj);
104        }
105    }
106
107    /**
108     * Process all events for the key queued to the watcher.
109     */
110    private void processEvents() {
111        if (Main.isDebugEnabled()) {
112            Main.debug("File watcher thread started");
113        }
114        while (true) {
115
116            // wait for key to be signaled
117            WatchKey key;
118            try {
119                key = watcher.take();
120            } catch (InterruptedException x) {
121                return;
122            }
123
124            for (WatchEvent<?> event: key.pollEvents()) {
125                Kind<?> kind = event.kind();
126
127                if (StandardWatchEventKinds.OVERFLOW.equals(kind)) {
128                    continue;
129                }
130
131                // The filename is the context of the event.
132                @SuppressWarnings("unchecked")
133                WatchEvent<Path> ev = (WatchEvent<Path>) event;
134                Path filename = ev.context();
135                if (filename == null) {
136                    continue;
137                }
138
139                // Only way to get full path (http://stackoverflow.com/a/7802029/2257172)
140                Path fullPath = ((Path) key.watchable()).resolve(filename);
141
142                synchronized (this) {
143                    StyleSource style = styleMap.get(fullPath);
144                    SourceEntry rule = ruleMap.get(fullPath);
145                    if (style != null) {
146                        Main.info("Map style "+style.getDisplayString()+" has been modified. Reloading style...");
147                        Main.worker.submit(new MapPaintStyleLoader(Collections.singleton(style)));
148                    } else if (rule != null) {
149                        Main.info("Validator rule "+rule.getDisplayString()+" has been modified. Reloading rule...");
150                        MapCSSTagChecker tagChecker = OsmValidator.getTest(MapCSSTagChecker.class);
151                        if (tagChecker != null) {
152                            try {
153                                tagChecker.addMapCSS(rule.url);
154                            } catch (IOException | ParseException e) {
155                                Main.warn(e);
156                            }
157                        }
158                    } else if (Main.isDebugEnabled()) {
159                        Main.debug("Received "+kind.name()+" event for unregistered file: "+fullPath);
160                    }
161                }
162            }
163
164            // Reset the key -- this step is critical to receive
165            // further watch events. If the key is no longer valid, the directory
166            // is inaccessible so exit the loop.
167            if (!key.reset()) {
168                break;
169            }
170        }
171    }
172}