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