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}