[8378] | 1 | // License: GPL. For details, see LICENSE file.
|
---|
[3669] | 2 | package org.openstreetmap.josm.data.validation.tests;
|
---|
| 3 |
|
---|
| 4 | import static org.openstreetmap.josm.tools.I18n.marktr;
|
---|
| 5 | import static org.openstreetmap.josm.tools.I18n.tr;
|
---|
| 6 |
|
---|
| 7 | import java.awt.GridBagConstraints;
|
---|
| 8 | import java.awt.event.ActionListener;
|
---|
| 9 | import java.io.BufferedReader;
|
---|
| 10 | import java.io.IOException;
|
---|
| 11 | import java.util.ArrayList;
|
---|
| 12 | import java.util.Arrays;
|
---|
| 13 | import java.util.Collection;
|
---|
[14490] | 14 | import java.util.Collections;
|
---|
[3669] | 15 | import java.util.HashMap;
|
---|
[14508] | 16 | import java.util.HashSet;
|
---|
[3669] | 17 | import java.util.List;
|
---|
[8404] | 18 | import java.util.Locale;
|
---|
[3669] | 19 | import java.util.Map;
|
---|
| 20 | import java.util.Map.Entry;
|
---|
[3674] | 21 | import java.util.Set;
|
---|
[3669] | 22 | import java.util.regex.Matcher;
|
---|
| 23 | import java.util.regex.Pattern;
|
---|
| 24 | import java.util.regex.PatternSyntaxException;
|
---|
| 25 |
|
---|
| 26 | import javax.swing.JCheckBox;
|
---|
| 27 | import javax.swing.JLabel;
|
---|
| 28 | import javax.swing.JPanel;
|
---|
| 29 |
|
---|
| 30 | import org.openstreetmap.josm.command.ChangePropertyCommand;
|
---|
[4806] | 31 | import org.openstreetmap.josm.command.ChangePropertyKeyCommand;
|
---|
[3669] | 32 | import org.openstreetmap.josm.command.Command;
|
---|
| 33 | import org.openstreetmap.josm.command.SequenceCommand;
|
---|
[13809] | 34 | import org.openstreetmap.josm.data.osm.AbstractPrimitive;
|
---|
[3669] | 35 | import org.openstreetmap.josm.data.osm.OsmPrimitive;
|
---|
| 36 | import org.openstreetmap.josm.data.osm.OsmPrimitiveType;
|
---|
| 37 | import org.openstreetmap.josm.data.osm.OsmUtils;
|
---|
[6699] | 38 | import org.openstreetmap.josm.data.osm.Tag;
|
---|
[13414] | 39 | import org.openstreetmap.josm.data.osm.Tagged;
|
---|
[12649] | 40 | import org.openstreetmap.josm.data.preferences.sources.ValidatorPrefHelper;
|
---|
[3669] | 41 | import org.openstreetmap.josm.data.validation.Severity;
|
---|
[8435] | 42 | import org.openstreetmap.josm.data.validation.Test.TagTest;
|
---|
[3669] | 43 | import org.openstreetmap.josm.data.validation.TestError;
|
---|
| 44 | import org.openstreetmap.josm.data.validation.util.Entities;
|
---|
| 45 | import org.openstreetmap.josm.gui.progress.ProgressMonitor;
|
---|
[8863] | 46 | import org.openstreetmap.josm.gui.tagging.presets.TaggingPreset;
|
---|
| 47 | import org.openstreetmap.josm.gui.tagging.presets.TaggingPresetItem;
|
---|
| 48 | import org.openstreetmap.josm.gui.tagging.presets.TaggingPresets;
|
---|
| 49 | import org.openstreetmap.josm.gui.tagging.presets.items.Check;
|
---|
| 50 | import org.openstreetmap.josm.gui.tagging.presets.items.CheckGroup;
|
---|
| 51 | import org.openstreetmap.josm.gui.tagging.presets.items.KeyedItem;
|
---|
[6553] | 52 | import org.openstreetmap.josm.gui.widgets.EditableList;
|
---|
[7248] | 53 | import org.openstreetmap.josm.io.CachedFile;
|
---|
[12846] | 54 | import org.openstreetmap.josm.spi.preferences.Config;
|
---|
[3669] | 55 | import org.openstreetmap.josm.tools.GBC;
|
---|
[12620] | 56 | import org.openstreetmap.josm.tools.Logging;
|
---|
[3674] | 57 | import org.openstreetmap.josm.tools.MultiMap;
|
---|
[8435] | 58 | import 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] | 66 | public 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 | }
|
---|