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}