source: josm/trunk/src/org/openstreetmap/josm/data/validation/tests/MapCSSTagChecker.java@ 12410

Last change on this file since 12410 was 12410, checked in by Don-vip, 7 years ago

fix #14945 - InvalidPathException at startup when invalid URL is registered for map paint style

  • Property svn:eol-style set to native
File size: 36.0 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.data.validation.tests;
3
4import static org.openstreetmap.josm.tools.I18n.tr;
5
6import java.io.BufferedReader;
7import java.io.IOException;
8import java.io.InputStream;
9import java.io.Reader;
10import java.io.StringReader;
11import java.text.MessageFormat;
12import java.util.ArrayList;
13import java.util.Arrays;
14import java.util.Collection;
15import java.util.Collections;
16import java.util.HashMap;
17import java.util.HashSet;
18import java.util.Iterator;
19import java.util.LinkedHashMap;
20import java.util.LinkedHashSet;
21import java.util.LinkedList;
22import java.util.List;
23import java.util.Locale;
24import java.util.Map;
25import java.util.Objects;
26import java.util.Set;
27import java.util.function.Predicate;
28import java.util.regex.Matcher;
29import java.util.regex.Pattern;
30
31import org.openstreetmap.josm.Main;
32import org.openstreetmap.josm.command.ChangePropertyCommand;
33import org.openstreetmap.josm.command.ChangePropertyKeyCommand;
34import org.openstreetmap.josm.command.Command;
35import org.openstreetmap.josm.command.DeleteCommand;
36import org.openstreetmap.josm.command.SequenceCommand;
37import org.openstreetmap.josm.data.osm.DataSet;
38import org.openstreetmap.josm.data.osm.OsmPrimitive;
39import org.openstreetmap.josm.data.osm.OsmUtils;
40import org.openstreetmap.josm.data.osm.Tag;
41import org.openstreetmap.josm.data.validation.Severity;
42import org.openstreetmap.josm.data.validation.Test;
43import org.openstreetmap.josm.data.validation.TestError;
44import org.openstreetmap.josm.gui.mappaint.Environment;
45import org.openstreetmap.josm.gui.mappaint.Keyword;
46import org.openstreetmap.josm.gui.mappaint.MultiCascade;
47import org.openstreetmap.josm.gui.mappaint.mapcss.Condition;
48import org.openstreetmap.josm.gui.mappaint.mapcss.ConditionFactory.ClassCondition;
49import org.openstreetmap.josm.gui.mappaint.mapcss.Expression;
50import org.openstreetmap.josm.gui.mappaint.mapcss.Instruction;
51import org.openstreetmap.josm.gui.mappaint.mapcss.MapCSSRule;
52import org.openstreetmap.josm.gui.mappaint.mapcss.MapCSSRule.Declaration;
53import org.openstreetmap.josm.gui.mappaint.mapcss.MapCSSStyleSource;
54import org.openstreetmap.josm.gui.mappaint.mapcss.Selector;
55import org.openstreetmap.josm.gui.mappaint.mapcss.Selector.AbstractSelector;
56import org.openstreetmap.josm.gui.mappaint.mapcss.Selector.GeneralSelector;
57import org.openstreetmap.josm.gui.mappaint.mapcss.parsergen.MapCSSParser;
58import org.openstreetmap.josm.gui.mappaint.mapcss.parsergen.ParseException;
59import org.openstreetmap.josm.gui.preferences.SourceEntry;
60import org.openstreetmap.josm.gui.preferences.validator.ValidatorPreference;
61import org.openstreetmap.josm.gui.preferences.validator.ValidatorTagCheckerRulesPreference;
62import org.openstreetmap.josm.io.CachedFile;
63import org.openstreetmap.josm.io.IllegalDataException;
64import org.openstreetmap.josm.io.UTFInputStreamReader;
65import org.openstreetmap.josm.tools.CheckParameterUtil;
66import org.openstreetmap.josm.tools.MultiMap;
67import org.openstreetmap.josm.tools.Utils;
68
69/**
70 * MapCSS-based tag checker/fixer.
71 * @since 6506
72 */
73public class MapCSSTagChecker extends Test.TagTest {
74
75 /**
76 * A grouped MapCSSRule with multiple selectors for a single declaration.
77 * @see MapCSSRule
78 */
79 public static class GroupedMapCSSRule {
80 /** MapCSS selectors **/
81 public final List<Selector> selectors;
82 /** MapCSS declaration **/
83 public final Declaration declaration;
84
85 /**
86 * Constructs a new {@code GroupedMapCSSRule}.
87 * @param selectors MapCSS selectors
88 * @param declaration MapCSS declaration
89 */
90 public GroupedMapCSSRule(List<Selector> selectors, Declaration declaration) {
91 this.selectors = selectors;
92 this.declaration = declaration;
93 }
94
95 @Override
96 public int hashCode() {
97 return Objects.hash(selectors, declaration);
98 }
99
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 /**
143 * Checks that object is either an {@link Expression} or a {@link String}.
144 * @param obj object to check
145 * @throws IllegalArgumentException if object is not an {@code Expression} or a {@code String}
146 */
147 static void checkObject(final Object obj) {
148 CheckParameterUtil.ensureThat(obj instanceof Expression || obj instanceof String,
149 "instance of Exception or String expected, but got " + obj);
150 }
151
152 /**
153 * Evaluates given object as {@link Expression} or {@link String} on the matched {@link OsmPrimitive} and {@code matchingSelector}.
154 * @param obj object to evaluate ({@link Expression} or {@link String})
155 * @param p OSM primitive
156 * @param matchingSelector matching selector
157 * @return result string
158 */
159 static String evaluateObject(final Object obj, final OsmPrimitive p, final Selector matchingSelector) {
160 final String s;
161 if (obj instanceof Expression) {
162 s = (String) ((Expression) obj).evaluate(new Environment(p));
163 } else if (obj instanceof String) {
164 s = (String) obj;
165 } else {
166 return null;
167 }
168 return TagCheck.insertArguments(matchingSelector, s, p);
169 }
170
171 /**
172 * Creates a fixing command which executes a {@link ChangePropertyCommand} on the specified tag.
173 * @param obj object to evaluate ({@link Expression} or {@link String})
174 * @return created fix command
175 */
176 static FixCommand fixAdd(final Object obj) {
177 checkObject(obj);
178 return new FixCommand() {
179 @Override
180 public Command createCommand(OsmPrimitive p, Selector matchingSelector) {
181 final Tag tag = Tag.ofString(evaluateObject(obj, p, matchingSelector));
182 return new ChangePropertyCommand(p, tag.getKey(), tag.getValue());
183 }
184
185 @Override
186 public String toString() {
187 return "fixAdd: " + obj;
188 }
189 };
190 }
191
192 /**
193 * Creates a fixing command which executes a {@link ChangePropertyCommand} to delete the specified key.
194 * @param obj object to evaluate ({@link Expression} or {@link String})
195 * @return created fix command
196 */
197 static FixCommand fixRemove(final Object obj) {
198 checkObject(obj);
199 return new FixCommand() {
200 @Override
201 public Command createCommand(OsmPrimitive p, Selector matchingSelector) {
202 final String key = evaluateObject(obj, p, matchingSelector);
203 return new ChangePropertyCommand(p, key, "");
204 }
205
206 @Override
207 public String toString() {
208 return "fixRemove: " + obj;
209 }
210 };
211 }
212
213 /**
214 * Creates a fixing command which executes a {@link ChangePropertyKeyCommand} on the specified keys.
215 * @param oldKey old key
216 * @param newKey new key
217 * @return created fix command
218 */
219 static FixCommand fixChangeKey(final String oldKey, final String newKey) {
220 return new FixCommand() {
221 @Override
222 public Command createCommand(OsmPrimitive p, Selector matchingSelector) {
223 return new ChangePropertyKeyCommand(p,
224 TagCheck.insertArguments(matchingSelector, oldKey, p),
225 TagCheck.insertArguments(matchingSelector, newKey, p));
226 }
227
228 @Override
229 public String toString() {
230 return "fixChangeKey: " + oldKey + " => " + newKey;
231 }
232 };
233 }
234 }
235
236 final MultiMap<String, TagCheck> checks = new MultiMap<>();
237
238 /**
239 * Result of {@link TagCheck#readMapCSS}
240 * @since 8936
241 */
242 public static class ParseResult {
243 /** Checks successfully parsed */
244 public final List<TagCheck> parseChecks;
245 /** Errors that occured during parsing */
246 public final Collection<Throwable> parseErrors;
247
248 /**
249 * Constructs a new {@code ParseResult}.
250 * @param parseChecks Checks successfully parsed
251 * @param parseErrors Errors that occured during parsing
252 */
253 public ParseResult(List<TagCheck> parseChecks, Collection<Throwable> parseErrors) {
254 this.parseChecks = parseChecks;
255 this.parseErrors = parseErrors;
256 }
257 }
258
259 /**
260 * Tag check.
261 */
262 public static class TagCheck implements Predicate<OsmPrimitive> {
263 /** The selector of this {@code TagCheck} */
264 protected final GroupedMapCSSRule rule;
265 /** Commands to apply in order to fix a matching primitive */
266 protected final List<FixCommand> fixCommands = new ArrayList<>();
267 /** Tags (or arbitraty strings) of alternatives to be presented to the user */
268 protected final List<String> alternatives = new ArrayList<>();
269 /** An {@link org.openstreetmap.josm.gui.mappaint.mapcss.Instruction.AssignmentInstruction}-{@link Severity} pair.
270 * Is evaluated on the matching primitive to give the error message. Map is checked to contain exactly one element. */
271 protected final Map<Instruction.AssignmentInstruction, Severity> errors = new HashMap<>();
272 /** Unit tests */
273 protected final Map<String, Boolean> assertions = new HashMap<>();
274 /** MapCSS Classes to set on matching primitives */
275 protected final Set<String> setClassExpressions = new HashSet<>();
276 /** Denotes whether the object should be deleted for fixing it */
277 protected boolean deletion;
278 /** A string used to group similar tests */
279 protected String group;
280
281 TagCheck(GroupedMapCSSRule rule) {
282 this.rule = rule;
283 }
284
285 private static final String POSSIBLE_THROWS = possibleThrows();
286
287 static final String possibleThrows() {
288 StringBuilder sb = new StringBuilder();
289 for (Severity s : Severity.values()) {
290 if (sb.length() > 0) {
291 sb.append('/');
292 }
293 sb.append("throw")
294 .append(s.name().charAt(0))
295 .append(s.name().substring(1).toLowerCase(Locale.ENGLISH));
296 }
297 return sb.toString();
298 }
299
300 static TagCheck ofMapCSSRule(final GroupedMapCSSRule rule) throws IllegalDataException {
301 final TagCheck check = new TagCheck(rule);
302 for (Instruction i : rule.declaration.instructions) {
303 if (i instanceof Instruction.AssignmentInstruction) {
304 final Instruction.AssignmentInstruction ai = (Instruction.AssignmentInstruction) i;
305 if (ai.isSetInstruction) {
306 check.setClassExpressions.add(ai.key);
307 continue;
308 }
309 try {
310 final String val = ai.val instanceof Expression
311 ? (String) ((Expression) ai.val).evaluate(new Environment())
312 : ai.val instanceof String
313 ? (String) ai.val
314 : ai.val instanceof Keyword
315 ? ((Keyword) ai.val).val
316 : null;
317 if (ai.key.startsWith("throw")) {
318 try {
319 check.errors.put(ai, Severity.valueOf(ai.key.substring("throw".length()).toUpperCase(Locale.ENGLISH)));
320 } catch (IllegalArgumentException e) {
321 Main.warn(e, "Unsupported "+ai.key+" instruction. Allowed instructions are "+POSSIBLE_THROWS+'.');
322 }
323 } else if ("fixAdd".equals(ai.key)) {
324 check.fixCommands.add(FixCommand.fixAdd(ai.val));
325 } else if ("fixRemove".equals(ai.key)) {
326 CheckParameterUtil.ensureThat(!(ai.val instanceof String) || !(val != null && val.contains("=")),
327 "Unexpected '='. Please only specify the key to remove!");
328 check.fixCommands.add(FixCommand.fixRemove(ai.val));
329 } else if (val != null && "fixChangeKey".equals(ai.key)) {
330 CheckParameterUtil.ensureThat(val.contains("=>"), "Separate old from new key by '=>'!");
331 final String[] x = val.split("=>", 2);
332 check.fixCommands.add(FixCommand.fixChangeKey(Tag.removeWhiteSpaces(x[0]), Tag.removeWhiteSpaces(x[1])));
333 } else if (val != null && "fixDeleteObject".equals(ai.key)) {
334 CheckParameterUtil.ensureThat("this".equals(val), "fixDeleteObject must be followed by 'this'");
335 check.deletion = true;
336 } else if (val != null && "suggestAlternative".equals(ai.key)) {
337 check.alternatives.add(val);
338 } else if (val != null && "assertMatch".equals(ai.key)) {
339 check.assertions.put(val, Boolean.TRUE);
340 } else if (val != null && "assertNoMatch".equals(ai.key)) {
341 check.assertions.put(val, Boolean.FALSE);
342 } else if (val != null && "group".equals(ai.key)) {
343 check.group = val;
344 } else {
345 throw new IllegalDataException("Cannot add instruction " + ai.key + ": " + ai.val + '!');
346 }
347 } catch (IllegalArgumentException e) {
348 throw new IllegalDataException(e);
349 }
350 }
351 }
352 if (check.errors.isEmpty() && check.setClassExpressions.isEmpty()) {
353 throw new IllegalDataException(
354 "No "+POSSIBLE_THROWS+" given! You should specify a validation error message for " + rule.selectors);
355 } else if (check.errors.size() > 1) {
356 throw new IllegalDataException(
357 "More than one "+POSSIBLE_THROWS+" given! You should specify a single validation error message for "
358 + rule.selectors);
359 }
360 return check;
361 }
362
363 static ParseResult readMapCSS(Reader css) throws ParseException {
364 CheckParameterUtil.ensureParameterNotNull(css, "css");
365
366 final MapCSSStyleSource source = new MapCSSStyleSource("");
367 final MapCSSParser preprocessor = new MapCSSParser(css, MapCSSParser.LexicalState.PREPROCESSOR);
368 final StringReader mapcss = new StringReader(preprocessor.pp_root(source));
369 final MapCSSParser parser = new MapCSSParser(mapcss, MapCSSParser.LexicalState.DEFAULT);
370 parser.sheet(source);
371 // Ignore "meta" rule(s) from external rules of JOSM wiki
372 removeMetaRules(source);
373 // group rules with common declaration block
374 Map<Declaration, List<Selector>> g = new LinkedHashMap<>();
375 for (MapCSSRule rule : source.rules) {
376 if (!g.containsKey(rule.declaration)) {
377 List<Selector> sels = new ArrayList<>();
378 sels.add(rule.selector);
379 g.put(rule.declaration, sels);
380 } else {
381 g.get(rule.declaration).add(rule.selector);
382 }
383 }
384 List<TagCheck> parseChecks = new ArrayList<>();
385 for (Map.Entry<Declaration, List<Selector>> map : g.entrySet()) {
386 try {
387 parseChecks.add(TagCheck.ofMapCSSRule(
388 new GroupedMapCSSRule(map.getValue(), map.getKey())));
389 } catch (IllegalDataException e) {
390 Main.error("Cannot add MapCss rule: "+e.getMessage());
391 source.logError(e);
392 }
393 }
394 return new ParseResult(parseChecks, source.getErrors());
395 }
396
397 private static void removeMetaRules(MapCSSStyleSource source) {
398 for (Iterator<MapCSSRule> it = source.rules.iterator(); it.hasNext();) {
399 MapCSSRule x = it.next();
400 if (x.selector instanceof GeneralSelector) {
401 GeneralSelector gs = (GeneralSelector) x.selector;
402 if ("meta".equals(gs.base)) {
403 it.remove();
404 }
405 }
406 }
407 }
408
409 @Override
410 public boolean test(OsmPrimitive primitive) {
411 // Tests whether the primitive contains a deprecated tag which is represented by this MapCSSTagChecker.
412 return whichSelectorMatchesPrimitive(primitive) != null;
413 }
414
415 Selector whichSelectorMatchesPrimitive(OsmPrimitive primitive) {
416 return whichSelectorMatchesEnvironment(new Environment(primitive));
417 }
418
419 Selector whichSelectorMatchesEnvironment(Environment env) {
420 for (Selector i : rule.selectors) {
421 env.clearSelectorMatchingInformation();
422 if (i.matches(env)) {
423 return i;
424 }
425 }
426 return null;
427 }
428
429 /**
430 * Determines the {@code index}-th key/value/tag (depending on {@code type}) of the
431 * {@link org.openstreetmap.josm.gui.mappaint.mapcss.Selector.GeneralSelector}.
432 * @param matchingSelector matching selector
433 * @param index index
434 * @param type selector type ("key", "value" or "tag")
435 * @param p OSM primitive
436 * @return argument value, can be {@code null}
437 */
438 static String determineArgument(Selector.GeneralSelector matchingSelector, int index, String type, OsmPrimitive p) {
439 try {
440 final Condition c = matchingSelector.getConditions().get(index);
441 final Tag tag = c instanceof Condition.ToTagConvertable
442 ? ((Condition.ToTagConvertable) c).asTag(p)
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 Main.debug(ignore);
455 }
456 return null;
457 }
458
459 /**
460 * Replaces occurrences of <code>{i.key}</code>, <code>{i.value}</code>, <code>{i.tag}</code> in {@code s} by the corresponding
461 * key/value/tag of the {@code index}-th {@link Condition} of {@code matchingSelector}.
462 * @param matchingSelector matching selector
463 * @param s any string
464 * @param p OSM primitive
465 * @return string with arguments inserted
466 */
467 static String insertArguments(Selector matchingSelector, String s, OsmPrimitive p) {
468 if (s != null && matchingSelector instanceof Selector.ChildOrParentSelector) {
469 return insertArguments(((Selector.ChildOrParentSelector) matchingSelector).right, s, p);
470 } else if (s == null || !(matchingSelector instanceof GeneralSelector)) {
471 return s;
472 }
473 final Matcher m = Pattern.compile("\\{(\\d+)\\.(key|value|tag)\\}").matcher(s);
474 final StringBuffer sb = new StringBuffer();
475 while (m.find()) {
476 final String argument = determineArgument((Selector.GeneralSelector) matchingSelector,
477 Integer.parseInt(m.group(1)), m.group(2), p);
478 try {
479 // Perform replacement with null-safe + regex-safe handling
480 m.appendReplacement(sb, String.valueOf(argument).replace("^(", "").replace(")$", ""));
481 } catch (IndexOutOfBoundsException | IllegalArgumentException e) {
482 Main.error(e, tr("Unable to replace argument {0} in {1}: {2}", argument, sb, e.getMessage()));
483 }
484 }
485 m.appendTail(sb);
486 return sb.toString();
487 }
488
489 /**
490 * Constructs a fix in terms of a {@link org.openstreetmap.josm.command.Command} for the {@link OsmPrimitive}
491 * if the error is fixable, or {@code null} otherwise.
492 *
493 * @param p the primitive to construct the fix for
494 * @return the fix or {@code null}
495 */
496 Command fixPrimitive(OsmPrimitive p) {
497 if (fixCommands.isEmpty() && !deletion) {
498 return null;
499 }
500 final Selector matchingSelector = whichSelectorMatchesPrimitive(p);
501 Collection<Command> cmds = new LinkedList<>();
502 for (FixCommand fixCommand : fixCommands) {
503 cmds.add(fixCommand.createCommand(p, matchingSelector));
504 }
505 if (deletion && !p.isDeleted()) {
506 cmds.add(new DeleteCommand(p));
507 }
508 return new SequenceCommand(tr("Fix of {0}", getDescriptionForMatchingSelector(p, matchingSelector)), cmds);
509 }
510
511 /**
512 * Constructs a (localized) message for this deprecation check.
513 * @param p OSM primitive
514 *
515 * @return a message
516 */
517 String getMessage(OsmPrimitive p) {
518 if (errors.isEmpty()) {
519 // Return something to avoid NPEs
520 return rule.declaration.toString();
521 } else {
522 final Object val = errors.keySet().iterator().next().val;
523 return String.valueOf(
524 val instanceof Expression
525 ? ((Expression) val).evaluate(new Environment(p))
526 : val
527 );
528 }
529 }
530
531 /**
532 * Constructs a (localized) description for this deprecation check.
533 * @param p OSM primitive
534 *
535 * @return a description (possibly with alternative suggestions)
536 * @see #getDescriptionForMatchingSelector
537 */
538 String getDescription(OsmPrimitive p) {
539 if (alternatives.isEmpty()) {
540 return getMessage(p);
541 } else {
542 /* I18N: {0} is the test error message and {1} is an alternative */
543 return tr("{0}, use {1} instead", getMessage(p), Utils.join(tr(" or "), alternatives));
544 }
545 }
546
547 /**
548 * Constructs a (localized) description for this deprecation check
549 * where any placeholders are replaced by values of the matched selector.
550 *
551 * @param matchingSelector matching selector
552 * @param p OSM primitive
553 * @return a description (possibly with alternative suggestions)
554 */
555 String getDescriptionForMatchingSelector(OsmPrimitive p, Selector matchingSelector) {
556 return insertArguments(matchingSelector, getDescription(p), p);
557 }
558
559 Severity getSeverity() {
560 return errors.isEmpty() ? null : errors.values().iterator().next();
561 }
562
563 @Override
564 public String toString() {
565 return getDescription(null);
566 }
567
568 /**
569 * Constructs a {@link TestError} for the given primitive, or returns null if the primitive does not give rise to an error.
570 *
571 * @param p the primitive to construct the error for
572 * @return an instance of {@link TestError}, or returns null if the primitive does not give rise to an error.
573 */
574 TestError getErrorForPrimitive(OsmPrimitive p) {
575 final Environment env = new Environment(p);
576 return getErrorForPrimitive(p, whichSelectorMatchesEnvironment(env), env, null);
577 }
578
579 TestError getErrorForPrimitive(OsmPrimitive p, Selector matchingSelector, Environment env, Test tester) {
580 if (matchingSelector != null && !errors.isEmpty()) {
581 final Command fix = fixPrimitive(p);
582 final String description = getDescriptionForMatchingSelector(p, matchingSelector);
583 final String description1 = group == null ? description : group;
584 final String description2 = group == null ? null : description;
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 final TestError.Builder error = TestError.builder(tester, getSeverity(), 3000)
592 .messageWithManuallyTranslatedDescription(description1, description2, matchingSelector.toString())
593 .primitives(primitives);
594 if (fix != null) {
595 return error.fix(() -> fix).build();
596 } else {
597 return error.build();
598 }
599 } else {
600 return null;
601 }
602 }
603
604 /**
605 * Returns the set of tagchecks on which this check depends on.
606 * @param schecks the collection of tagcheks to search in
607 * @return the set of tagchecks on which this check depends on
608 * @since 7881
609 */
610 public Set<TagCheck> getTagCheckDependencies(Collection<TagCheck> schecks) {
611 Set<TagCheck> result = new HashSet<>();
612 Set<String> classes = getClassesIds();
613 if (schecks != null && !classes.isEmpty()) {
614 for (TagCheck tc : schecks) {
615 if (this.equals(tc)) {
616 continue;
617 }
618 for (String id : tc.setClassExpressions) {
619 if (classes.contains(id)) {
620 result.add(tc);
621 break;
622 }
623 }
624 }
625 }
626 return result;
627 }
628
629 /**
630 * Returns the list of ids of all MapCSS classes referenced in the rule selectors.
631 * @return the list of ids of all MapCSS classes referenced in the rule selectors
632 * @since 7881
633 */
634 public Set<String> getClassesIds() {
635 Set<String> result = new HashSet<>();
636 for (Selector s : rule.selectors) {
637 if (s instanceof AbstractSelector) {
638 for (Condition c : ((AbstractSelector) s).getConditions()) {
639 if (c instanceof ClassCondition) {
640 result.add(((ClassCondition) c).id);
641 }
642 }
643 }
644 }
645 return result;
646 }
647 }
648
649 static class MapCSSTagCheckerAndRule extends MapCSSTagChecker {
650 public final GroupedMapCSSRule rule;
651
652 MapCSSTagCheckerAndRule(GroupedMapCSSRule rule) {
653 this.rule = rule;
654 }
655
656 @Override
657 public synchronized boolean equals(Object obj) {
658 return super.equals(obj)
659 || (obj instanceof TagCheck && rule.equals(((TagCheck) obj).rule))
660 || (obj instanceof GroupedMapCSSRule && rule.equals(obj));
661 }
662
663 @Override
664 public synchronized int hashCode() {
665 return Objects.hash(super.hashCode(), rule);
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, new MapCSSTagCheckerAndRule(check.rule));
697 if (error != null) {
698 r.add(error);
699 }
700 }
701 }
702 }
703 return r;
704 }
705
706 /**
707 * Visiting call for primitives.
708 *
709 * @param p The primitive to inspect.
710 */
711 @Override
712 public void check(OsmPrimitive p) {
713 errors.addAll(getErrorsForPrimitive(p, ValidatorPreference.PREF_OTHER.get()));
714 }
715
716 /**
717 * Adds a new MapCSS config file from the given URL.
718 * @param url The unique URL of the MapCSS config file
719 * @return List of tag checks and parsing errors, or null
720 * @throws ParseException if the config file does not match MapCSS syntax
721 * @throws IOException if any I/O error occurs
722 * @since 7275
723 */
724 public synchronized ParseResult addMapCSS(String url) throws ParseException, IOException {
725 CheckParameterUtil.ensureParameterNotNull(url, "url");
726 ParseResult result;
727 try (CachedFile cache = new CachedFile(url);
728 InputStream zip = cache.findZipEntryInputStream("validator.mapcss", "");
729 InputStream s = zip != null ? zip : cache.getInputStream();
730 Reader reader = new BufferedReader(UTFInputStreamReader.create(s))) {
731 result = TagCheck.readMapCSS(reader);
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 Main.fileWatcher.registerValidatorRule(source);
761 }
762 } catch (IOException | IllegalStateException | IllegalArgumentException ex) {
763 Main.warn(tr("Failed to add {0} to tag checker", i));
764 Main.warn(ex, false);
765 } catch (ParseException ex) {
766 Main.warn(tr("Failed to add {0} to tag checker", i));
767 Main.warn(ex);
768 }
769 }
770 }
771
772 /**
773 * Checks that rule assertions are met for the given set of TagChecks.
774 * @param schecks The TagChecks for which assertions have to be checked
775 * @return A set of error messages, empty if all assertions are met
776 * @since 7356
777 */
778 public Set<String> checkAsserts(final Collection<TagCheck> schecks) {
779 Set<String> assertionErrors = new LinkedHashSet<>();
780 final DataSet ds = new DataSet();
781 for (final TagCheck check : schecks) {
782 if (Main.isDebugEnabled()) {
783 Main.debug("Check: "+check);
784 }
785 for (final Map.Entry<String, Boolean> i : check.assertions.entrySet()) {
786 if (Main.isDebugEnabled()) {
787 Main.debug("- Assertion: "+i);
788 }
789 final OsmPrimitive p = OsmUtils.createPrimitive(i.getKey());
790 // Build minimal ordered list of checks to run to test the assertion
791 List<Set<TagCheck>> checksToRun = new ArrayList<>();
792 Set<TagCheck> checkDependencies = check.getTagCheckDependencies(schecks);
793 if (!checkDependencies.isEmpty()) {
794 checksToRun.add(checkDependencies);
795 }
796 checksToRun.add(Collections.singleton(check));
797 // Add primitive to dataset to avoid DataIntegrityProblemException when evaluating selectors
798 ds.addPrimitive(p);
799 final Collection<TestError> pErrors = getErrorsForPrimitive(p, true, checksToRun);
800 if (Main.isDebugEnabled()) {
801 Main.debug("- Errors: "+pErrors);
802 }
803 @SuppressWarnings({"EqualsBetweenInconvertibleTypes", "EqualsIncompatibleType"})
804 final boolean isError = pErrors.stream().anyMatch(e -> e.getTester().equals(check.rule));
805 if (isError != i.getValue()) {
806 final String error = MessageFormat.format("Expecting test ''{0}'' (i.e., {1}) to {2} {3} (i.e., {4})",
807 check.getMessage(p), check.rule.selectors, i.getValue() ? "match" : "not match", i.getKey(), p.getKeys());
808 assertionErrors.add(error);
809 }
810 ds.removePrimitive(p);
811 }
812 }
813 return assertionErrors;
814 }
815
816 @Override
817 public synchronized int hashCode() {
818 return Objects.hash(super.hashCode(), checks);
819 }
820
821 @Override
822 public synchronized boolean equals(Object obj) {
823 if (this == obj) return true;
824 if (obj == null || getClass() != obj.getClass()) return false;
825 if (!super.equals(obj)) return false;
826 MapCSSTagChecker that = (MapCSSTagChecker) obj;
827 return Objects.equals(checks, that.checks);
828 }
829}
Note: See TracBrowser for help on using the repository browser.