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