source: josm/trunk/src/org/openstreetmap/josm/data/validation/tests/TagChecker.java@ 10632

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

sonar - squid:RedundantThrowsDeclarationCheck - Throws declarations should not be superfluous

  • Property svn:eol-style set to native
File size: 37.5 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.marktr;
5import static org.openstreetmap.josm.tools.I18n.tr;
6
7import java.awt.GridBagConstraints;
8import java.awt.event.ActionListener;
9import java.io.BufferedReader;
10import java.io.IOException;
11import java.io.InputStream;
12import java.text.MessageFormat;
13import java.util.ArrayList;
14import java.util.Arrays;
15import java.util.Collection;
16import java.util.HashMap;
17import java.util.HashSet;
18import java.util.List;
19import java.util.Locale;
20import java.util.Map;
21import java.util.Map.Entry;
22import java.util.Set;
23import java.util.regex.Matcher;
24import java.util.regex.Pattern;
25import java.util.regex.PatternSyntaxException;
26
27import javax.swing.JCheckBox;
28import javax.swing.JLabel;
29import javax.swing.JPanel;
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.SequenceCommand;
36import org.openstreetmap.josm.data.osm.OsmPrimitive;
37import org.openstreetmap.josm.data.osm.OsmPrimitiveType;
38import org.openstreetmap.josm.data.osm.OsmUtils;
39import org.openstreetmap.josm.data.osm.Tag;
40import org.openstreetmap.josm.data.validation.FixableTestError;
41import org.openstreetmap.josm.data.validation.Severity;
42import org.openstreetmap.josm.data.validation.Test.TagTest;
43import org.openstreetmap.josm.data.validation.TestError;
44import org.openstreetmap.josm.data.validation.util.Entities;
45import org.openstreetmap.josm.gui.preferences.validator.ValidatorPreference;
46import org.openstreetmap.josm.gui.progress.ProgressMonitor;
47import org.openstreetmap.josm.gui.tagging.presets.TaggingPreset;
48import org.openstreetmap.josm.gui.tagging.presets.TaggingPresetItem;
49import org.openstreetmap.josm.gui.tagging.presets.TaggingPresets;
50import org.openstreetmap.josm.gui.tagging.presets.items.Check;
51import org.openstreetmap.josm.gui.tagging.presets.items.CheckGroup;
52import org.openstreetmap.josm.gui.tagging.presets.items.KeyedItem;
53import org.openstreetmap.josm.gui.widgets.EditableList;
54import org.openstreetmap.josm.io.CachedFile;
55import org.openstreetmap.josm.io.UTFInputStreamReader;
56import org.openstreetmap.josm.tools.GBC;
57import org.openstreetmap.josm.tools.MultiMap;
58import org.openstreetmap.josm.tools.Utils;
59
60/**
61 * Check for misspelled or wrong tags
62 *
63 * @author frsantos
64 * @since 3669
65 */
66public class TagChecker extends TagTest {
67
68 /** The config file of ignored tags */
69 public static final String IGNORE_FILE = "resource://data/validator/ignoretags.cfg";
70 /** The config file of dictionary words */
71 public static final String SPELL_FILE = "resource://data/validator/words.cfg";
72
73 /** Normalized keys: the key should be substituted by the value if the key was not found in presets */
74 private static final Map<String, String> harmonizedKeys = new HashMap<>();
75 /** The spell check preset values */
76 private static volatile MultiMap<String, String> presetsValueData;
77 /** The TagChecker data */
78 private static final List<CheckerData> checkerData = new ArrayList<>();
79 private static final List<String> ignoreDataStartsWith = new ArrayList<>();
80 private static final List<String> ignoreDataEquals = new ArrayList<>();
81 private static final List<String> ignoreDataEndsWith = new ArrayList<>();
82 private static final List<Tag> ignoreDataTag = new ArrayList<>();
83
84 /** The preferences prefix */
85 protected static final String PREFIX = ValidatorPreference.PREFIX + "." + TagChecker.class.getSimpleName();
86
87 public static final String PREF_CHECK_VALUES = PREFIX + ".checkValues";
88 public static final String PREF_CHECK_KEYS = PREFIX + ".checkKeys";
89 public static final String PREF_CHECK_COMPLEX = PREFIX + ".checkComplex";
90 public static final String PREF_CHECK_FIXMES = PREFIX + ".checkFixmes";
91
92 public static final String PREF_SOURCES = PREFIX + ".source";
93
94 public static final String PREF_CHECK_KEYS_BEFORE_UPLOAD = PREF_CHECK_KEYS + "BeforeUpload";
95 public static final String PREF_CHECK_VALUES_BEFORE_UPLOAD = PREF_CHECK_VALUES + "BeforeUpload";
96 public static final String PREF_CHECK_COMPLEX_BEFORE_UPLOAD = PREF_CHECK_COMPLEX + "BeforeUpload";
97 public static final String PREF_CHECK_FIXMES_BEFORE_UPLOAD = PREF_CHECK_FIXMES + "BeforeUpload";
98
99 protected boolean checkKeys;
100 protected boolean checkValues;
101 protected boolean checkComplex;
102 protected boolean checkFixmes;
103
104 protected JCheckBox prefCheckKeys;
105 protected JCheckBox prefCheckValues;
106 protected JCheckBox prefCheckComplex;
107 protected JCheckBox prefCheckFixmes;
108 protected JCheckBox prefCheckPaint;
109
110 protected JCheckBox prefCheckKeysBeforeUpload;
111 protected JCheckBox prefCheckValuesBeforeUpload;
112 protected JCheckBox prefCheckComplexBeforeUpload;
113 protected JCheckBox prefCheckFixmesBeforeUpload;
114 protected JCheckBox prefCheckPaintBeforeUpload;
115
116 // CHECKSTYLE.OFF: SingleSpaceSeparator
117 protected static final int EMPTY_VALUES = 1200;
118 protected static final int INVALID_KEY = 1201;
119 protected static final int INVALID_VALUE = 1202;
120 protected static final int FIXME = 1203;
121 protected static final int INVALID_SPACE = 1204;
122 protected static final int INVALID_KEY_SPACE = 1205;
123 protected static final int INVALID_HTML = 1206; /* 1207 was PAINT */
124 protected static final int LONG_VALUE = 1208;
125 protected static final int LONG_KEY = 1209;
126 protected static final int LOW_CHAR_VALUE = 1210;
127 protected static final int LOW_CHAR_KEY = 1211;
128 protected static final int MISSPELLED_VALUE = 1212;
129 protected static final int MISSPELLED_KEY = 1213;
130 protected static final int MULTIPLE_SPACES = 1214;
131 // CHECKSTYLE.ON: SingleSpaceSeparator
132 // 1250 and up is used by tagcheck
133
134 protected EditableList sourcesList;
135
136 private static final Set<String> DEFAULT_SOURCES = new HashSet<>(Arrays.asList(/*DATA_FILE, */IGNORE_FILE, SPELL_FILE));
137
138 /**
139 * Constructor
140 */
141 public TagChecker() {
142 super(tr("Tag checker"), tr("This test checks for errors in tag keys and values."));
143 }
144
145 @Override
146 public void initialize() throws IOException {
147 initializeData();
148 initializePresets();
149 }
150
151 /**
152 * Reads the spellcheck file into a HashMap.
153 * The data file is a list of words, beginning with +/-. If it starts with +,
154 * the word is valid, but if it starts with -, the word should be replaced
155 * by the nearest + word before this.
156 *
157 * @throws IOException if any I/O error occurs
158 */
159 private static void initializeData() throws IOException {
160 checkerData.clear();
161 ignoreDataStartsWith.clear();
162 ignoreDataEquals.clear();
163 ignoreDataEndsWith.clear();
164 ignoreDataTag.clear();
165 harmonizedKeys.clear();
166
167 StringBuilder errorSources = new StringBuilder();
168 for (String source : Main.pref.getCollection(PREF_SOURCES, DEFAULT_SOURCES)) {
169 try (
170 CachedFile cf = new CachedFile(source);
171 InputStream s = cf.getInputStream();
172 BufferedReader reader = new BufferedReader(UTFInputStreamReader.create(s));
173 ) {
174 String okValue = null;
175 boolean tagcheckerfile = false;
176 boolean ignorefile = false;
177 boolean isFirstLine = true;
178 String line;
179 while ((line = reader.readLine()) != null && (tagcheckerfile || !line.isEmpty())) {
180 if (line.startsWith("#")) {
181 if (line.startsWith("# JOSM TagChecker")) {
182 tagcheckerfile = true;
183 if (!DEFAULT_SOURCES.contains(source)) {
184 Main.info(tr("Adding {0} to tag checker", source));
185 }
186 } else
187 if (line.startsWith("# JOSM IgnoreTags")) {
188 ignorefile = true;
189 if (!DEFAULT_SOURCES.contains(source)) {
190 Main.info(tr("Adding {0} to ignore tags", source));
191 }
192 }
193 } else if (ignorefile) {
194 line = line.trim();
195 if (line.length() < 4) {
196 continue;
197 }
198
199 String key = line.substring(0, 2);
200 line = line.substring(2);
201
202 switch (key) {
203 case "S:":
204 ignoreDataStartsWith.add(line);
205 break;
206 case "E:":
207 ignoreDataEquals.add(line);
208 break;
209 case "F:":
210 ignoreDataEndsWith.add(line);
211 break;
212 case "K:":
213 ignoreDataTag.add(Tag.ofString(line));
214 break;
215 default:
216 if (!key.startsWith(";")) {
217 Main.warn("Unsupported TagChecker key: " + key);
218 }
219 }
220 } else if (tagcheckerfile) {
221 if (!line.isEmpty()) {
222 CheckerData d = new CheckerData();
223 String err = d.getData(line);
224
225 if (err == null) {
226 checkerData.add(d);
227 } else {
228 Main.error(tr("Invalid tagchecker line - {0}: {1}", err, line));
229 }
230 }
231 } else if (line.charAt(0) == '+') {
232 okValue = line.substring(1);
233 } else if (line.charAt(0) == '-' && okValue != null) {
234 harmonizedKeys.put(harmonizeKey(line.substring(1)), okValue);
235 } else {
236 Main.error(tr("Invalid spellcheck line: {0}", line));
237 }
238 if (isFirstLine) {
239 isFirstLine = false;
240 if (!(tagcheckerfile || ignorefile) && !DEFAULT_SOURCES.contains(source)) {
241 Main.info(tr("Adding {0} to spellchecker", source));
242 }
243 }
244 }
245 } catch (IOException e) {
246 Main.error(e);
247 errorSources.append(source).append('\n');
248 }
249 }
250
251 if (errorSources.length() > 0)
252 throw new IOException(tr("Could not access data file(s):\n{0}", errorSources));
253 }
254
255 /**
256 * Reads the presets data.
257 *
258 */
259 public static void initializePresets() {
260
261 if (!Main.pref.getBoolean(PREF_CHECK_VALUES, true))
262 return;
263
264 Collection<TaggingPreset> presets = TaggingPresets.getTaggingPresets();
265 if (!presets.isEmpty()) {
266 presetsValueData = new MultiMap<>();
267 for (String a : OsmPrimitive.getUninterestingKeys()) {
268 presetsValueData.putVoid(a);
269 }
270 // TODO directionKeys are no longer in OsmPrimitive (search pattern is used instead)
271 for (String a : Main.pref.getCollection(ValidatorPreference.PREFIX + ".knownkeys",
272 Arrays.asList(new String[]{"is_in", "int_ref", "fixme", "population"}))) {
273 presetsValueData.putVoid(a);
274 }
275 for (TaggingPreset p : presets) {
276 for (TaggingPresetItem i : p.data) {
277 if (i instanceof KeyedItem) {
278 addPresetValue(p, (KeyedItem) i);
279 } else if (i instanceof CheckGroup) {
280 for (Check c : ((CheckGroup) i).checks) {
281 addPresetValue(p, c);
282 }
283 }
284 }
285 }
286 }
287 }
288
289 private static void addPresetValue(TaggingPreset p, KeyedItem ky) {
290 Collection<String> values = ky.getValues();
291 if (ky.key != null && values != null) {
292 try {
293 presetsValueData.putAll(ky.key, values);
294 harmonizedKeys.put(harmonizeKey(ky.key), ky.key);
295 } catch (NullPointerException e) {
296 Main.error(e, p+": Unable to initialize "+ky+'.');
297 }
298 }
299 }
300
301 /**
302 * Checks given string (key or value) if it contains characters with code below 0x20 (either newline or some other special characters)
303 * @param s string to check
304 * @return {@code true} if {@code s} contains characters with code below 0x20
305 */
306 private static boolean containsLow(String s) {
307 if (s == null)
308 return false;
309 for (int i = 0; i < s.length(); i++) {
310 if (s.charAt(i) < 0x20)
311 return true;
312 }
313 return false;
314 }
315
316 /**
317 * Determines if the given key is in internal presets.
318 * @param key key
319 * @return {@code true} if the given key is in internal presets
320 * @since 9023
321 */
322 public static boolean isKeyInPresets(String key) {
323 return presetsValueData.get(key) != null;
324 }
325
326 /**
327 * Determines if the given tag is in internal presets.
328 * @param key key
329 * @param value value
330 * @return {@code true} if the given tag is in internal presets
331 * @since 9023
332 */
333 public static boolean isTagInPresets(String key, String value) {
334 final Set<String> values = presetsValueData.get(key);
335 return values != null && (values.isEmpty() || values.contains(value));
336 }
337
338 /**
339 * Returns the list of ignored tags.
340 * @return the list of ignored tags
341 * @since 9023
342 */
343 public static List<Tag> getIgnoredTags() {
344 return new ArrayList<>(ignoreDataTag);
345 }
346
347 /**
348 * Determines if the given tag is ignored for checks "key/tag not in presets".
349 * @param key key
350 * @param value value
351 * @return {@code true} if the given tag is ignored
352 * @since 9023
353 */
354 public static boolean isTagIgnored(String key, String value) {
355 boolean tagInPresets = isTagInPresets(key, value);
356 boolean ignore = false;
357
358 for (String a : ignoreDataStartsWith) {
359 if (key.startsWith(a)) {
360 ignore = true;
361 }
362 }
363 for (String a : ignoreDataEquals) {
364 if (key.equals(a)) {
365 ignore = true;
366 }
367 }
368 for (String a : ignoreDataEndsWith) {
369 if (key.endsWith(a)) {
370 ignore = true;
371 }
372 }
373
374 if (!tagInPresets) {
375 for (Tag a : ignoreDataTag) {
376 if (key.equals(a.getKey()) && value.equals(a.getValue())) {
377 ignore = true;
378 }
379 }
380 }
381 return ignore;
382 }
383
384 /**
385 * Checks the primitive tags
386 * @param p The primitive to check
387 */
388 @Override
389 public void check(OsmPrimitive p) {
390 // Just a collection to know if a primitive has been already marked with error
391 MultiMap<OsmPrimitive, String> withErrors = new MultiMap<>();
392
393 if (checkComplex) {
394 Map<String, String> keys = p.getKeys();
395 for (CheckerData d : checkerData) {
396 if (d.match(p, keys)) {
397 errors.add(new TestError(this, d.getSeverity(), tr("Suspicious tag/value combinations"),
398 d.getDescription(), d.getDescriptionOrig(), d.getCode(), p));
399 withErrors.put(p, "TC");
400 }
401 }
402 }
403
404 for (Entry<String, String> prop : p.getKeys().entrySet()) {
405 String s = marktr("Key ''{0}'' invalid.");
406 String key = prop.getKey();
407 String value = prop.getValue();
408 if (checkValues && (containsLow(value)) && !withErrors.contains(p, "ICV")) {
409 errors.add(new TestError(this, Severity.WARNING, tr("Tag value contains character with code less than 0x20"),
410 tr(s, key), MessageFormat.format(s, key), LOW_CHAR_VALUE, p));
411 withErrors.put(p, "ICV");
412 }
413 if (checkKeys && (containsLow(key)) && !withErrors.contains(p, "ICK")) {
414 errors.add(new TestError(this, Severity.WARNING, tr("Tag key contains character with code less than 0x20"),
415 tr(s, key), MessageFormat.format(s, key), LOW_CHAR_KEY, p));
416 withErrors.put(p, "ICK");
417 }
418 if (checkValues && (value != null && value.length() > 255) && !withErrors.contains(p, "LV")) {
419 errors.add(new TestError(this, Severity.ERROR, tr("Tag value longer than allowed"),
420 tr(s, key), MessageFormat.format(s, key), LONG_VALUE, p));
421 withErrors.put(p, "LV");
422 }
423 if (checkKeys && (key != null && key.length() > 255) && !withErrors.contains(p, "LK")) {
424 errors.add(new TestError(this, Severity.ERROR, tr("Tag key longer than allowed"),
425 tr(s, key), MessageFormat.format(s, key), LONG_KEY, p));
426 withErrors.put(p, "LK");
427 }
428 if (checkValues && (value == null || value.trim().isEmpty()) && !withErrors.contains(p, "EV")) {
429 errors.add(new TestError(this, Severity.WARNING, tr("Tags with empty values"),
430 tr(s, key), MessageFormat.format(s, key), EMPTY_VALUES, p));
431 withErrors.put(p, "EV");
432 }
433 if (checkKeys && key != null && key.indexOf(' ') >= 0 && !withErrors.contains(p, "IPK")) {
434 errors.add(new TestError(this, Severity.WARNING, tr("Invalid white space in property key"),
435 tr(s, key), MessageFormat.format(s, key), INVALID_KEY_SPACE, p));
436 withErrors.put(p, "IPK");
437 }
438 if (checkValues && value != null && (value.startsWith(" ") || value.endsWith(" ")) && !withErrors.contains(p, "SPACE")) {
439 errors.add(new TestError(this, Severity.WARNING, tr("Property values start or end with white space"),
440 tr(s, key), MessageFormat.format(s, key), INVALID_SPACE, p));
441 withErrors.put(p, "SPACE");
442 }
443 if (checkValues && value != null && value.contains(" ") && !withErrors.contains(p, "SPACE")) {
444 errors.add(new TestError(this, Severity.WARNING, tr("Property values contain multiple white spaces"),
445 tr(s, key), MessageFormat.format(s, key), MULTIPLE_SPACES, p));
446 withErrors.put(p, "SPACE");
447 }
448 if (checkValues && value != null && !value.equals(Entities.unescape(value)) && !withErrors.contains(p, "HTML")) {
449 errors.add(new TestError(this, Severity.OTHER, tr("Property values contain HTML entity"),
450 tr(s, key), MessageFormat.format(s, key), INVALID_HTML, p));
451 withErrors.put(p, "HTML");
452 }
453 if (checkValues && key != null && value != null && !value.isEmpty() && presetsValueData != null) {
454 if (!isTagIgnored(key, value)) {
455 if (!isKeyInPresets(key)) {
456 String prettifiedKey = harmonizeKey(key);
457 String fixedKey = harmonizedKeys.get(prettifiedKey);
458 if (fixedKey != null && !"".equals(fixedKey) && !fixedKey.equals(key)) {
459 // misspelled preset key
460 String i = marktr("Key ''{0}'' looks like ''{1}''.");
461 final TestError error;
462 if (p.hasKey(fixedKey)) {
463 error = new TestError(this, Severity.WARNING, tr("Misspelled property key"),
464 tr(i, key, fixedKey),
465 MessageFormat.format(i, key, fixedKey), MISSPELLED_KEY, p);
466 } else {
467 error = new FixableTestError(this, Severity.WARNING, tr("Misspelled property key"),
468 tr(i, key, fixedKey),
469 MessageFormat.format(i, key, fixedKey), MISSPELLED_KEY, p,
470 new ChangePropertyKeyCommand(p, key, fixedKey));
471 }
472 errors.add(error);
473 withErrors.put(p, "WPK");
474 } else {
475 String i = marktr("Key ''{0}'' not in presets.");
476 errors.add(new TestError(this, Severity.OTHER, tr("Presets do not contain property key"),
477 tr(i, key), MessageFormat.format(i, key), INVALID_VALUE, p));
478 withErrors.put(p, "UPK");
479 }
480 } else if (!isTagInPresets(key, value)) {
481 // try to fix common typos and check again if value is still unknown
482 String fixedValue = harmonizeValue(prop.getValue());
483 Map<String, String> possibleValues = getPossibleValues(presetsValueData.get(key));
484 if (possibleValues.containsKey(fixedValue)) {
485 fixedValue = possibleValues.get(fixedValue);
486 // misspelled preset value
487 String i = marktr("Value ''{0}'' for key ''{1}'' looks like ''{2}''.");
488 errors.add(new FixableTestError(this, Severity.WARNING, tr("Misspelled property value"),
489 tr(i, prop.getValue(), key, fixedValue), MessageFormat.format(i, prop.getValue(), fixedValue),
490 MISSPELLED_VALUE, p, new ChangePropertyCommand(p, key, fixedValue)));
491 withErrors.put(p, "WPV");
492 } else {
493 // unknown preset value
494 String i = marktr("Value ''{0}'' for key ''{1}'' not in presets.");
495 errors.add(new TestError(this, Severity.OTHER, tr("Presets do not contain property value"),
496 tr(i, prop.getValue(), key), MessageFormat.format(i, prop.getValue(), key), INVALID_VALUE, p));
497 withErrors.put(p, "UPV");
498 }
499 }
500 }
501 }
502 if (checkFixmes && key != null && value != null && !value.isEmpty()) {
503 if ((value.toLowerCase(Locale.ENGLISH).contains("fixme")
504 || value.contains("check and delete")
505 || key.contains("todo") || key.toLowerCase(Locale.ENGLISH).contains("fixme"))
506 && !withErrors.contains(p, "FIXME")) {
507 errors.add(new TestError(this, Severity.OTHER,
508 tr("FIXMES"), FIXME, p));
509 withErrors.put(p, "FIXME");
510 }
511 }
512 }
513 }
514
515 private static Map<String, String> getPossibleValues(Set<String> values) {
516 // generate a map with common typos
517 Map<String, String> map = new HashMap<>();
518 if (values != null) {
519 for (String value : values) {
520 map.put(value, value);
521 if (value.contains("_")) {
522 map.put(value.replace("_", ""), value);
523 }
524 }
525 }
526 return map;
527 }
528
529 private static String harmonizeKey(String key) {
530 key = key.toLowerCase(Locale.ENGLISH).replace('-', '_').replace(':', '_').replace(' ', '_');
531 return Utils.strip(key, "-_;:,");
532 }
533
534 private static String harmonizeValue(String value) {
535 value = value.toLowerCase(Locale.ENGLISH).replace('-', '_').replace(' ', '_');
536 return Utils.strip(value, "-_;:,");
537 }
538
539 @Override
540 public void startTest(ProgressMonitor monitor) {
541 super.startTest(monitor);
542 checkKeys = Main.pref.getBoolean(PREF_CHECK_KEYS, true);
543 if (isBeforeUpload) {
544 checkKeys = checkKeys && Main.pref.getBoolean(PREF_CHECK_KEYS_BEFORE_UPLOAD, true);
545 }
546
547 checkValues = Main.pref.getBoolean(PREF_CHECK_VALUES, true);
548 if (isBeforeUpload) {
549 checkValues = checkValues && Main.pref.getBoolean(PREF_CHECK_VALUES_BEFORE_UPLOAD, true);
550 }
551
552 checkComplex = Main.pref.getBoolean(PREF_CHECK_COMPLEX, true);
553 if (isBeforeUpload) {
554 checkComplex = checkComplex && Main.pref.getBoolean(PREF_CHECK_COMPLEX_BEFORE_UPLOAD, true);
555 }
556
557 checkFixmes = Main.pref.getBoolean(PREF_CHECK_FIXMES, true);
558 if (isBeforeUpload) {
559 checkFixmes = checkFixmes && Main.pref.getBoolean(PREF_CHECK_FIXMES_BEFORE_UPLOAD, true);
560 }
561 }
562
563 @Override
564 public void visit(Collection<OsmPrimitive> selection) {
565 if (checkKeys || checkValues || checkComplex || checkFixmes) {
566 super.visit(selection);
567 }
568 }
569
570 @Override
571 public void addGui(JPanel testPanel) {
572 GBC a = GBC.eol();
573 a.anchor = GridBagConstraints.EAST;
574
575 testPanel.add(new JLabel(name+" :"), GBC.eol().insets(3, 0, 0, 0));
576
577 prefCheckKeys = new JCheckBox(tr("Check property keys."), Main.pref.getBoolean(PREF_CHECK_KEYS, true));
578 prefCheckKeys.setToolTipText(tr("Validate that property keys are valid checking against list of words."));
579 testPanel.add(prefCheckKeys, GBC.std().insets(20, 0, 0, 0));
580
581 prefCheckKeysBeforeUpload = new JCheckBox();
582 prefCheckKeysBeforeUpload.setSelected(Main.pref.getBoolean(PREF_CHECK_KEYS_BEFORE_UPLOAD, true));
583 testPanel.add(prefCheckKeysBeforeUpload, a);
584
585 prefCheckComplex = new JCheckBox(tr("Use complex property checker."), Main.pref.getBoolean(PREF_CHECK_COMPLEX, true));
586 prefCheckComplex.setToolTipText(tr("Validate property values and tags using complex rules."));
587 testPanel.add(prefCheckComplex, GBC.std().insets(20, 0, 0, 0));
588
589 prefCheckComplexBeforeUpload = new JCheckBox();
590 prefCheckComplexBeforeUpload.setSelected(Main.pref.getBoolean(PREF_CHECK_COMPLEX_BEFORE_UPLOAD, true));
591 testPanel.add(prefCheckComplexBeforeUpload, a);
592
593 final Collection<String> sources = Main.pref.getCollection(PREF_SOURCES, DEFAULT_SOURCES);
594 sourcesList = new EditableList(tr("TagChecker source"));
595 sourcesList.setItems(sources);
596 testPanel.add(new JLabel(tr("Data sources ({0})", "*.cfg")), GBC.eol().insets(23, 0, 0, 0));
597 testPanel.add(sourcesList, GBC.eol().fill(GridBagConstraints.HORIZONTAL).insets(23, 0, 0, 0));
598
599 ActionListener disableCheckActionListener = e -> handlePrefEnable();
600 prefCheckKeys.addActionListener(disableCheckActionListener);
601 prefCheckKeysBeforeUpload.addActionListener(disableCheckActionListener);
602 prefCheckComplex.addActionListener(disableCheckActionListener);
603 prefCheckComplexBeforeUpload.addActionListener(disableCheckActionListener);
604
605 handlePrefEnable();
606
607 prefCheckValues = new JCheckBox(tr("Check property values."), Main.pref.getBoolean(PREF_CHECK_VALUES, true));
608 prefCheckValues.setToolTipText(tr("Validate that property values are valid checking against presets."));
609 testPanel.add(prefCheckValues, GBC.std().insets(20, 0, 0, 0));
610
611 prefCheckValuesBeforeUpload = new JCheckBox();
612 prefCheckValuesBeforeUpload.setSelected(Main.pref.getBoolean(PREF_CHECK_VALUES_BEFORE_UPLOAD, true));
613 testPanel.add(prefCheckValuesBeforeUpload, a);
614
615 prefCheckFixmes = new JCheckBox(tr("Check for FIXMES."), Main.pref.getBoolean(PREF_CHECK_FIXMES, true));
616 prefCheckFixmes.setToolTipText(tr("Looks for nodes or ways with FIXME in any property value."));
617 testPanel.add(prefCheckFixmes, GBC.std().insets(20, 0, 0, 0));
618
619 prefCheckFixmesBeforeUpload = new JCheckBox();
620 prefCheckFixmesBeforeUpload.setSelected(Main.pref.getBoolean(PREF_CHECK_FIXMES_BEFORE_UPLOAD, true));
621 testPanel.add(prefCheckFixmesBeforeUpload, a);
622 }
623
624 public void handlePrefEnable() {
625 boolean selected = prefCheckKeys.isSelected() || prefCheckKeysBeforeUpload.isSelected()
626 || prefCheckComplex.isSelected() || prefCheckComplexBeforeUpload.isSelected();
627 sourcesList.setEnabled(selected);
628 }
629
630 @Override
631 public boolean ok() {
632 enabled = prefCheckKeys.isSelected() || prefCheckValues.isSelected() || prefCheckComplex.isSelected() || prefCheckFixmes.isSelected();
633 testBeforeUpload = prefCheckKeysBeforeUpload.isSelected() || prefCheckValuesBeforeUpload.isSelected()
634 || prefCheckFixmesBeforeUpload.isSelected() || prefCheckComplexBeforeUpload.isSelected();
635
636 Main.pref.put(PREF_CHECK_VALUES, prefCheckValues.isSelected());
637 Main.pref.put(PREF_CHECK_COMPLEX, prefCheckComplex.isSelected());
638 Main.pref.put(PREF_CHECK_KEYS, prefCheckKeys.isSelected());
639 Main.pref.put(PREF_CHECK_FIXMES, prefCheckFixmes.isSelected());
640 Main.pref.put(PREF_CHECK_VALUES_BEFORE_UPLOAD, prefCheckValuesBeforeUpload.isSelected());
641 Main.pref.put(PREF_CHECK_COMPLEX_BEFORE_UPLOAD, prefCheckComplexBeforeUpload.isSelected());
642 Main.pref.put(PREF_CHECK_KEYS_BEFORE_UPLOAD, prefCheckKeysBeforeUpload.isSelected());
643 Main.pref.put(PREF_CHECK_FIXMES_BEFORE_UPLOAD, prefCheckFixmesBeforeUpload.isSelected());
644 return Main.pref.putCollection(PREF_SOURCES, sourcesList.getItems());
645 }
646
647 @Override
648 public Command fixError(TestError testError) {
649 List<Command> commands = new ArrayList<>(50);
650
651 if (testError instanceof FixableTestError) {
652 commands.add(testError.getFix());
653 } else {
654 Collection<? extends OsmPrimitive> primitives = testError.getPrimitives();
655 for (OsmPrimitive p : primitives) {
656 Map<String, String> tags = p.getKeys();
657 if (tags == null || tags.isEmpty()) {
658 continue;
659 }
660
661 for (Entry<String, String> prop: tags.entrySet()) {
662 String key = prop.getKey();
663 String value = prop.getValue();
664 if (value == null || value.trim().isEmpty()) {
665 commands.add(new ChangePropertyCommand(p, key, null));
666 } else if (value.startsWith(" ") || value.endsWith(" ") || value.contains(" ")) {
667 commands.add(new ChangePropertyCommand(p, key, Tag.removeWhiteSpaces(value)));
668 } else if (key.startsWith(" ") || key.endsWith(" ") || key.contains(" ")) {
669 commands.add(new ChangePropertyKeyCommand(p, key, Tag.removeWhiteSpaces(key)));
670 } else {
671 String evalue = Entities.unescape(value);
672 if (!evalue.equals(value)) {
673 commands.add(new ChangePropertyCommand(p, key, evalue));
674 }
675 }
676 }
677 }
678 }
679
680 if (commands.isEmpty())
681 return null;
682 if (commands.size() == 1)
683 return commands.get(0);
684
685 return new SequenceCommand(tr("Fix tags"), commands);
686 }
687
688 @Override
689 public boolean isFixable(TestError testError) {
690 if (testError.getTester() instanceof TagChecker) {
691 int code = testError.getCode();
692 return code == INVALID_KEY || code == EMPTY_VALUES || code == INVALID_SPACE ||
693 code == INVALID_KEY_SPACE || code == INVALID_HTML || code == MISSPELLED_VALUE ||
694 code == MULTIPLE_SPACES;
695 }
696
697 return false;
698 }
699
700 protected static class CheckerData {
701 private String description;
702 protected List<CheckerElement> data = new ArrayList<>();
703 private OsmPrimitiveType type;
704 private int code;
705 protected Severity severity;
706 // CHECKSTYLE.OFF: SingleSpaceSeparator
707 protected static final int TAG_CHECK_ERROR = 1250;
708 protected static final int TAG_CHECK_WARN = 1260;
709 protected static final int TAG_CHECK_INFO = 1270;
710 // CHECKSTYLE.ON: SingleSpaceSeparator
711
712 protected static class CheckerElement {
713 public Object tag;
714 public Object value;
715 public boolean noMatch;
716 public boolean tagAll;
717 public boolean valueAll;
718 public boolean valueBool;
719
720 private static Pattern getPattern(String str) {
721 if (str.endsWith("/i"))
722 return Pattern.compile(str.substring(1, str.length()-2), Pattern.CASE_INSENSITIVE);
723 if (str.endsWith("/"))
724 return Pattern.compile(str.substring(1, str.length()-1));
725
726 throw new IllegalStateException();
727 }
728
729 public CheckerElement(String exp) {
730 Matcher m = Pattern.compile("(.+)([!=]=)(.+)").matcher(exp);
731 m.matches();
732
733 String n = m.group(1).trim();
734
735 if ("*".equals(n)) {
736 tagAll = true;
737 } else {
738 tag = n.startsWith("/") ? getPattern(n) : n;
739 noMatch = "!=".equals(m.group(2));
740 n = m.group(3).trim();
741 if ("*".equals(n)) {
742 valueAll = true;
743 } else if ("BOOLEAN_TRUE".equals(n)) {
744 valueBool = true;
745 value = OsmUtils.trueval;
746 } else if ("BOOLEAN_FALSE".equals(n)) {
747 valueBool = true;
748 value = OsmUtils.falseval;
749 } else {
750 value = n.startsWith("/") ? getPattern(n) : n;
751 }
752 }
753 }
754
755 public boolean match(Map<String, String> keys) {
756 for (Entry<String, String> prop: keys.entrySet()) {
757 String key = prop.getKey();
758 String val = valueBool ? OsmUtils.getNamedOsmBoolean(prop.getValue()) : prop.getValue();
759 if ((tagAll || (tag instanceof Pattern ? ((Pattern) tag).matcher(key).matches() : key.equals(tag)))
760 && (valueAll || (value instanceof Pattern ? ((Pattern) value).matcher(val).matches() : val.equals(value))))
761 return !noMatch;
762 }
763 return noMatch;
764 }
765 }
766
767 private static final Pattern CLEAN_STR_PATTERN = Pattern.compile(" *# *([^#]+) *$");
768 private static final Pattern SPLIT_TRIMMED_PATTERN = Pattern.compile(" *: *");
769 private static final Pattern SPLIT_ELEMENTS_PATTERN = Pattern.compile(" *&& *");
770
771 public String getData(final String str) {
772 Matcher m = CLEAN_STR_PATTERN.matcher(str);
773 String trimmed = m.replaceFirst("").trim();
774 try {
775 description = m.group(1);
776 if (description != null && description.isEmpty()) {
777 description = null;
778 }
779 } catch (IllegalStateException e) {
780 Main.error(e);
781 description = null;
782 }
783 String[] n = SPLIT_TRIMMED_PATTERN.split(trimmed, 3);
784 switch (n[0]) {
785 case "way":
786 type = OsmPrimitiveType.WAY;
787 break;
788 case "node":
789 type = OsmPrimitiveType.NODE;
790 break;
791 case "relation":
792 type = OsmPrimitiveType.RELATION;
793 break;
794 case "*":
795 type = null;
796 break;
797 default:
798 return tr("Could not find element type");
799 }
800 if (n.length != 3)
801 return tr("Incorrect number of parameters");
802
803 switch (n[1]) {
804 case "W":
805 severity = Severity.WARNING;
806 code = TAG_CHECK_WARN;
807 break;
808 case "E":
809 severity = Severity.ERROR;
810 code = TAG_CHECK_ERROR;
811 break;
812 case "I":
813 severity = Severity.OTHER;
814 code = TAG_CHECK_INFO;
815 break;
816 default:
817 return tr("Could not find warning level");
818 }
819 for (String exp: SPLIT_ELEMENTS_PATTERN.split(n[2])) {
820 try {
821 data.add(new CheckerElement(exp));
822 } catch (IllegalStateException e) {
823 Main.trace(e);
824 return tr("Illegal expression ''{0}''", exp);
825 } catch (PatternSyntaxException e) {
826 Main.trace(e);
827 return tr("Illegal regular expression ''{0}''", exp);
828 }
829 }
830 return null;
831 }
832
833 public boolean match(OsmPrimitive osm, Map<String, String> keys) {
834 if (type != null && OsmPrimitiveType.from(osm) != type)
835 return false;
836
837 for (CheckerElement ce : data) {
838 if (!ce.match(keys))
839 return false;
840 }
841 return true;
842 }
843
844 public String getDescription() {
845 return tr(description);
846 }
847
848 public String getDescriptionOrig() {
849 return description;
850 }
851
852 public Severity getSeverity() {
853 return severity;
854 }
855
856 public int getCode() {
857 if (type == null)
858 return code;
859
860 return code + type.ordinal() + 1;
861 }
862 }
863}
Note: See TracBrowser for help on using the repository browser.