001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.data.validation.tests; 003 004import static org.openstreetmap.josm.tools.I18n.tr; 005 006import java.util.ArrayList; 007import java.util.Arrays; 008import java.util.Collection; 009import java.util.HashSet; 010import java.util.List; 011import java.util.Set; 012import java.util.regex.Matcher; 013import java.util.regex.Pattern; 014 015import org.openstreetmap.josm.Main; 016import org.openstreetmap.josm.data.osm.OsmPrimitive; 017import org.openstreetmap.josm.data.validation.Severity; 018import org.openstreetmap.josm.data.validation.Test; 019import org.openstreetmap.josm.data.validation.TestError; 020import org.openstreetmap.josm.tools.LanguageInfo; 021import org.openstreetmap.josm.tools.SubclassFilteredCollection; 022 023/** 024 * Checks for <a href="http://wiki.openstreetmap.org/wiki/Conditional_restrictions">conditional restrictions</a> 025 * @since 6605 026 */ 027public class ConditionalKeys extends Test.TagTest { 028 029 private final OpeningHourTest openingHourTest = new OpeningHourTest(); 030 private static final Set<String> RESTRICTION_TYPES = new HashSet<>(Arrays.asList("oneway", "toll", "noexit", "maxspeed", "minspeed", 031 "maxstay", "maxweight", "maxaxleload", "maxheight", "maxwidth", "maxlength", "overtaking", "maxgcweight", "maxgcweightrating", 032 "fee")); 033 private static final Set<String> RESTRICTION_VALUES = new HashSet<>(Arrays.asList("yes", "official", "designated", "destination", 034 "delivery", "permissive", "private", "agricultural", "forestry", "no")); 035 private static final Set<String> TRANSPORT_MODES = new HashSet<>(Arrays.asList("access", "foot", "ski", "inline_skates", "ice_skates", 036 "horse", "vehicle", "bicycle", "carriage", "trailer", "caravan", "motor_vehicle", "motorcycle", "moped", "mofa", 037 "motorcar", "motorhome", "psv", "bus", "taxi", "tourist_bus", "goods", "hgv", "agricultural", "atv", "snowmobile" 038 /*,"hov","emergency","hazmat","disabled"*/)); 039 040 /** 041 * Constructs a new {@code ConditionalKeys}. 042 */ 043 public ConditionalKeys() { 044 super(tr("Conditional Keys"), tr("Tests for the correct usage of ''*:conditional'' tags.")); 045 } 046 047 @Override 048 public void initialize() throws Exception { 049 super.initialize(); 050 openingHourTest.initialize(); 051 } 052 053 public static boolean isRestrictionType(String part) { 054 return RESTRICTION_TYPES.contains(part); 055 } 056 057 public static boolean isRestrictionValue(String part) { 058 return RESTRICTION_VALUES.contains(part); 059 } 060 061 public static boolean isTransportationMode(String part) { 062 // http://wiki.openstreetmap.org/wiki/Key:access#Transport_mode_restrictions 063 return TRANSPORT_MODES.contains(part); 064 } 065 066 public static boolean isDirection(String part) { 067 return "forward".equals(part) || "backward".equals(part); 068 } 069 070 public boolean isKeyValid(String key) { 071 // <restriction-type>[:<transportation mode>][:<direction>]:conditional 072 // -- or -- <transportation mode> [:<direction>]:conditional 073 if (!key.endsWith(":conditional")) { 074 return false; 075 } 076 final String[] parts = key.replaceAll(":conditional", "").split(":"); 077 return isKeyValid3Parts(parts) || isKeyValid1Part(parts) || isKeyValid2Parts(parts); 078 } 079 080 private static boolean isKeyValid3Parts(String ... parts) { 081 return parts.length == 3 && isRestrictionType(parts[0]) && isTransportationMode(parts[1]) && isDirection(parts[2]); 082 } 083 084 private static boolean isKeyValid2Parts(String ... parts) { 085 return parts.length == 2 && ((isRestrictionType(parts[0]) && (isTransportationMode(parts[1]) || isDirection(parts[1]))) 086 || (isTransportationMode(parts[0]) && isDirection(parts[1]))); 087 } 088 089 private static boolean isKeyValid1Part(String ... parts) { 090 return parts.length == 1 && (isRestrictionType(parts[0]) || isTransportationMode(parts[0])); 091 } 092 093 public boolean isValueValid(String key, String value) { 094 return validateValue(key, value) == null; 095 } 096 097 static class ConditionalParsingException extends RuntimeException { 098 ConditionalParsingException(String message) { 099 super(message); 100 } 101 } 102 103 public static class ConditionalValue { 104 public final String restrictionValue; 105 public final Collection<String> conditions; 106 107 public ConditionalValue(String restrictionValue, Collection<String> conditions) { 108 this.restrictionValue = restrictionValue; 109 this.conditions = conditions; 110 } 111 112 /** 113 * Parses the condition values as string. 114 * @param value value, must match {@code <restriction-value> @ <condition>[;<restriction-value> @ <condition>]} pattern 115 * @return list of {@code ConditionalValue}s 116 * @throws ConditionalParsingException if {@code value} does not match expected pattern 117 */ 118 public static List<ConditionalValue> parse(String value) { 119 // <restriction-value> @ <condition>[;<restriction-value> @ <condition>] 120 final List<ConditionalValue> r = new ArrayList<>(); 121 final String part = Pattern.compile("([^@\\p{Space}][^@]*?)" 122 + "\\s*@\\s*" + "(\\([^)\\p{Space}][^)]+?\\)|[^();\\p{Space}][^();]*?)\\s*").toString(); 123 final Matcher m = Pattern.compile('(' + part + ")(;\\s*" + part + ")*").matcher(value); 124 if (!m.matches()) { 125 throw new ConditionalParsingException(tr("Does not match pattern ''restriction value @ condition''")); 126 } else { 127 int i = 2; 128 while (i + 1 <= m.groupCount() && m.group(i + 1) != null) { 129 final String restrictionValue = m.group(i); 130 final String[] conditions = m.group(i + 1).replace("(", "").replace(")", "").split("\\s+(AND|and)\\s+"); 131 r.add(new ConditionalValue(restrictionValue, Arrays.asList(conditions))); 132 i += 3; 133 } 134 } 135 return r; 136 } 137 } 138 139 public String validateValue(String key, String value) { 140 try { 141 for (final ConditionalValue conditional : ConditionalValue.parse(value)) { 142 // validate restriction value 143 if (isTransportationMode(key.split(":")[0]) && !isRestrictionValue(conditional.restrictionValue)) { 144 return tr("{0} is not a valid restriction value", conditional.restrictionValue); 145 } 146 // validate opening hour if the value contains an hour (heuristic) 147 for (final String condition : conditional.conditions) { 148 if (condition.matches(".*[0-9]:[0-9]{2}.*")) { 149 final List<OpeningHourTest.OpeningHoursTestError> errors = openingHourTest.checkOpeningHourSyntax( 150 "", condition, OpeningHourTest.CheckMode.TIME_RANGE, true, LanguageInfo.getJOSMLocaleCode()); 151 if (!errors.isEmpty()) { 152 return errors.get(0).getMessage(); 153 } 154 } 155 } 156 } 157 } catch (ConditionalParsingException ex) { 158 Main.debug(ex); 159 return ex.getMessage(); 160 } 161 return null; 162 } 163 164 public List<TestError> validatePrimitive(OsmPrimitive p) { 165 final List<TestError> errors = new ArrayList<>(); 166 for (final String key : SubclassFilteredCollection.filter(p.keySet(), 167 Pattern.compile(":conditional(:.*)?$").asPredicate())) { 168 if (!isKeyValid(key)) { 169 errors.add(TestError.builder(this, Severity.WARNING, 3201) 170 .message(tr("Wrong syntax in {0} key", key)) 171 .primitives(p) 172 .build()); 173 continue; 174 } 175 final String value = p.get(key); 176 final String error = validateValue(key, value); 177 if (error != null) { 178 errors.add(TestError.builder(this, Severity.WARNING, 3202) 179 .message(tr("Error in {0} value: {1}", key, error)) 180 .primitives(p) 181 .build()); 182 } 183 } 184 return errors; 185 } 186 187 @Override 188 public void check(OsmPrimitive p) { 189 errors.addAll(validatePrimitive(p)); 190 } 191}