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

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

see #15229 - see #15182 - make FileWatcher generic so it has no more dependence on MapCSS

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