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}