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