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

Last change on this file since 12841 was 12841, checked in by bastiK, 5 weeks ago

see #15229 - fix deprecations caused by [12840]

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