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