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