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