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 Set<String> names = p.getKeys().entrySet().stream() 081 .filter(e -> e.getValue() != null && e.getKey().startsWith("name:") && !EXCLUSIONS.contains(e.getKey())) 082 .map(Entry::getValue) 083 .collect(Collectors.toSet()); 084 085 if (names.isEmpty()) return; 086 087 String name = p.get("name"); 088 089 if (name == null) { 090 errors.add(TestError.builder(this, Severity.OTHER, NAME_MISSING) 091 .message(tr("A name is missing, even though name:* exists.")) 092 .primitives(p) 093 .build()); 094 return; 095 } 096 097 if (names.contains(name)) return; 098 /* If name is not equal to one of the name:*, it should be a 099 composition of some (not necessarily all) name:* labels. 100 Check if this is the case. */ 101 102 String[] splitNames = NAME_SPLIT_PATTERN.split(name); 103 if (splitNames.length == 1) { 104 /* The name is not composed of multiple parts. Complain. */ 105 missingTranslation(p, splitNames[0]); 106 return; 107 } 108 109 /* Check that each part corresponds to a translated name:*. */ 110 for (String n : splitNames) { 111 if (!names.contains(n)) { 112 missingTranslation(p, n); 113 } 114 } 115 } 116}