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

Last change on this file since 6534 was 6534, checked in by simon04, 10 years ago

fix #9470 see #9414 - make "layer tag with + sign" auto fixable

File size: 12.3 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.InputStreamReader;
7import java.io.Reader;
8import java.io.UnsupportedEncodingException;
9import java.util.ArrayList;
10import java.util.Collection;
11import java.util.HashMap;
12import java.util.LinkedHashMap;
13import java.util.LinkedList;
14import java.util.List;
15import java.util.Map;
16
17import org.openstreetmap.josm.command.ChangePropertyCommand;
18import org.openstreetmap.josm.command.ChangePropertyKeyCommand;
19import org.openstreetmap.josm.command.Command;
20import org.openstreetmap.josm.command.SequenceCommand;
21import org.openstreetmap.josm.data.osm.Node;
22import org.openstreetmap.josm.data.osm.OsmPrimitive;
23import org.openstreetmap.josm.data.osm.Relation;
24import org.openstreetmap.josm.data.osm.Tag;
25import org.openstreetmap.josm.data.osm.Way;
26import org.openstreetmap.josm.data.validation.FixableTestError;
27import org.openstreetmap.josm.data.validation.Severity;
28import org.openstreetmap.josm.data.validation.Test;
29import org.openstreetmap.josm.data.validation.TestError;
30import org.openstreetmap.josm.gui.mappaint.Environment;
31import org.openstreetmap.josm.gui.mappaint.mapcss.Expression;
32import org.openstreetmap.josm.gui.mappaint.mapcss.Instruction;
33import org.openstreetmap.josm.gui.mappaint.mapcss.MapCSSRule;
34import org.openstreetmap.josm.gui.mappaint.mapcss.MapCSSStyleSource;
35import org.openstreetmap.josm.gui.mappaint.mapcss.Selector;
36import org.openstreetmap.josm.gui.mappaint.mapcss.parsergen.MapCSSParser;
37import org.openstreetmap.josm.gui.mappaint.mapcss.parsergen.ParseException;
38import org.openstreetmap.josm.tools.CheckParameterUtil;
39import org.openstreetmap.josm.tools.Predicate;
40import org.openstreetmap.josm.tools.Utils;
41
42/**
43 * MapCSS-based tag checker/fixer.
44 * @since 6506
45 */
46public class MapCSSTagChecker extends Test {
47
48 /**
49 * Constructs a new {@code MapCSSTagChecker}.
50 */
51 public MapCSSTagChecker() {
52 super(tr("Tag checker (new)"), tr("This test checks for errors in tag keys and values."));
53 }
54
55 final List<TagCheck> checks = new ArrayList<TagCheck>();
56
57 static class TagCheck implements Predicate<OsmPrimitive> {
58 protected final List<Selector> selector;
59 protected final List<PrimitiveToTag> change = new ArrayList<PrimitiveToTag>();
60 protected final Map<String, String> keyChange = new LinkedHashMap<String, String>();
61 protected final List<Tag> alternatives = new ArrayList<Tag>();
62 protected final Map<String, Severity> errors = new HashMap<String, Severity>();
63 protected final Map<String, Boolean> assertions = new HashMap<String, Boolean>();
64
65 TagCheck(List<Selector> selector) {
66 this.selector = selector;
67 }
68
69 /**
70 * A function mapping the matched {@link OsmPrimitive} to a {@link Tag}.
71 */
72 static abstract class PrimitiveToTag implements Utils.Function<OsmPrimitive, Tag> {
73
74 /**
75 * Creates a new mapping from an {@code MapCSS} object.
76 * In case of an {@link Expression}, that is evaluated on the matched {@link OsmPrimitive}.
77 * In case of a {@link String}, that is "compiled" to a {@link Tag} instance.
78 */
79 static PrimitiveToTag ofMapCSSObject(final Object obj, final boolean keyOnly) {
80 if (obj instanceof Expression) {
81 return new PrimitiveToTag() {
82 @Override
83 public Tag apply(OsmPrimitive p) {
84 final String s = (String) ((Expression) obj).evaluate(new Environment().withPrimitive(p));
85 return keyOnly? new Tag(s) : Tag.ofString(s);
86 }
87 };
88 } else if (obj instanceof String) {
89 final Tag tag = keyOnly ? new Tag((String) obj) : Tag.ofString((String) obj);
90 return new PrimitiveToTag() {
91 @Override
92 public Tag apply(OsmPrimitive ignore) {
93 return tag;
94 }
95 };
96 } else {
97 return null;
98 }
99 }
100 }
101
102 static TagCheck ofMapCSSRule(final MapCSSRule rule) {
103 final TagCheck check = new TagCheck(rule.selectors);
104 for (Instruction i : rule.declaration) {
105 if (i instanceof Instruction.AssignmentInstruction) {
106 final Instruction.AssignmentInstruction ai = (Instruction.AssignmentInstruction) i;
107 final String val = ai.val instanceof Expression
108 ? (String) ((Expression) ai.val).evaluate(new Environment())
109 : ai.val instanceof String
110 ? (String) ai.val
111 : null;
112 if (ai.key.startsWith("throw")) {
113 final Severity severity = Severity.valueOf(ai.key.substring("throw".length()).toUpperCase());
114 check.errors.put(val, severity);
115 } else if ("fixAdd".equals(ai.key)) {
116 final PrimitiveToTag toTag = PrimitiveToTag.ofMapCSSObject(ai.val, false);
117 check.change.add(toTag);
118 } else if ("fixRemove".equals(ai.key)) {
119 CheckParameterUtil.ensureThat(!(ai.val instanceof String) || !val.contains("="), "Unexpected '='. Please only specify the key to remove!");
120 final PrimitiveToTag toTag = PrimitiveToTag.ofMapCSSObject(ai.val, true);
121 check.change.add(toTag);
122 } else if ("fixChangeKey".equals(ai.key) && val != null) {
123 CheckParameterUtil.ensureThat(val.contains("=>"), "Separate old from new key by '=>'!");
124 final String[] x = val.split("=>", 2);
125 check.keyChange.put(x[0].trim(), x[1].trim());
126 } else if ("suggestAlternative".equals(ai.key) && val != null) {
127 check.alternatives.add(val.contains("=") ? Tag.ofString(val) : new Tag(val));
128 } else if ("assertMatch".equals(ai.key) && val != null) {
129 check.assertions.put(val, true);
130 } else if ("assertNoMatch".equals(ai.key) && val != null) {
131 check.assertions.put(val, false);
132 } else {
133 throw new RuntimeException("Cannot add instruction " + ai.key + ": " + ai.val + "!");
134 }
135 }
136 }
137 if (check.errors.isEmpty()) {
138 throw new RuntimeException("No throwError/throwWarning/throwOther given! You should specify a validation error message for " + rule.selectors);
139 } else if (check.errors.size() > 1) {
140 throw new RuntimeException("More than one throwError/throwWarning/throwOther given! You should specify a single validation error message for " + rule.selectors);
141 }
142 return check;
143 }
144
145 static List<TagCheck> readMapCSS(Reader css) throws ParseException {
146 CheckParameterUtil.ensureParameterNotNull(css, "css");
147 return readMapCSS(new MapCSSParser(css));
148 }
149
150 static List<TagCheck> readMapCSS(MapCSSParser css) throws ParseException {
151 CheckParameterUtil.ensureParameterNotNull(css, "css");
152 final MapCSSStyleSource source = new MapCSSStyleSource("");
153 css.sheet(source);
154 assert source.getErrors().isEmpty();
155 return new ArrayList<TagCheck>(Utils.transform(source.rules, new Utils.Function<MapCSSRule, TagCheck>() {
156 @Override
157 public TagCheck apply(MapCSSRule x) {
158 return TagCheck.ofMapCSSRule(x);
159 }
160 }));
161 }
162
163 @Override
164 public boolean evaluate(OsmPrimitive primitive) {
165 return matchesPrimitive(primitive);
166 }
167
168 /**
169 * Tests whether the {@link OsmPrimitive} contains a deprecated tag which is represented by this {@code MapCSSTagChecker}.
170 *
171 * @param primitive the primitive to test
172 * @return true when the primitive contains a deprecated tag
173 */
174 boolean matchesPrimitive(OsmPrimitive primitive) {
175 final Environment env = new Environment().withPrimitive(primitive);
176 for (Selector i : selector) {
177 if (i.matches(env)) {
178 return true;
179 }
180 }
181 return false;
182 }
183
184 /**
185 * Constructs a fix in terms of a {@link org.openstreetmap.josm.command.Command} for the {@link OsmPrimitive}
186 * if the error is fixable, or {@code null} otherwise.
187 *
188 * @param p the primitive to construct the fix for
189 * @return the fix or {@code null}
190 */
191 Command fixPrimitive(OsmPrimitive p) {
192 if (change.isEmpty() && keyChange.isEmpty()) {
193 return null;
194 }
195 Collection<Command> cmds = new LinkedList<Command>();
196 for (PrimitiveToTag toTag : change) {
197 final Tag tag = toTag.apply(p);
198 cmds.add(new ChangePropertyCommand(p, tag.getKey(), tag.getValue()));
199 }
200 for (Map.Entry<String, String> i : keyChange.entrySet()) {
201 cmds.add(new ChangePropertyKeyCommand(p, i.getKey(), i.getValue()));
202 }
203 return new SequenceCommand(tr("Fix of {0}", getDescription()), cmds);
204 }
205
206 /**
207 * Constructs a (localized) message for this deprecation check.
208 *
209 * @return a message
210 */
211 String getMessage() {
212 return errors.keySet().iterator().next();
213 }
214
215 /**
216 * Constructs a (localized) description for this deprecation check.
217 *
218 * @return a description (possibly with alternative suggestions)
219 */
220 String getDescription() {
221 if (alternatives.isEmpty()) {
222 return getMessage();
223 } else {
224 /* I18N: {0} is the test error message and {1} is an alternative */
225 return tr("{0}, use {1} instead", getMessage(), Utils.join(tr(" or "), alternatives));
226 }
227 }
228
229 Severity getSeverity() {
230 return errors.values().iterator().next();
231 }
232
233 }
234
235 /**
236 * Visiting call for primitives.
237 *
238 * @param p The primitive to inspect.
239 */
240 public void visit(OsmPrimitive p) {
241 for (TagCheck check : checks) {
242 if (check.matchesPrimitive(p)) {
243 final Command fix = check.fixPrimitive(p);
244 if (fix != null) {
245 errors.add(new FixableTestError(this, check.getSeverity(), check.getDescription(), 3000, p, fix));
246 } else {
247 errors.add(new TestError(this, check.getSeverity(), check.getDescription(), 3000, p));
248 }
249 }
250 }
251 }
252
253 @Override
254 public void visit(Node n) {
255 visit((OsmPrimitive) n);
256 }
257
258 @Override
259 public void visit(Way w) {
260 visit((OsmPrimitive) w);
261 }
262
263 @Override
264 public void visit(Relation r) {
265 visit((OsmPrimitive) r);
266 }
267
268 /**
269 * Adds a new MapCSS config file from the given {@code Reader}.
270 * @param css The reader
271 * @throws ParseException if the config file does not match MapCSS syntax
272 */
273 public void addMapCSS(Reader css) throws ParseException {
274 checks.addAll(TagCheck.readMapCSS(css));
275 }
276
277 /**
278 * Adds a new MapCSS config file from the given internal filename.
279 * @param internalConfigFile the filename in data/validator
280 * @throws ParseException if the config file does not match MapCSS syntax
281 * @throws UnsupportedEncodingException if UTF-8 charset is not supported on the platform
282 */
283 private void addMapCSS(String internalConfigFile) throws ParseException, UnsupportedEncodingException {
284 addMapCSS(new InputStreamReader(getClass().getResourceAsStream("/data/validator/"+internalConfigFile), "UTF-8"));
285 }
286
287 @Override
288 public void initialize() throws Exception {
289 addMapCSS("deprecated.mapcss");
290 addMapCSS("highway.mapcss");
291 addMapCSS("numeric.mapcss");
292 }
293}
Note: See TracBrowser for help on using the repository browser.