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

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

fix #15733 - catch TokenMgrError when parsing invalid validator mapcss files

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