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

Last change on this file since 12649 was 12649, checked in by Don-vip, 4 months ago

see #15182 - code refactoring to avoid dependence on GUI packages from Preferences

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