Index: trunk/src/org/openstreetmap/josm/data/validation/tests/MapCSSTagChecker.java
===================================================================
--- trunk/src/org/openstreetmap/josm/data/validation/tests/MapCSSTagChecker.java	(revision 7275)
+++ trunk/src/org/openstreetmap/josm/data/validation/tests/MapCSSTagChecker.java	(revision 7276)
@@ -45,4 +45,5 @@
 import org.openstreetmap.josm.gui.mappaint.mapcss.parsergen.MapCSSParser;
 import org.openstreetmap.josm.gui.mappaint.mapcss.parsergen.ParseException;
+import org.openstreetmap.josm.gui.preferences.SourceEntry;
 import org.openstreetmap.josm.gui.preferences.validator.ValidatorPreference;
 import org.openstreetmap.josm.gui.preferences.validator.ValidatorTagCheckerRulesPreference;
@@ -539,10 +540,13 @@
      * @throws ParseException if the config file does not match MapCSS syntax
      * @throws IOException if any I/O error occurs
-     * @since
+     * @since 7275
      */
-    public void addMapCSS(String url) throws ParseException, IOException {
+    public synchronized void addMapCSS(String url) throws ParseException, IOException {
         CheckParameterUtil.ensureParameterNotNull(url, "url");
-        try (InputStream s = new CachedFile(url).getInputStream()) {
-            checks.putAll(url, TagCheck.readMapCSS(new BufferedReader(UTFInputStreamReader.create(s))));
+        CachedFile cache = new CachedFile(url);
+        try (InputStream s = cache.getInputStream()) {
+            List<TagCheck> tagchecks = TagCheck.readMapCSS(new BufferedReader(UTFInputStreamReader.create(s)));
+            checks.remove(url);
+            checks.putAll(url, tagchecks);
         }
     }
@@ -551,5 +555,9 @@
     public synchronized void initialize() throws Exception {
         checks.clear();
-        for (String i : new ValidatorTagCheckerRulesPreference.RulePrefHelper().getActiveUrls()) {
+        for (SourceEntry source : new ValidatorTagCheckerRulesPreference.RulePrefHelper().get()) {
+            if (!source.active) {
+                continue;
+            }
+            String i = source.url;
             try {
                 if (i.startsWith("resource:")) {
@@ -559,4 +567,11 @@
                 }
                 addMapCSS(i);
+                if (Main.pref.getBoolean("validator.auto_reload_local_rules", true) && source.isLocal()) {
+                    try {
+                        Main.fileWatcher.registerValidatorRule(source);
+                    } catch (IOException e) {
+                        Main.error(e);
+                    }
+                }
             } catch (IOException ex) {
                 Main.warn(tr("Failed to add {0} to tag checker", i));
Index: trunk/src/org/openstreetmap/josm/gui/mappaint/MapPaintStyles.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/mappaint/MapPaintStyles.java	(revision 7275)
+++ trunk/src/org/openstreetmap/josm/gui/mappaint/MapPaintStyles.java	(revision 7276)
@@ -4,4 +4,5 @@
 import static org.openstreetmap.josm.tools.I18n.tr;
 
+import java.io.File;
 import java.io.IOException;
 import java.io.InputStreamReader;
@@ -164,12 +165,11 @@
         List<String> dirs = new LinkedList<>();
 
-        String sourceDir = source.getLocalSourceDir();
+        File sourceDir = source.getLocalSourceDir();
         if (sourceDir != null) {
-            dirs.add(sourceDir);
+            dirs.add(sourceDir.getPath());
         }
 
         Collection<String> prefIconDirs = Main.pref.getCollection("mappaint.icon.sources");
-        for(String fileset : prefIconDirs)
-        {
+        for (String fileset : prefIconDirs) {
             String[] a;
             if(fileset.indexOf('=') >= 0) {
Index: trunk/src/org/openstreetmap/josm/gui/preferences/SourceEntry.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/preferences/SourceEntry.java	(revision 7275)
+++ trunk/src/org/openstreetmap/josm/gui/preferences/SourceEntry.java	(revision 7276)
@@ -11,10 +11,11 @@
 /**
  * A source entry primarily used to save the user's selection of mappaint styles,
- * but also for preset sources.
+ * but also for preset sources or validator rules.
+ * @since 3796
  */
 public class SourceEntry {
 
     /**
-     *  A URL can be anything that MirroredInputStream understands, i.e.
+     *  A URL can be anything that CachedFile understands, i.e.
      *  a local file, http://, or a file from the current jar
      */
@@ -53,4 +54,19 @@
     public boolean active;
 
+    /**
+     * Constructs a new {@code SourceEntry}.
+     * @param url URL that {@link org.openstreetmap.josm.io.CachedFile} understands
+     * @param isZip if url is a zip file and the resource is inside the zip file
+     * @param zipEntryPath If {@code isZip} is {@code true}, denotes the path inside the zip file
+     * @param name Source name
+     * @param title title that can be used as menu entry
+     * @param active boolean flag that can be used to turn the source on or off at runtime
+     * @see #url
+     * @see #isZip
+     * @see #zipEntryPath
+     * @see #name
+     * @see #title
+     * @see #active
+     */
     public SourceEntry(String url, boolean isZip, String zipEntryPath, String name, String title, boolean active) {
         this.url = url;
@@ -62,8 +78,23 @@
     }
 
+    /**
+     * Constructs a new {@code SourceEntry}.
+     * @param url URL that {@link org.openstreetmap.josm.io.CachedFile} understands
+     * @param name Source name
+     * @param title title that can be used as menu entry
+     * @param active boolean flag that can be used to turn the source on or off at runtime
+     * @see #url
+     * @see #name
+     * @see #title
+     * @see #active
+     */
     public SourceEntry(String url, String name, String title, Boolean active) {
         this(url, false, null, name, title, active);
     }
 
+    /**
+     * Constructs a new {@code SourceEntry}.
+     * @param e existing source entry to copy
+     */
     public SourceEntry(SourceEntry e) {
         this.url = e.url;
@@ -118,6 +149,7 @@
 
     /**
-     * extract file part from url, e.g.:
-     * http://www.test.com/file.xml?format=text --&gt; file.xml
+     * Extracts file part from url, e.g.:
+     * <code>http://www.test.com/file.xml?format=text --&gt; file.xml</code>
+     * @return The filename part of the URL
      */
     public String getFileNamePart() {
@@ -141,4 +173,8 @@
     }
 
+    /**
+     * Determines if this source denotes a file on a local filesystem.
+     * @return {@code true} if the source is a local file
+     */
     public boolean isLocal() {
         if (url.startsWith("http://") || url.startsWith("https://") || url.startsWith("resource://"))
@@ -147,12 +183,13 @@
     }
 
-    public String getLocalSourceDir() {
+    /**
+     * Return the source directory, only for local files.
+     * @return The source directory, or {@code null} if this file isn't local, or does not have a parent
+     * @since 7276
+     */
+    public File getLocalSourceDir() {
         if (!isLocal())
             return null;
-        File f = new File(url);
-        File dir = f.getParentFile();
-        if (dir == null)
-            return null;
-        return dir.getPath();
+        return new File(url).getParentFile();
     }
 
Index: trunk/src/org/openstreetmap/josm/io/FileWatcher.java
===================================================================
--- trunk/src/org/openstreetmap/josm/io/FileWatcher.java	(revision 7275)
+++ trunk/src/org/openstreetmap/josm/io/FileWatcher.java	(revision 7276)
@@ -16,6 +16,10 @@
 
 import org.openstreetmap.josm.Main;
+import org.openstreetmap.josm.data.validation.OsmValidator;
+import org.openstreetmap.josm.data.validation.tests.MapCSSTagChecker;
 import org.openstreetmap.josm.gui.mappaint.MapPaintStyles.MapPaintStyleLoader;
 import org.openstreetmap.josm.gui.mappaint.StyleSource;
+import org.openstreetmap.josm.gui.mappaint.mapcss.parsergen.ParseException;
+import org.openstreetmap.josm.gui.preferences.SourceEntry;
 import org.openstreetmap.josm.tools.CheckParameterUtil;
 
@@ -29,4 +33,5 @@
 
     private final Map<Path, StyleSource> styleMap = new HashMap<>();
+    private final Map<Path, SourceEntry> ruleMap = new HashMap<>();
 
     /**
@@ -55,18 +60,30 @@
      */
     public void registerStyleSource(StyleSource style) throws IOException {
-        CheckParameterUtil.ensureParameterNotNull(style, "style");
+        register(style, styleMap);
+    }
+
+    /**
+     * Registers a validator rule for local file changes, allowing dynamic reloading.
+     * @param rule The rule to watch
+     * @throws IllegalArgumentException if {@code rule} is null or if it does not provide a local file
+     * @throws IllegalStateException if the watcher service failed to start
+     * @throws IOException if an I/O error occurs
+     * @since 7276
+     */
+    public void registerValidatorRule(SourceEntry rule) throws IOException {
+        register(rule, ruleMap);
+    }
+
+    private <T extends SourceEntry> void register(T obj, Map<Path, T> map) throws IOException {
+        CheckParameterUtil.ensureParameterNotNull(obj, "obj");
         if (watcher == null) {
             throw new IllegalStateException("File watcher is not available");
         }
-        CachedFile cf = style.getCachedFile();
-        // Get underlying file
-        File file = cf.getFile();
-        if (file == null) {
-            throw new IllegalArgumentException("Style "+style+" does not have a local file");
-        }
+        // Get local file, as this method is only called for local style sources
+        File file = new File(obj.url);
         // Get parent directory as WatchService allows only to monitor directories, not single files
         File dir = file.getParentFile();
         if (dir == null) {
-            throw new IllegalArgumentException("Style "+style+" does not have a parent directory");
+            throw new IllegalArgumentException("Resource "+obj+" does not have a parent directory");
         }
         synchronized(this) {
@@ -74,5 +91,5 @@
             // (it returns the same key so it should not send events several times)
             dir.toPath().register(watcher, StandardWatchEventKinds.ENTRY_MODIFY, StandardWatchEventKinds.ENTRY_CREATE);
-            styleMap.put(file.toPath(), style);
+            map.put(file.toPath(), obj);
         }
     }
@@ -115,8 +132,18 @@
                 synchronized(this) {
                     StyleSource style = styleMap.get(fullPath);
+                    SourceEntry rule = ruleMap.get(fullPath);
                     if (style != null) {
                         Main.info("Map style "+style.getDisplayString()+" has been modified. Reloading style...");
-                        //style.loadStyleSource();
                         Main.worker.submit(new MapPaintStyleLoader(Collections.singleton(style)));
+                    } else if (rule != null) {
+                        Main.info("Validator rule "+rule.getDisplayString()+" has been modified. Reloading rule...");
+                        MapCSSTagChecker tagChecker = OsmValidator.getTest(MapCSSTagChecker.class);
+                        if (tagChecker != null) {
+                            try {
+                                tagChecker.addMapCSS(rule.url);
+                            } catch (IOException | ParseException e) {
+                                Main.warn(e);
+                            }
+                        }
                     } else if (Main.isDebugEnabled()) {
                         Main.debug("Received "+kind.name()+" event for unregistered file: "+fullPath);
Index: trunk/src/org/openstreetmap/josm/tools/MultiMap.java
===================================================================
--- trunk/src/org/openstreetmap/josm/tools/MultiMap.java	(revision 7275)
+++ trunk/src/org/openstreetmap/josm/tools/MultiMap.java	(revision 7276)
@@ -213,4 +213,20 @@
 
     @Override
+    public int hashCode() {
+        return map.hashCode();
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj)
+            return true;
+        if (obj == null)
+            return false;
+        if (!(obj instanceof MultiMap))
+            return false;
+        return map.equals(((MultiMap<?,?>) obj).map);
+    }
+
+    @Override
     public String toString() {
         List<String> entries = new ArrayList<>(map.size());
