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

Last change on this file since 14531 was 14531, checked in by GerdP, 5 years ago

see #17055 improve TagChecker: Don't say "looks like" when all characters of given value are different, e.g. "Value '10' for key 'fee' looks like 'no'." makes no sense. The Levenshtein distance from 10 to no is only 2, but the length is also only 2.

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