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