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

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

sonar - fb-contrib - minor performance improvements:

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