// License: GPL. For details, see LICENSE file.
package org.openstreetmap.josm.data.validation.tests;
import static org.openstreetmap.josm.tools.I18n.marktr;
import static org.openstreetmap.josm.tools.I18n.tr;
import java.awt.GridBagConstraints;
import java.awt.event.ActionListener;
import java.io.BufferedReader;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;
import javax.swing.JCheckBox;
import javax.swing.JLabel;
import javax.swing.JPanel;
import org.openstreetmap.josm.Main;
import org.openstreetmap.josm.command.ChangePropertyCommand;
import org.openstreetmap.josm.command.ChangePropertyKeyCommand;
import org.openstreetmap.josm.command.Command;
import org.openstreetmap.josm.command.SequenceCommand;
import org.openstreetmap.josm.data.osm.OsmPrimitive;
import org.openstreetmap.josm.data.osm.OsmPrimitiveType;
import org.openstreetmap.josm.data.osm.OsmUtils;
import org.openstreetmap.josm.data.osm.Tag;
import org.openstreetmap.josm.data.validation.Severity;
import org.openstreetmap.josm.data.validation.Test.TagTest;
import org.openstreetmap.josm.data.validation.TestError;
import org.openstreetmap.josm.data.validation.util.Entities;
import org.openstreetmap.josm.gui.preferences.validator.ValidatorPreference;
import org.openstreetmap.josm.gui.progress.ProgressMonitor;
import org.openstreetmap.josm.gui.tagging.presets.TaggingPreset;
import org.openstreetmap.josm.gui.tagging.presets.TaggingPresetItem;
import org.openstreetmap.josm.gui.tagging.presets.TaggingPresets;
import org.openstreetmap.josm.gui.tagging.presets.items.Check;
import org.openstreetmap.josm.gui.tagging.presets.items.CheckGroup;
import org.openstreetmap.josm.gui.tagging.presets.items.KeyedItem;
import org.openstreetmap.josm.gui.widgets.EditableList;
import org.openstreetmap.josm.io.CachedFile;
import org.openstreetmap.josm.tools.GBC;
import org.openstreetmap.josm.tools.MultiMap;
import org.openstreetmap.josm.tools.Utils;
/**
* Check for misspelled or wrong tags
*
* @author frsantos
* @since 3669
*/
public class TagChecker extends TagTest {
/** The config file of ignored tags */
public static final String IGNORE_FILE = "resource://data/validator/ignoretags.cfg";
/** The config file of dictionary words */
public static final String SPELL_FILE = "resource://data/validator/words.cfg";
/** Normalized keys: the key should be substituted by the value if the key was not found in presets */
private static final Map<String, String> harmonizedKeys = new HashMap<>();
/** The spell check preset values */
private static volatile MultiMap<String, String> presetsValueData;
/** The TagChecker data */
private static final List<CheckerData> checkerData = new ArrayList<>();
private static final List<String> ignoreDataStartsWith = new ArrayList<>();
private static final List<String> ignoreDataEquals = new ArrayList<>();
private static final List<String> ignoreDataEndsWith = new ArrayList<>();
private static final List<Tag> ignoreDataTag = new ArrayList<>();
/** The preferences prefix */
protected static final String PREFIX = ValidatorPreference.PREFIX + "." + TagChecker.class.getSimpleName();
public static final String PREF_CHECK_VALUES = PREFIX + ".checkValues";
public static final String PREF_CHECK_KEYS = PREFIX + ".checkKeys";
public static final String PREF_CHECK_COMPLEX = PREFIX + ".checkComplex";
public static final String PREF_CHECK_FIXMES = PREFIX + ".checkFixmes";
public static final String PREF_SOURCES = PREFIX + ".source";
public static final String PREF_CHECK_KEYS_BEFORE_UPLOAD = PREF_CHECK_KEYS + "BeforeUpload";
public static final String PREF_CHECK_VALUES_BEFORE_UPLOAD = PREF_CHECK_VALUES + "BeforeUpload";
public static final String PREF_CHECK_COMPLEX_BEFORE_UPLOAD = PREF_CHECK_COMPLEX + "BeforeUpload";
public static final String PREF_CHECK_FIXMES_BEFORE_UPLOAD = PREF_CHECK_FIXMES + "BeforeUpload";
protected boolean checkKeys;
protected boolean checkValues;
protected boolean checkComplex;
protected boolean checkFixmes;
protected JCheckBox prefCheckKeys;
protected JCheckBox prefCheckValues;
protected JCheckBox prefCheckComplex;
protected JCheckBox prefCheckFixmes;
protected JCheckBox prefCheckPaint;
protected JCheckBox prefCheckKeysBeforeUpload;
protected JCheckBox prefCheckValuesBeforeUpload;
protected JCheckBox prefCheckComplexBeforeUpload;
protected JCheckBox prefCheckFixmesBeforeUpload;
protected JCheckBox prefCheckPaintBeforeUpload;
// CHECKSTYLE.OFF: SingleSpaceSeparator
protected static final int EMPTY_VALUES = 1200;
protected static final int INVALID_KEY = 1201;
protected static final int INVALID_VALUE = 1202;
protected static final int FIXME = 1203;
protected static final int INVALID_SPACE = 1204;
protected static final int INVALID_KEY_SPACE = 1205;
protected static final int INVALID_HTML = 1206; /* 1207 was PAINT */
protected static final int LONG_VALUE = 1208;
protected static final int LONG_KEY = 1209;
protected static final int LOW_CHAR_VALUE = 1210;
protected static final int LOW_CHAR_KEY = 1211;
protected static final int MISSPELLED_VALUE = 1212;
protected static final int MISSPELLED_KEY = 1213;
protected static final int MULTIPLE_SPACES = 1214;
// CHECKSTYLE.ON: SingleSpaceSeparator
// 1250 and up is used by tagcheck
protected EditableList sourcesList;
private static final Set<String> DEFAULT_SOURCES = new HashSet<>(Arrays.asList(/*DATA_FILE, */IGNORE_FILE, SPELL_FILE));
/**
* Constructor
*/
public TagChecker() {
super(tr("Tag checker"), tr("This test checks for errors in tag keys and values."));
}
@Override
public void initialize() throws IOException {
initializeData();
initializePresets();
}
/**
* Reads the spellcheck file into a HashMap.
* The data file is a list of words, beginning with +/-. If it starts with +,
* the word is valid, but if it starts with -, the word should be replaced
* by the nearest + word before this.
*
* @throws IOException if any I/O error occurs
*/
private static void initializeData() throws IOException {
checkerData.clear();
ignoreDataStartsWith.clear();
ignoreDataEquals.clear();
ignoreDataEndsWith.clear();
ignoreDataTag.clear();
harmonizedKeys.clear();
StringBuilder errorSources = new StringBuilder();
for (String source : Main.pref.getCollection(PREF_SOURCES, DEFAULT_SOURCES)) {
try (
CachedFile cf = new CachedFile(source);
BufferedReader reader = cf.getContentReader()
) {
String okValue = null;
boolean tagcheckerfile = false;
boolean ignorefile = false;
boolean isFirstLine = true;
String line;
while ((line = reader.readLine()) != null && (tagcheckerfile || !line.isEmpty())) {
if (line.startsWith("#")) {
if (line.startsWith("# JOSM TagChecker")) {
tagcheckerfile = true;
if (!DEFAULT_SOURCES.contains(source)) {
Main.info(tr("Adding {0} to tag checker", source));
}
} else
if (line.startsWith("# JOSM IgnoreTags")) {
ignorefile = true;
if (!DEFAULT_SOURCES.contains(source)) {
Main.info(tr("Adding {0} to ignore tags", source));
}
}
} else if (ignorefile) {
line = line.trim();
if (line.length() < 4) {
continue;
}
String key = line.substring(0, 2);
line = line.substring(2);
switch (key) {
case "S:":
ignoreDataStartsWith.add(line);
break;
case "E:":
ignoreDataEquals.add(line);
break;
case "F:":
ignoreDataEndsWith.add(line);
break;
case "K:":
ignoreDataTag.add(Tag.ofString(line));
break;
default:
if (!key.startsWith(";")) {
Main.warn("Unsupported TagChecker key: " + key);
}
}
} else if (tagcheckerfile) {
if (!line.isEmpty()) {
CheckerData d = new CheckerData();
String err = d.getData(line);
if (err == null) {
checkerData.add(d);
} else {
Main.error(tr("Invalid tagchecker line - {0}: {1}", err, line));
}
}
} else if (line.charAt(0) == '+') {
okValue = line.substring(1);
} else if (line.charAt(0) == '-' && okValue != null) {
harmonizedKeys.put(harmonizeKey(line.substring(1)), okValue);
} else {
Main.error(tr("Invalid spellcheck line: {0}", line));
}
if (isFirstLine) {
isFirstLine = false;
if (!(tagcheckerfile || ignorefile) && !DEFAULT_SOURCES.contains(source)) {
Main.info(tr("Adding {0} to spellchecker", source));
}
}
}
} catch (IOException e) {
Main.error(e);
errorSources.append(source).append('\n');
}
}
if (errorSources.length() > 0)
throw new IOException(tr("Could not access data file(s):\n{0}", errorSources));
}
/**
* Reads the presets data.
*
*/
public static void initializePresets() {
if (!Main.pref.getBoolean(PREF_CHECK_VALUES, true))
return;
Collection<TaggingPreset> presets = TaggingPresets.getTaggingPresets();
if (!presets.isEmpty()) {
presetsValueData = new MultiMap<>();
for (String a : OsmPrimitive.getUninterestingKeys()) {
presetsValueData.putVoid(a);
}
// TODO directionKeys are no longer in OsmPrimitive (search pattern is used instead)
for (String a : Main.pref.getCollection(ValidatorPreference.PREFIX + ".knownkeys",
Arrays.asList(new String[]{"is_in", "int_ref", "fixme", "population"}))) {
presetsValueData.putVoid(a);
}
for (TaggingPreset p : presets) {
for (TaggingPresetItem i : p.data) {
if (i instanceof KeyedItem) {
addPresetValue((KeyedItem) i);
} else if (i instanceof CheckGroup) {
for (Check c : ((CheckGroup) i).checks) {
addPresetValue(c);
}
}
}
}
}
}
private static void addPresetValue(KeyedItem ky) {
Collection<String> values = ky.getValues();
if (ky.key != null && values != null) {
presetsValueData.putAll(ky.key, values);
harmonizedKeys.put(harmonizeKey(ky.key), ky.key);
}
}
/**
* Checks given string (key or value) if it contains characters with code below 0x20 (either newline or some other special characters)
* @param s string to check
* @return {@code true} if {@code s} contains characters with code below 0x20
*/
private static boolean containsLow(String s) {
if (s == null)
return false;
for (int i = 0; i < s.length(); i++) {
if (s.charAt(i) < 0x20)
return true;
}
return false;
}
/**
* Determines if the given key is in internal presets.
* @param key key
* @return {@code true} if the given key is in internal presets
* @since 9023
*/
public static boolean isKeyInPresets(String key) {
return presetsValueData.get(key) != null;
}
/**
* Determines if the given tag is in internal presets.
* @param key key
* @param value value
* @return {@code true} if the given tag is in internal presets
* @since 9023
*/
public static boolean isTagInPresets(String key, String value) {
final Set<String> values = presetsValueData.get(key);
return values != null && (values.isEmpty() || values.contains(value));
}
/**
* Returns the list of ignored tags.
* @return the list of ignored tags
* @since 9023
*/
public static List<Tag> getIgnoredTags() {
return new ArrayList<>(ignoreDataTag);
}
/**
* Determines if the given tag is ignored for checks "key/tag not in presets".
* @param key key
* @param value value
* @return {@code true} if the given tag is ignored
* @since 9023
*/
public static boolean isTagIgnored(String key, String value) {
boolean tagInPresets = isTagInPresets(key, value);
boolean ignore = false;
for (String a : ignoreDataStartsWith) {
if (key.startsWith(a)) {
ignore = true;
}
}
for (String a : ignoreDataEquals) {
if (key.equals(a)) {
ignore = true;
}
}
for (String a : ignoreDataEndsWith) {
if (key.endsWith(a)) {
ignore = true;
}
}
if (!tagInPresets) {
for (Tag a : ignoreDataTag) {
if (key.equals(a.getKey()) && value.equals(a.getValue())) {
ignore = true;
}
}
}
return ignore;
}
/**
* Checks the primitive tags
* @param p The primitive to check
*/
@Override
public void check(OsmPrimitive p) {
// Just a collection to know if a primitive has been already marked with error
MultiMap<OsmPrimitive, String> withErrors = new MultiMap<>();
if (checkComplex) {
Map<String, String> keys = p.getKeys();
for (CheckerData d : checkerData) {
if (d.match(p, keys)) {
errors.add(TestError.builder(this, d.getSeverity(), d.getCode())
.message(tr("Suspicious tag/value combinations"), d.getDescription())
.primitives(p)
.build());
withErrors.put(p, "TC");
}
}
}
for (Entry<String, String> prop : p.getKeys().entrySet()) {
String s = marktr("Key ''{0}'' invalid.");
String key = prop.getKey();
String value = prop.getValue();
if (checkValues && (containsLow(value)) && !withErrors.contains(p, "ICV")) {
errors.add(TestError.builder(this, Severity.WARNING, LOW_CHAR_VALUE)
.message(tr("Tag value contains character with code less than 0x20"), s, key)
.primitives(p)
.build());
withErrors.put(p, "ICV");
}
if (checkKeys && (containsLow(key)) && !withErrors.contains(p, "ICK")) {
errors.add(TestError.builder(this, Severity.WARNING, LOW_CHAR_KEY)
.message(tr("Tag key contains character with code less than 0x20"), s, key)
.primitives(p)
.build());
withErrors.put(p, "ICK");
}
if (checkValues && (value != null && value.length() > 255) && !withErrors.contains(p, "LV")) {
errors.add(TestError.builder(this, Severity.ERROR, LONG_VALUE)
.message(tr("Tag value longer than allowed"), s, key)
.primitives(p)
.build());
withErrors.put(p, "LV");
}
if (checkKeys && (key != null && key.length() > 255) && !withErrors.contains(p, "LK")) {
errors.add(TestError.builder(this, Severity.ERROR, LONG_KEY)
.message(tr("Tag key longer than allowed"), s, key)
.primitives(p)
.build());
withErrors.put(p, "LK");
}
if (checkValues && (value == null || value.trim().isEmpty()) && !withErrors.contains(p, "EV")) {
errors.add(TestError.builder(this, Severity.WARNING, EMPTY_VALUES)
.message(tr("Tags with empty values"), s, key)
.primitives(p)
.build());
withErrors.put(p, "EV");
}
if (checkKeys && key != null && key.indexOf(' ') >= 0 && !withErrors.contains(p, "IPK")) {
errors.add(TestError.builder(this, Severity.WARNING, INVALID_KEY_SPACE)
.message(tr("Invalid white space in property key"), s, key)
.primitives(p)
.build());
withErrors.put(p, "IPK");
}
if (checkValues && value != null && (value.startsWith(" ") || value.endsWith(" ")) && !withErrors.contains(p, "SPACE")) {
errors.add(TestError.builder(this, Severity.WARNING, INVALID_SPACE)
.message(tr("Property values start or end with white space"), s, key)
.primitives(p)
.build());
withErrors.put(p, "SPACE");
}
if (checkValues && value != null && value.contains(" ") && !withErrors.contains(p, "SPACE")) {
errors.add(TestError.builder(this, Severity.WARNING, MULTIPLE_SPACES)
.message(tr("Property values contain multiple white spaces"), s, key)
.primitives(p)
.build());
withErrors.put(p, "SPACE");
}
if (checkValues && value != null && !value.equals(Entities.unescape(value)) && !withErrors.contains(p, "HTML")) {
errors.add(TestError.builder(this, Severity.OTHER, INVALID_HTML)
.message(tr("Property values contain HTML entity"), s, key)
.primitives(p)
.build());
withErrors.put(p, "HTML");
}
if (checkValues && key != null && value != null && !value.isEmpty() && presetsValueData != null && !isTagIgnored(key, value)) {
if (!isKeyInPresets(key)) {
String prettifiedKey = harmonizeKey(key);
String fixedKey = harmonizedKeys.get(prettifiedKey);
if (fixedKey != null && !"".equals(fixedKey) && !fixedKey.equals(key)) {
// misspelled preset key
final TestError.Builder error = TestError.builder(this, Severity.WARNING, MISSPELLED_KEY)
.message(tr("Misspelled property key"), marktr("Key ''{0}'' looks like ''{1}''."), key, fixedKey)
.primitives(p);
if (p.hasKey(fixedKey)) {
errors.add(error.build());
} else {
errors.add(error.fix(() -> new ChangePropertyKeyCommand(p, key, fixedKey)).build());
}
withErrors.put(p, "WPK");
} else {
errors.add(TestError.builder(this, Severity.OTHER, INVALID_VALUE)
.message(tr("Presets do not contain property key"), marktr("Key ''{0}'' not in presets."), key)
.primitives(p)
.build());
withErrors.put(p, "UPK");
}
} else if (!isTagInPresets(key, value)) {
// try to fix common typos and check again if value is still unknown
String fixedValue = harmonizeValue(prop.getValue());
Map<String, String> possibleValues = getPossibleValues(presetsValueData.get(key));
if (possibleValues.containsKey(fixedValue)) {
final String newKey = possibleValues.get(fixedValue);
// misspelled preset value
errors.add(TestError.builder(this, Severity.WARNING, MISSPELLED_VALUE)
.message(tr("Misspelled property value"),
marktr("Value ''{0}'' for key ''{1}'' looks like ''{2}''."), prop.getValue(), key, fixedValue)
.primitives(p)
.fix(() -> new ChangePropertyCommand(p, key, newKey))
.build());
withErrors.put(p, "WPV");
} else {
// unknown preset value
errors.add(TestError.builder(this, Severity.OTHER, INVALID_VALUE)
.message(tr("Presets do not contain property value"),
marktr("Value ''{0}'' for key ''{1}'' not in presets."), prop.getValue(), key)
.primitives(p)
.build());
withErrors.put(p, "UPV");
}
}
}
if (checkFixmes && key != null && value != null && !value.isEmpty() && isFixme(key, value) && !withErrors.contains(p, "FIXME")) {
errors.add(TestError.builder(this, Severity.OTHER, FIXME)
.message(tr("FIXMES"))
.primitives(p)
.build());
withErrors.put(p, "FIXME");
}
}
}
private static boolean isFixme(String key, String value) {
return key.toLowerCase(Locale.ENGLISH).contains("fixme") || key.contains("todo")
|| value.toLowerCase(Locale.ENGLISH).contains("fixme") || value.contains("check and delete");
}
private static Map<String, String> getPossibleValues(Set<String> values) {
// generate a map with common typos
Map<String, String> map = new HashMap<>();
if (values != null) {
for (String value : values) {
map.put(value, value);
if (value.contains("_")) {
map.put(value.replace("_", ""), value);
}
}
}
return map;
}
private static String harmonizeKey(String key) {
return Utils.strip(key.toLowerCase(Locale.ENGLISH).replace('-', '_').replace(':', '_').replace(' ', '_'), "-_;:,");
}
private static String harmonizeValue(String value) {
return Utils.strip(value.toLowerCase(Locale.ENGLISH).replace('-', '_').replace(' ', '_'), "-_;:,");
}
@Override
public void startTest(ProgressMonitor monitor) {
super.startTest(monitor);
checkKeys = Main.pref.getBoolean(PREF_CHECK_KEYS, true);
if (isBeforeUpload) {
checkKeys = checkKeys && Main.pref.getBoolean(PREF_CHECK_KEYS_BEFORE_UPLOAD, true);
}
checkValues = Main.pref.getBoolean(PREF_CHECK_VALUES, true);
if (isBeforeUpload) {
checkValues = checkValues && Main.pref.getBoolean(PREF_CHECK_VALUES_BEFORE_UPLOAD, true);
}
checkComplex = Main.pref.getBoolean(PREF_CHECK_COMPLEX, true);
if (isBeforeUpload) {
checkComplex = checkComplex && Main.pref.getBoolean(PREF_CHECK_COMPLEX_BEFORE_UPLOAD, true);
}
checkFixmes = Main.pref.getBoolean(PREF_CHECK_FIXMES, true);
if (isBeforeUpload) {
checkFixmes = checkFixmes && Main.pref.getBoolean(PREF_CHECK_FIXMES_BEFORE_UPLOAD, true);
}
}
@Override
public void visit(Collection<OsmPrimitive> selection) {
if (checkKeys || checkValues || checkComplex || checkFixmes) {
super.visit(selection);
}
}
@Override
public void addGui(JPanel testPanel) {
GBC a = GBC.eol();
a.anchor = GridBagConstraints.EAST;
testPanel.add(new JLabel(name+" :"), GBC.eol().insets(3, 0, 0, 0));
prefCheckKeys = new JCheckBox(tr("Check property keys."), Main.pref.getBoolean(PREF_CHECK_KEYS, true));
prefCheckKeys.setToolTipText(tr("Validate that property keys are valid checking against list of words."));
testPanel.add(prefCheckKeys, GBC.std().insets(20, 0, 0, 0));
prefCheckKeysBeforeUpload = new JCheckBox();
prefCheckKeysBeforeUpload.setSelected(Main.pref.getBoolean(PREF_CHECK_KEYS_BEFORE_UPLOAD, true));
testPanel.add(prefCheckKeysBeforeUpload, a);
prefCheckComplex = new JCheckBox(tr("Use complex property checker."), Main.pref.getBoolean(PREF_CHECK_COMPLEX, true));
prefCheckComplex.setToolTipText(tr("Validate property values and tags using complex rules."));
testPanel.add(prefCheckComplex, GBC.std().insets(20, 0, 0, 0));
prefCheckComplexBeforeUpload = new JCheckBox();
prefCheckComplexBeforeUpload.setSelected(Main.pref.getBoolean(PREF_CHECK_COMPLEX_BEFORE_UPLOAD, true));
testPanel.add(prefCheckComplexBeforeUpload, a);
final Collection<String> sources = Main.pref.getCollection(PREF_SOURCES, DEFAULT_SOURCES);
sourcesList = new EditableList(tr("TagChecker source"));
sourcesList.setItems(sources);
testPanel.add(new JLabel(tr("Data sources ({0})", "*.cfg")), GBC.eol().insets(23, 0, 0, 0));
testPanel.add(sourcesList, GBC.eol().fill(GridBagConstraints.HORIZONTAL).insets(23, 0, 0, 0));
ActionListener disableCheckActionListener = e -> handlePrefEnable();
prefCheckKeys.addActionListener(disableCheckActionListener);
prefCheckKeysBeforeUpload.addActionListener(disableCheckActionListener);
prefCheckComplex.addActionListener(disableCheckActionListener);
prefCheckComplexBeforeUpload.addActionListener(disableCheckActionListener);
handlePrefEnable();
prefCheckValues = new JCheckBox(tr("Check property values."), Main.pref.getBoolean(PREF_CHECK_VALUES, true));
prefCheckValues.setToolTipText(tr("Validate that property values are valid checking against presets."));
testPanel.add(prefCheckValues, GBC.std().insets(20, 0, 0, 0));
prefCheckValuesBeforeUpload = new JCheckBox();
prefCheckValuesBeforeUpload.setSelected(Main.pref.getBoolean(PREF_CHECK_VALUES_BEFORE_UPLOAD, true));
testPanel.add(prefCheckValuesBeforeUpload, a);
prefCheckFixmes = new JCheckBox(tr("Check for FIXMES."), Main.pref.getBoolean(PREF_CHECK_FIXMES, true));
prefCheckFixmes.setToolTipText(tr("Looks for nodes or ways with FIXME in any property value."));
testPanel.add(prefCheckFixmes, GBC.std().insets(20, 0, 0, 0));
prefCheckFixmesBeforeUpload = new JCheckBox();
prefCheckFixmesBeforeUpload.setSelected(Main.pref.getBoolean(PREF_CHECK_FIXMES_BEFORE_UPLOAD, true));
testPanel.add(prefCheckFixmesBeforeUpload, a);
}
public void handlePrefEnable() {
boolean selected = prefCheckKeys.isSelected() || prefCheckKeysBeforeUpload.isSelected()
|| prefCheckComplex.isSelected() || prefCheckComplexBeforeUpload.isSelected();
sourcesList.setEnabled(selected);
}
@Override
public boolean ok() {
enabled = prefCheckKeys.isSelected() || prefCheckValues.isSelected() || prefCheckComplex.isSelected() || prefCheckFixmes.isSelected();
testBeforeUpload = prefCheckKeysBeforeUpload.isSelected() || prefCheckValuesBeforeUpload.isSelected()
|| prefCheckFixmesBeforeUpload.isSelected() || prefCheckComplexBeforeUpload.isSelected();
Main.pref.put(PREF_CHECK_VALUES, prefCheckValues.isSelected());
Main.pref.put(PREF_CHECK_COMPLEX, prefCheckComplex.isSelected());
Main.pref.put(PREF_CHECK_KEYS, prefCheckKeys.isSelected());
Main.pref.put(PREF_CHECK_FIXMES, prefCheckFixmes.isSelected());
Main.pref.put(PREF_CHECK_VALUES_BEFORE_UPLOAD, prefCheckValuesBeforeUpload.isSelected());
Main.pref.put(PREF_CHECK_COMPLEX_BEFORE_UPLOAD, prefCheckComplexBeforeUpload.isSelected());
Main.pref.put(PREF_CHECK_KEYS_BEFORE_UPLOAD, prefCheckKeysBeforeUpload.isSelected());
Main.pref.put(PREF_CHECK_FIXMES_BEFORE_UPLOAD, prefCheckFixmesBeforeUpload.isSelected());
return Main.pref.putCollection(PREF_SOURCES, sourcesList.getItems());
}
@Override
public Command fixError(TestError testError) {
List<Command> commands = new ArrayList<>(50);
Collection<? extends OsmPrimitive> primitives = testError.getPrimitives();
for (OsmPrimitive p : primitives) {
Map<String, String> tags = p.getKeys();
if (tags.isEmpty()) {
continue;
}
for (Entry<String, String> prop: tags.entrySet()) {
String key = prop.getKey();
String value = prop.getValue();
if (value == null || value.trim().isEmpty()) {
commands.add(new ChangePropertyCommand(p, key, null));
} else if (value.startsWith(" ") || value.endsWith(" ") || value.contains(" ")) {
commands.add(new ChangePropertyCommand(p, key, Tag.removeWhiteSpaces(value)));
} else if (key.startsWith(" ") || key.endsWith(" ") || key.contains(" ")) {
commands.add(new ChangePropertyKeyCommand(p, key, Tag.removeWhiteSpaces(key)));
} else {
String evalue = Entities.unescape(value);
if (!evalue.equals(value)) {
commands.add(new ChangePropertyCommand(p, key, evalue));
}
}
}
}
if (commands.isEmpty())
return null;
if (commands.size() == 1)
return commands.get(0);
return new SequenceCommand(tr("Fix tags"), commands);
}
@Override
public boolean isFixable(TestError testError) {
if (testError.getTester() instanceof TagChecker) {
int code = testError.getCode();
return code == INVALID_KEY || code == EMPTY_VALUES || code == INVALID_SPACE ||
code == INVALID_KEY_SPACE || code == INVALID_HTML || code == MISSPELLED_VALUE ||
code == MULTIPLE_SPACES;
}
return false;
}
protected static class CheckerData {
private String description;
protected List<CheckerElement> data = new ArrayList<>();
private OsmPrimitiveType type;
private int code;
protected Severity severity;
// CHECKSTYLE.OFF: SingleSpaceSeparator
protected static final int TAG_CHECK_ERROR = 1250;
protected static final int TAG_CHECK_WARN = 1260;
protected static final int TAG_CHECK_INFO = 1270;
// CHECKSTYLE.ON: SingleSpaceSeparator
protected static class CheckerElement {
public Object tag;
public Object value;
public boolean noMatch;
public boolean tagAll;
public boolean valueAll;
public boolean valueBool;
private static Pattern getPattern(String str) {
if (str.endsWith("/i"))
return Pattern.compile(str.substring(1, str.length()-2), Pattern.CASE_INSENSITIVE);
if (str.endsWith("/"))
return Pattern.compile(str.substring(1, str.length()-1));
throw new IllegalStateException();
}
public CheckerElement(String exp) {
Matcher m = Pattern.compile("(.+)([!=]=)(.+)").matcher(exp);
m.matches();
String n = m.group(1).trim();
if ("*".equals(n)) {
tagAll = true;
} else {
tag = n.startsWith("/") ? getPattern(n) : n;
noMatch = "!=".equals(m.group(2));
n = m.group(3).trim();
if ("*".equals(n)) {
valueAll = true;
} else if ("BOOLEAN_TRUE".equals(n)) {
valueBool = true;
value = OsmUtils.trueval;
} else if ("BOOLEAN_FALSE".equals(n)) {
valueBool = true;
value = OsmUtils.falseval;
} else {
value = n.startsWith("/") ? getPattern(n) : n;
}
}
}
public boolean match(Map<String, String> keys) {
for (Entry<String, String> prop: keys.entrySet()) {
String key = prop.getKey();
String val = valueBool ? OsmUtils.getNamedOsmBoolean(prop.getValue()) : prop.getValue();
if ((tagAll || (tag instanceof Pattern ? ((Pattern) tag).matcher(key).matches() : key.equals(tag)))
&& (valueAll || (value instanceof Pattern ? ((Pattern) value).matcher(val).matches() : val.equals(value))))
return !noMatch;
}
return noMatch;
}
}
private static final Pattern CLEAN_STR_PATTERN = Pattern.compile(" *# *([^#]+) *$");
private static final Pattern SPLIT_TRIMMED_PATTERN = Pattern.compile(" *: *");
private static final Pattern SPLIT_ELEMENTS_PATTERN = Pattern.compile(" *&& *");
public String getData(final String str) {
Matcher m = CLEAN_STR_PATTERN.matcher(str);
String trimmed = m.replaceFirst("").trim();
try {
description = m.group(1);
if (description != null && description.isEmpty()) {
description = null;
}
} catch (IllegalStateException e) {
Main.error(e);
description = null;
}
String[] n = SPLIT_TRIMMED_PATTERN.split(trimmed, 3);
switch (n[0]) {
case "way":
type = OsmPrimitiveType.WAY;
break;
case "node":
type = OsmPrimitiveType.NODE;
break;
case "relation":
type = OsmPrimitiveType.RELATION;
break;
case "*":
type = null;
break;
default:
return tr("Could not find element type");
}
if (n.length != 3)
return tr("Incorrect number of parameters");
switch (n[1]) {
case "W":
severity = Severity.WARNING;
code = TAG_CHECK_WARN;
break;
case "E":
severity = Severity.ERROR;
code = TAG_CHECK_ERROR;
break;
case "I":
severity = Severity.OTHER;
code = TAG_CHECK_INFO;
break;
default:
return tr("Could not find warning level");
}
for (String exp: SPLIT_ELEMENTS_PATTERN.split(n[2])) {
try {
data.add(new CheckerElement(exp));
} catch (IllegalStateException e) {
Main.trace(e);
return tr("Illegal expression ''{0}''", exp);
} catch (PatternSyntaxException e) {
Main.trace(e);
return tr("Illegal regular expression ''{0}''", exp);
}
}
return null;
}
public boolean match(OsmPrimitive osm, Map<String, String> keys) {
if (type != null && OsmPrimitiveType.from(osm) != type)
return false;
for (CheckerElement ce : data) {
if (!ce.match(keys))
return false;
}
return true;
}
public String getDescription() {
return description;
}
public Severity getSeverity() {
return severity;
}
public int getCode() {
if (type == null)
return code;
return code + type.ordinal() + 1;
}
}
}