001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.data.validation.tests; 003 004import static org.openstreetmap.josm.tools.I18n.tr; 005 006import java.io.BufferedReader; 007import java.io.IOException; 008import java.io.InputStream; 009import java.io.Reader; 010import java.text.MessageFormat; 011import java.util.ArrayList; 012import java.util.Arrays; 013import java.util.Collection; 014import java.util.Collections; 015import java.util.HashMap; 016import java.util.Iterator; 017import java.util.LinkedHashMap; 018import java.util.LinkedHashSet; 019import java.util.LinkedList; 020import java.util.List; 021import java.util.Map; 022import java.util.Set; 023import java.util.regex.Matcher; 024import java.util.regex.Pattern; 025 026import org.openstreetmap.josm.Main; 027import org.openstreetmap.josm.command.ChangePropertyCommand; 028import org.openstreetmap.josm.command.ChangePropertyKeyCommand; 029import org.openstreetmap.josm.command.Command; 030import org.openstreetmap.josm.command.DeleteCommand; 031import org.openstreetmap.josm.command.SequenceCommand; 032import org.openstreetmap.josm.data.osm.OsmPrimitive; 033import org.openstreetmap.josm.data.osm.OsmUtils; 034import org.openstreetmap.josm.data.osm.Tag; 035import org.openstreetmap.josm.data.validation.FixableTestError; 036import org.openstreetmap.josm.data.validation.Severity; 037import org.openstreetmap.josm.data.validation.Test; 038import org.openstreetmap.josm.data.validation.TestError; 039import org.openstreetmap.josm.gui.mappaint.Environment; 040import org.openstreetmap.josm.gui.mappaint.Keyword; 041import org.openstreetmap.josm.gui.mappaint.MultiCascade; 042import org.openstreetmap.josm.gui.mappaint.mapcss.Condition; 043import org.openstreetmap.josm.gui.mappaint.mapcss.Expression; 044import org.openstreetmap.josm.gui.mappaint.mapcss.Instruction; 045import org.openstreetmap.josm.gui.mappaint.mapcss.MapCSSRule; 046import org.openstreetmap.josm.gui.mappaint.mapcss.MapCSSRule.Declaration; 047import org.openstreetmap.josm.gui.mappaint.mapcss.MapCSSStyleSource; 048import org.openstreetmap.josm.gui.mappaint.mapcss.Selector; 049import org.openstreetmap.josm.gui.mappaint.mapcss.Selector.GeneralSelector; 050import org.openstreetmap.josm.gui.mappaint.mapcss.parsergen.MapCSSParser; 051import org.openstreetmap.josm.gui.mappaint.mapcss.parsergen.ParseException; 052import org.openstreetmap.josm.gui.preferences.SourceEntry; 053import org.openstreetmap.josm.gui.preferences.validator.ValidatorPreference; 054import org.openstreetmap.josm.gui.preferences.validator.ValidatorTagCheckerRulesPreference; 055import org.openstreetmap.josm.io.CachedFile; 056import org.openstreetmap.josm.io.IllegalDataException; 057import org.openstreetmap.josm.io.UTFInputStreamReader; 058import org.openstreetmap.josm.tools.CheckParameterUtil; 059import org.openstreetmap.josm.tools.MultiMap; 060import org.openstreetmap.josm.tools.Predicate; 061import org.openstreetmap.josm.tools.Utils; 062 063/** 064 * MapCSS-based tag checker/fixer. 065 * @since 6506 066 */ 067public class MapCSSTagChecker extends Test.TagTest { 068 069 /** 070 * A grouped MapCSSRule with multiple selectors for a single declaration. 071 * @see MapCSSRule 072 */ 073 public static class GroupedMapCSSRule { 074 /** MapCSS selectors **/ 075 final public List<Selector> selectors; 076 /** MapCSS declaration **/ 077 final public Declaration declaration; 078 079 /** 080 * Constructs a new {@code GroupedMapCSSRule}. 081 * @param selectors MapCSS selectors 082 * @param declaration MapCSS declaration 083 */ 084 public GroupedMapCSSRule(List<Selector> selectors, Declaration declaration) { 085 this.selectors = selectors; 086 this.declaration = declaration; 087 } 088 089 @Override 090 public int hashCode() { 091 final int prime = 31; 092 int result = 1; 093 result = prime * result + ((declaration == null) ? 0 : declaration.hashCode()); 094 result = prime * result + ((selectors == null) ? 0 : selectors.hashCode()); 095 return result; 096 } 097 098 @Override 099 public boolean equals(Object obj) { 100 if (this == obj) 101 return true; 102 if (obj == null) 103 return false; 104 if (!(obj instanceof GroupedMapCSSRule)) 105 return false; 106 GroupedMapCSSRule other = (GroupedMapCSSRule) obj; 107 if (declaration == null) { 108 if (other.declaration != null) 109 return false; 110 } else if (!declaration.equals(other.declaration)) 111 return false; 112 if (selectors == null) { 113 if (other.selectors != null) 114 return false; 115 } else if (!selectors.equals(other.selectors)) 116 return false; 117 return true; 118 } 119 } 120 121 /** 122 * The preference key for tag checker source entries. 123 * @since 6670 124 */ 125 public static final String ENTRIES_PREF_KEY = "validator." + MapCSSTagChecker.class.getName() + ".entries"; 126 127 /** 128 * Constructs a new {@code MapCSSTagChecker}. 129 */ 130 public MapCSSTagChecker() { 131 super(tr("Tag checker (MapCSS based)"), tr("This test checks for errors in tag keys and values.")); 132 } 133 134 final MultiMap<String, TagCheck> checks = new MultiMap<>(); 135 136 static class TagCheck implements Predicate<OsmPrimitive> { 137 protected final GroupedMapCSSRule rule; 138 protected final List<PrimitiveToTag> change = new ArrayList<>(); 139 protected final Map<String, String> keyChange = new LinkedHashMap<>(); 140 protected final List<String> alternatives = new ArrayList<>(); 141 protected final Map<Instruction.AssignmentInstruction, Severity> errors = new HashMap<>(); 142 protected final Map<String, Boolean> assertions = new HashMap<>(); 143 protected boolean deletion = false; 144 145 TagCheck(GroupedMapCSSRule rule) { 146 this.rule = rule; 147 } 148 149 /** 150 * A function mapping the matched {@link OsmPrimitive} to a {@link Tag}. 151 */ 152 abstract static class PrimitiveToTag implements Utils.Function<OsmPrimitive, Tag> { 153 154 private PrimitiveToTag() { 155 // Hide implicit public constructor for utility class 156 } 157 158 /** 159 * Creates a new mapping from an {@code MapCSS} object. 160 * In case of an {@link Expression}, that is evaluated on the matched {@link OsmPrimitive}. 161 * In case of a {@link String}, that is "compiled" to a {@link Tag} instance. 162 */ 163 static PrimitiveToTag ofMapCSSObject(final Object obj, final boolean keyOnly) { 164 if (obj instanceof Expression) { 165 return new PrimitiveToTag() { 166 @Override 167 public Tag apply(OsmPrimitive p) { 168 final String s = (String) ((Expression) obj).evaluate(new Environment().withPrimitive(p)); 169 return keyOnly? new Tag(s) : Tag.ofString(s); 170 } 171 }; 172 } else if (obj instanceof String) { 173 final Tag tag = keyOnly ? new Tag((String) obj) : Tag.ofString((String) obj); 174 return new PrimitiveToTag() { 175 @Override 176 public Tag apply(OsmPrimitive ignore) { 177 return tag; 178 } 179 }; 180 } else { 181 return null; 182 } 183 } 184 } 185 186 static final String POSSIBLE_THROWS = possibleThrows(); 187 188 static final String possibleThrows() { 189 StringBuffer sb = new StringBuffer(); 190 for (Severity s : Severity.values()) { 191 if (sb.length() > 0) { 192 sb.append('/'); 193 } 194 sb.append("throw") 195 .append(s.name().charAt(0)) 196 .append(s.name().substring(1).toLowerCase()); 197 } 198 return sb.toString(); 199 } 200 201 static TagCheck ofMapCSSRule(final GroupedMapCSSRule rule) throws IllegalDataException { 202 final TagCheck check = new TagCheck(rule); 203 boolean containsSetClassExpression = false; 204 for (Instruction i : rule.declaration.instructions) { 205 if (i instanceof Instruction.AssignmentInstruction) { 206 final Instruction.AssignmentInstruction ai = (Instruction.AssignmentInstruction) i; 207 if (ai.isSetInstruction) { 208 containsSetClassExpression = true; 209 continue; 210 } 211 final String val = ai.val instanceof Expression 212 ? (String) ((Expression) ai.val).evaluate(new Environment()) 213 : ai.val instanceof String 214 ? (String) ai.val 215 : ai.val instanceof Keyword 216 ? ((Keyword) ai.val).val 217 : null; 218 if (ai.key.startsWith("throw")) { 219 try { 220 final Severity severity = Severity.valueOf(ai.key.substring("throw".length()).toUpperCase()); 221 check.errors.put(ai, severity); 222 } catch (IllegalArgumentException e) { 223 Main.warn("Unsupported "+ai.key+" instruction. Allowed instructions are "+POSSIBLE_THROWS); 224 } 225 } else if ("fixAdd".equals(ai.key)) { 226 final PrimitiveToTag toTag = PrimitiveToTag.ofMapCSSObject(ai.val, false); 227 if (toTag != null) { 228 check.change.add(toTag); 229 } else { 230 Main.warn("Invalid value for "+ai.key+": "+ai.val); 231 } 232 } else if ("fixRemove".equals(ai.key)) { 233 CheckParameterUtil.ensureThat(!(ai.val instanceof String) || !(val != null && val.contains("=")), 234 "Unexpected '='. Please only specify the key to remove!"); 235 final PrimitiveToTag toTag = PrimitiveToTag.ofMapCSSObject(ai.val, true); 236 if (toTag != null) { 237 check.change.add(toTag); 238 } else { 239 Main.warn("Invalid value for "+ai.key+": "+ai.val); 240 } 241 } else if ("fixChangeKey".equals(ai.key) && val != null) { 242 CheckParameterUtil.ensureThat(val.contains("=>"), "Separate old from new key by '=>'!"); 243 final String[] x = val.split("=>", 2); 244 check.keyChange.put(Tag.removeWhiteSpaces(x[0]), Tag.removeWhiteSpaces(x[1])); 245 } else if ("fixDeleteObject".equals(ai.key) && val != null) { 246 CheckParameterUtil.ensureThat(val.equals("this"), "fixDeleteObject must be followed by 'this'"); 247 check.deletion = true; 248 } else if ("suggestAlternative".equals(ai.key) && val != null) { 249 check.alternatives.add(val); 250 } else if ("assertMatch".equals(ai.key) && val != null) { 251 check.assertions.put(val, true); 252 } else if ("assertNoMatch".equals(ai.key) && val != null) { 253 check.assertions.put(val, false); 254 } else { 255 throw new IllegalDataException("Cannot add instruction " + ai.key + ": " + ai.val + "!"); 256 } 257 } 258 } 259 if (check.errors.isEmpty() && !containsSetClassExpression) { 260 throw new IllegalDataException("No "+POSSIBLE_THROWS+" given! You should specify a validation error message for " + rule.selectors); 261 } else if (check.errors.size() > 1) { 262 throw new IllegalDataException("More than one "+POSSIBLE_THROWS+" given! You should specify a single validation error message for " + rule.selectors); 263 } 264 return check; 265 } 266 267 static List<TagCheck> readMapCSS(Reader css) throws ParseException { 268 CheckParameterUtil.ensureParameterNotNull(css, "css"); 269 return readMapCSS(new MapCSSParser(css)); 270 } 271 272 static List<TagCheck> readMapCSS(MapCSSParser css) throws ParseException { 273 CheckParameterUtil.ensureParameterNotNull(css, "css"); 274 final MapCSSStyleSource source = new MapCSSStyleSource(""); 275 css.sheet(source); 276 assert source.getErrors().isEmpty(); 277 // Ignore "meta" rule(s) from external rules of JOSM wiki 278 removeMetaRules(source); 279 // group rules with common declaration block 280 Map<Declaration, List<Selector>> g = new LinkedHashMap<>(); 281 for (MapCSSRule rule : source.rules) { 282 if (!g.containsKey(rule.declaration)) { 283 List<Selector> sels = new ArrayList<>(); 284 sels.add(rule.selector); 285 g.put(rule.declaration, sels); 286 } else { 287 g.get(rule.declaration).add(rule.selector); 288 } 289 } 290 List<TagCheck> result = new ArrayList<>(); 291 for (Map.Entry<Declaration, List<Selector>> map : g.entrySet()) { 292 try { 293 result.add(TagCheck.ofMapCSSRule( 294 new GroupedMapCSSRule(map.getValue(), map.getKey()))); 295 } catch (IllegalDataException e) { 296 Main.error("Cannot add MapCss rule: "+e.getMessage()); 297 } 298 } 299 return result; 300 } 301 302 private static void removeMetaRules(MapCSSStyleSource source) { 303 for (Iterator<MapCSSRule> it = source.rules.iterator(); it.hasNext(); ) { 304 MapCSSRule x = it.next(); 305 if (x.selector instanceof GeneralSelector) { 306 GeneralSelector gs = (GeneralSelector) x.selector; 307 if ("meta".equals(gs.base) && gs.getConditions().isEmpty()) { 308 it.remove(); 309 } 310 } 311 } 312 } 313 314 @Override 315 public boolean evaluate(OsmPrimitive primitive) { 316 // Tests whether the primitive contains a deprecated tag which is represented by this MapCSSTagChecker. 317 return whichSelectorMatchesPrimitive(primitive) != null; 318 } 319 320 Selector whichSelectorMatchesPrimitive(OsmPrimitive primitive) { 321 return whichSelectorMatchesEnvironment(new Environment().withPrimitive(primitive)); 322 } 323 324 Selector whichSelectorMatchesEnvironment(Environment env) { 325 for (Selector i : rule.selectors) { 326 env.clearSelectorMatchingInformation(); 327 if (i.matches(env)) { 328 return i; 329 } 330 } 331 return null; 332 } 333 334 /** 335 * Determines the {@code index}-th key/value/tag (depending on {@code type}) of the 336 * {@link org.openstreetmap.josm.gui.mappaint.mapcss.Selector.GeneralSelector}. 337 */ 338 static String determineArgument(Selector.GeneralSelector matchingSelector, int index, String type) { 339 try { 340 final Condition c = matchingSelector.getConditions().get(index); 341 final Tag tag = c instanceof Condition.KeyCondition 342 ? ((Condition.KeyCondition) c).asTag() 343 : c instanceof Condition.SimpleKeyValueCondition 344 ? ((Condition.SimpleKeyValueCondition) c).asTag() 345 : c instanceof Condition.KeyValueCondition 346 ? ((Condition.KeyValueCondition) c).asTag() 347 : null; 348 if (tag == null) { 349 return null; 350 } else if ("key".equals(type)) { 351 return tag.getKey(); 352 } else if ("value".equals(type)) { 353 return tag.getValue(); 354 } else if ("tag".equals(type)) { 355 return tag.toString(); 356 } 357 } catch (IndexOutOfBoundsException ignore) { 358 if (Main.isDebugEnabled()) { 359 Main.debug(ignore.getMessage()); 360 } 361 } 362 return null; 363 } 364 365 /** 366 * Replaces occurrences of <code>{i.key}</code>, <code>{i.value}</code>, <code>{i.tag}</code> in {@code s} by the corresponding 367 * key/value/tag of the {@code index}-th {@link Condition} of {@code matchingSelector}. 368 */ 369 static String insertArguments(Selector matchingSelector, String s) { 370 if (s != null && matchingSelector instanceof Selector.ChildOrParentSelector) { 371 return insertArguments(((Selector.ChildOrParentSelector)matchingSelector).right, s); 372 } else if (s == null || !(matchingSelector instanceof GeneralSelector)) { 373 return s; 374 } 375 final Matcher m = Pattern.compile("\\{(\\d+)\\.(key|value|tag)\\}").matcher(s); 376 final StringBuffer sb = new StringBuffer(); 377 while (m.find()) { 378 final String argument = determineArgument((Selector.GeneralSelector) matchingSelector, Integer.parseInt(m.group(1)), m.group(2)); 379 try { 380 // Perform replacement with null-safe + regex-safe handling 381 m.appendReplacement(sb, String.valueOf(argument).replace("^(", "").replace(")$", "")); 382 } catch (IndexOutOfBoundsException | IllegalArgumentException e) { 383 Main.error(tr("Unable to replace argument {0} in {1}: {2}", argument, sb, e.getMessage())); 384 } 385 } 386 m.appendTail(sb); 387 return sb.toString(); 388 } 389 390 /** 391 * Constructs a fix in terms of a {@link org.openstreetmap.josm.command.Command} for the {@link OsmPrimitive} 392 * if the error is fixable, or {@code null} otherwise. 393 * 394 * @param p the primitive to construct the fix for 395 * @return the fix or {@code null} 396 */ 397 Command fixPrimitive(OsmPrimitive p) { 398 if (change.isEmpty() && keyChange.isEmpty() && !deletion) { 399 return null; 400 } 401 final Selector matchingSelector = whichSelectorMatchesPrimitive(p); 402 Collection<Command> cmds = new LinkedList<>(); 403 for (PrimitiveToTag toTag : change) { 404 final Tag tag = toTag.apply(p); 405 final String key = insertArguments(matchingSelector, tag.getKey()); 406 final String value = insertArguments(matchingSelector, tag.getValue()); 407 cmds.add(new ChangePropertyCommand(p, key, value)); 408 } 409 for (Map.Entry<String, String> i : keyChange.entrySet()) { 410 final String oldKey = insertArguments(matchingSelector, i.getKey()); 411 final String newKey = insertArguments(matchingSelector, i.getValue()); 412 cmds.add(new ChangePropertyKeyCommand(p, oldKey, newKey)); 413 } 414 if (deletion) { 415 cmds.add(new DeleteCommand(p)); 416 } 417 return new SequenceCommand(tr("Fix of {0}", getDescriptionForMatchingSelector(p, matchingSelector)), cmds); 418 } 419 420 /** 421 * Constructs a (localized) message for this deprecation check. 422 * 423 * @return a message 424 */ 425 String getMessage(OsmPrimitive p) { 426 if (errors.isEmpty()) { 427 // Return something to avoid NPEs 428 return rule.declaration.toString(); 429 } else { 430 final Object val = errors.keySet().iterator().next().val; 431 return String.valueOf( 432 val instanceof Expression 433 ? ((Expression) val).evaluate(new Environment().withPrimitive(p)) 434 : val 435 ); 436 } 437 } 438 439 /** 440 * Constructs a (localized) description for this deprecation check. 441 * 442 * @return a description (possibly with alternative suggestions) 443 * @see #getDescriptionForMatchingSelector 444 */ 445 String getDescription(OsmPrimitive p) { 446 if (alternatives.isEmpty()) { 447 return getMessage(p); 448 } else { 449 /* I18N: {0} is the test error message and {1} is an alternative */ 450 return tr("{0}, use {1} instead", getMessage(p), Utils.join(tr(" or "), alternatives)); 451 } 452 } 453 454 /** 455 * Constructs a (localized) description for this deprecation check 456 * where any placeholders are replaced by values of the matched selector. 457 * 458 * @return a description (possibly with alternative suggestions) 459 */ 460 String getDescriptionForMatchingSelector(OsmPrimitive p, Selector matchingSelector) { 461 return insertArguments(matchingSelector, getDescription(p)); 462 } 463 464 Severity getSeverity() { 465 return errors.isEmpty() ? null : errors.values().iterator().next(); 466 } 467 468 @Override 469 public String toString() { 470 return getDescription(null); 471 } 472 473 /** 474 * Constructs a {@link TestError} for the given primitive, or returns null if the primitive does not give rise to an error. 475 * 476 * @param p the primitive to construct the error for 477 * @return an instance of {@link TestError}, or returns null if the primitive does not give rise to an error. 478 */ 479 TestError getErrorForPrimitive(OsmPrimitive p) { 480 final Environment env = new Environment().withPrimitive(p); 481 return getErrorForPrimitive(p, whichSelectorMatchesEnvironment(env), env); 482 } 483 484 TestError getErrorForPrimitive(OsmPrimitive p, Selector matchingSelector, Environment env) { 485 if (matchingSelector != null && !errors.isEmpty()) { 486 final Command fix = fixPrimitive(p); 487 final String description = getDescriptionForMatchingSelector(p, matchingSelector); 488 final List<OsmPrimitive> primitives; 489 if (env.child != null) { 490 primitives = Arrays.asList(p, env.child); 491 } else { 492 primitives = Collections.singletonList(p); 493 } 494 if (fix != null) { 495 return new FixableTestError(null, getSeverity(), description, null, matchingSelector.toString(), 3000, primitives, fix); 496 } else { 497 return new TestError(null, getSeverity(), description, null, matchingSelector.toString(), 3000, primitives); 498 } 499 } else { 500 return null; 501 } 502 } 503 } 504 505 static class MapCSSTagCheckerAndRule extends MapCSSTagChecker { 506 public final GroupedMapCSSRule rule; 507 508 MapCSSTagCheckerAndRule(GroupedMapCSSRule rule) { 509 this.rule = rule; 510 } 511 512 @Override 513 public boolean equals(Object obj) { 514 return super.equals(obj) 515 || (obj instanceof TagCheck && rule.equals(((TagCheck) obj).rule)) 516 || (obj instanceof GroupedMapCSSRule && rule.equals(obj)); 517 } 518 519 @Override 520 public int hashCode() { 521 final int prime = 31; 522 int result = super.hashCode(); 523 result = prime * result + ((rule == null) ? 0 : rule.hashCode()); 524 return result; 525 } 526 } 527 528 /** 529 * Obtains all {@link TestError}s for the {@link OsmPrimitive} {@code p}. 530 * @param p The OSM primitive 531 * @param includeOtherSeverity if {@code true}, errors of severity {@link Severity#OTHER} (info) will also be returned 532 * @return all errors for the given primitive, with or without those of "info" severity 533 */ 534 public synchronized Collection<TestError> getErrorsForPrimitive(OsmPrimitive p, boolean includeOtherSeverity) { 535 final ArrayList<TestError> r = new ArrayList<>(); 536 final Environment env = new Environment(p, new MultiCascade(), Environment.DEFAULT_LAYER, null); 537 for (Set<TagCheck> schecks : checks.values()) { 538 for (TagCheck check : schecks) { 539 if (Severity.OTHER.equals(check.getSeverity()) && !includeOtherSeverity) { 540 continue; 541 } 542 final Selector selector = check.whichSelectorMatchesEnvironment(env); 543 if (selector != null) { 544 check.rule.declaration.execute(env); 545 final TestError error = check.getErrorForPrimitive(p, selector, env); 546 if (error != null) { 547 error.setTester(new MapCSSTagCheckerAndRule(check.rule)); 548 r.add(error); 549 } 550 } 551 } 552 } 553 return r; 554 } 555 556 /** 557 * Visiting call for primitives. 558 * 559 * @param p The primitive to inspect. 560 */ 561 @Override 562 public void check(OsmPrimitive p) { 563 errors.addAll(getErrorsForPrimitive(p, ValidatorPreference.PREF_OTHER.get())); 564 } 565 566 /** 567 * Adds a new MapCSS config file from the given URL. 568 * @param url The unique URL of the MapCSS config file 569 * @throws ParseException if the config file does not match MapCSS syntax 570 * @throws IOException if any I/O error occurs 571 * @since 7275 572 */ 573 public synchronized void addMapCSS(String url) throws ParseException, IOException { 574 CheckParameterUtil.ensureParameterNotNull(url, "url"); 575 CachedFile cache = new CachedFile(url); 576 try (InputStream s = cache.getInputStream()) { 577 List<TagCheck> tagchecks = TagCheck.readMapCSS(new BufferedReader(UTFInputStreamReader.create(s))); 578 checks.remove(url); 579 checks.putAll(url, tagchecks); 580 // Check assertions, useful for development of local files 581 if (Main.pref.getBoolean("validator.check_assert_local_rules", false) && Utils.isLocalUrl(url)) { 582 for (String msg : checkAsserts(tagchecks)) { 583 Main.warn(msg); 584 } 585 } 586 } 587 } 588 589 @Override 590 public synchronized void initialize() throws Exception { 591 checks.clear(); 592 for (SourceEntry source : new ValidatorTagCheckerRulesPreference.RulePrefHelper().get()) { 593 if (!source.active) { 594 continue; 595 } 596 String i = source.url; 597 try { 598 if (!i.startsWith("resource:")) { 599 Main.info(tr("Adding {0} to tag checker", i)); 600 } else if (Main.isDebugEnabled()) { 601 Main.debug(tr("Adding {0} to tag checker", i)); 602 } 603 addMapCSS(i); 604 if (Main.pref.getBoolean("validator.auto_reload_local_rules", true) && source.isLocal()) { 605 try { 606 Main.fileWatcher.registerValidatorRule(source); 607 } catch (IOException e) { 608 Main.error(e); 609 } 610 } 611 } catch (IOException ex) { 612 Main.warn(tr("Failed to add {0} to tag checker", i)); 613 Main.warn(ex, false); 614 } catch (Exception ex) { 615 Main.warn(tr("Failed to add {0} to tag checker", i)); 616 Main.warn(ex); 617 } 618 } 619 } 620 621 /** 622 * Checks that rule assertions are met for the given set of TagChecks. 623 * @param schecks The TagChecks for which assertions have to be checked 624 * @return A set of error messages, empty if all assertions are met 625 * @since 7356 626 */ 627 public Set<String> checkAsserts(final Collection<TagCheck> schecks) { 628 Set<String> assertionErrors = new LinkedHashSet<>(); 629 for (final TagCheck check : schecks) { 630 if (Main.isDebugEnabled()) { 631 Main.debug("Check: "+check); 632 } 633 for (final Map.Entry<String, Boolean> i : check.assertions.entrySet()) { 634 if (Main.isDebugEnabled()) { 635 Main.debug("- Assertion: "+i); 636 } 637 final OsmPrimitive p = OsmUtils.createPrimitive(i.getKey()); 638 final boolean isError = Utils.exists(getErrorsForPrimitive(p, true), new Predicate<TestError>() { 639 @Override 640 public boolean evaluate(TestError e) { 641 //noinspection EqualsBetweenInconvertibleTypes 642 return e.getTester().equals(check.rule); 643 } 644 }); 645 if (isError != i.getValue()) { 646 final String error = MessageFormat.format("Expecting test ''{0}'' (i.e., {1}) to {2} {3} (i.e., {4})", 647 check.getMessage(p), check.rule.selectors, i.getValue() ? "match" : "not match", i.getKey(), p.getKeys()); 648 assertionErrors.add(error); 649 } 650 } 651 } 652 return assertionErrors; 653 } 654 655 @Override 656 public synchronized int hashCode() { 657 final int prime = 31; 658 int result = super.hashCode(); 659 result = prime * result + ((checks == null) ? 0 : checks.hashCode()); 660 return result; 661 } 662 663 @Override 664 public synchronized boolean equals(Object obj) { 665 if (this == obj) 666 return true; 667 if (!super.equals(obj)) 668 return false; 669 if (!(obj instanceof MapCSSTagChecker)) 670 return false; 671 MapCSSTagChecker other = (MapCSSTagChecker) obj; 672 if (checks == null) { 673 if (other.checks != null) 674 return false; 675 } else if (!checks.equals(other.checks)) 676 return false; 677 return true; 678 } 679}