001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.data.validation.tests;
003
004import static org.openstreetmap.josm.tools.I18n.marktr;
005import static org.openstreetmap.josm.tools.I18n.tr;
006
007import java.util.HashSet;
008import java.util.Set;
009import java.util.regex.Pattern;
010
011import org.openstreetmap.josm.data.osm.OsmPrimitive;
012import org.openstreetmap.josm.data.validation.Severity;
013import org.openstreetmap.josm.data.validation.Test;
014import org.openstreetmap.josm.data.validation.TestError;
015
016/**
017 * Check for missing name:* translations.
018 * <p>
019 * This test finds multilingual objects whose 'name' attribute is not
020 * equal to any 'name:*' attribute and not a composition of some
021 * 'name:*' attributes separated by ' - '.
022 * <p>
023 * For example, a node with name=Europe, name:de=Europa should have
024 * name:en=Europe to avoid triggering this test.  An object with
025 * name='Suomi - Finland' should have at least name:fi=Suomi and
026 * name:sv=Finland to avoid a warning (name:et=Soome would not
027 * matter).  Also, complain if an object has some name:* attribute but
028 * no name.
029 *
030 * @author Skela
031 */
032public class NameMismatch extends Test.TagTest {
033    protected static final int NAME_MISSING = 1501;
034    protected static final int NAME_TRANSLATION_MISSING = 1502;
035    private static final Pattern NAME_SPLIT_PATTERN = Pattern.compile(" - ");
036
037    /**
038     * Constructs a new {@code NameMismatch} test.
039     */
040    public NameMismatch() {
041        super(tr("Missing name:* translation"),
042            tr("This test finds multilingual objects whose ''name'' attribute is not equal to some ''name:*'' attribute " +
043                    "and not a composition of ''name:*'' attributes, e.g., Italia - Italien - Italy."));
044    }
045
046    /**
047     * Report a missing translation.
048     *
049     * @param p The primitive whose translation is missing
050     * @param name The name whose translation is missing
051     */
052    private void missingTranslation(OsmPrimitive p, String name) {
053        errors.add(TestError.builder(this, Severity.OTHER, NAME_TRANSLATION_MISSING)
054                .message(tr("Missing name:* translation"), marktr("Missing name:*={0}. Add tag with correct language key."), name)
055                .primitives(p)
056                .build());
057    }
058
059    /**
060     * Check a primitive for a name mismatch.
061     *
062     * @param p The primitive to be tested
063     */
064    @Override
065    public void check(OsmPrimitive p) {
066        Set<String> names = new HashSet<>();
067
068        p.getKeys().forEach((key, n) -> {
069            if (n != null && key.startsWith("name:") && !"name:etymology:wikidata".equals(key)) {
070                names.add(n);
071            }
072        });
073
074        if (names.isEmpty()) return;
075
076        String name = p.get("name");
077
078        if (name == null) {
079            errors.add(TestError.builder(this, Severity.OTHER, NAME_MISSING)
080                    .message(tr("A name is missing, even though name:* exists."))
081                    .primitives(p)
082                    .build());
083            return;
084        }
085
086        if (names.contains(name)) return;
087        /* If name is not equal to one of the name:*, it should be a
088        composition of some (not necessarily all) name:* labels.
089        Check if this is the case. */
090
091        String[] splitNames = NAME_SPLIT_PATTERN.split(name);
092        if (splitNames.length == 1) {
093            /* The name is not composed of multiple parts. Complain. */
094            missingTranslation(p, splitNames[0]);
095            return;
096        }
097
098        /* Check that each part corresponds to a translated name:*. */
099        for (String n : splitNames) {
100            if (!names.contains(n)) {
101                missingTranslation(p, n);
102            }
103        }
104    }
105}