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.data.osm.OsmPrimitive; 016import org.openstreetmap.josm.data.validation.Severity; 017import org.openstreetmap.josm.data.validation.Test; 018import org.openstreetmap.josm.data.validation.TestError; 019import org.openstreetmap.josm.tools.LanguageInfo; 020import org.openstreetmap.josm.tools.Logging; 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", "restriction", "interval")); 033 private static final Set<String> RESTRICTION_VALUES = new HashSet<>(Arrays.asList("yes", "official", "designated", "destination", 034 "delivery", "customers", "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 "hgv_articulated", "ski:nordic", "ski:alpine", "ski:telemark", "coach", "golf_cart" 039 /*,"minibus","share_taxi","hov","car_sharing","emergency","hazmat","disabled"*/)); 040 041 private static final Pattern CONDITIONAL_PATTERN; 042 static { 043 final String part = Pattern.compile("([^@\\p{Space}][^@]*?)" 044 + "\\s*@\\s*" + "(\\([^)\\p{Space}][^)]+?\\)|[^();\\p{Space}][^();]*?)\\s*").toString(); 045 CONDITIONAL_PATTERN = Pattern.compile('(' + part + ")(;\\s*" + part + ")*"); 046 } 047 048 /** 049 * Constructs a new {@code ConditionalKeys}. 050 */ 051 public ConditionalKeys() { 052 super(tr("Conditional Keys"), tr("Tests for the correct usage of ''*:conditional'' tags.")); 053 } 054 055 @Override 056 public void initialize() throws Exception { 057 super.initialize(); 058 openingHourTest.initialize(); 059 } 060 061 /** 062 * Check if the key is a key for an access restriction 063 * @param part The key (or the restriction part of it, e.g. for lanes) 064 * @return <code>true</code> if it is a restriction 065 */ 066 public static boolean isRestrictionType(String part) { 067 return RESTRICTION_TYPES.contains(part); 068 } 069 070 /** 071 * Check if the value is a valid restriction value 072 * @param part The value 073 * @return <code>true</code> for allowed restriction values 074 */ 075 public static boolean isRestrictionValue(String part) { 076 return RESTRICTION_VALUES.contains(part); 077 } 078 079 /** 080 * Checks if the key denotes a 081 * <a href="http://wiki.openstreetmap.org/wiki/Key:access#Transport_mode_restrictions">transport access mode restriction</a> 082 * @param part The key (or the restriction part of it, e.g. for lanes) 083 * @return <code>true</code> if it is a restriction 084 */ 085 public static boolean isTransportationMode(String part) { 086 return TRANSPORT_MODES.contains(part); 087 } 088 089 /** 090 * Check if a key part is a valid direction 091 * @param part The part of the key 092 * @return <code>true</code> if it is a direction 093 */ 094 public static boolean isDirection(String part) { 095 return "forward".equals(part) || "backward".equals(part); 096 } 097 098 /** 099 * Checks if a given key is a valid access key 100 * @param key The conditional key 101 * @return <code>true</code> if the key is valid 102 */ 103 public boolean isKeyValid(String key) { 104 // <restriction-type>[:<transportation mode>][:<direction>]:conditional 105 // -- or -- <transportation mode> [:<direction>]:conditional 106 if (!key.endsWith(":conditional")) { 107 return false; 108 } 109 final String[] parts = key.replace(":conditional", "").split(":"); 110 return isKeyValid3Parts(parts) || isKeyValid1Part(parts) || isKeyValid2Parts(parts); 111 } 112 113 private static boolean isKeyValid3Parts(String... parts) { 114 return parts.length == 3 && isRestrictionType(parts[0]) && isTransportationMode(parts[1]) && isDirection(parts[2]); 115 } 116 117 private static boolean isKeyValid2Parts(String... parts) { 118 return parts.length == 2 && ((isRestrictionType(parts[0]) && (isTransportationMode(parts[1]) || isDirection(parts[1]))) 119 || (isTransportationMode(parts[0]) && isDirection(parts[1]))); 120 } 121 122 private static boolean isKeyValid1Part(String... parts) { 123 return parts.length == 1 && (isRestrictionType(parts[0]) || isTransportationMode(parts[0])); 124 } 125 126 /** 127 * Check if a value is valid 128 * @param key The key the value is for 129 * @param value The value 130 * @return <code>true</code> if it is valid 131 */ 132 public boolean isValueValid(String key, String value) { 133 return validateValue(key, value) == null; 134 } 135 136 static class ConditionalParsingException extends RuntimeException { 137 ConditionalParsingException(String message) { 138 super(message); 139 } 140 } 141 142 /** 143 * A conditional value is a value for the access restriction tag that depends on conditions (time, ...) 144 */ 145 public static class ConditionalValue { 146 /** 147 * The value the tag should have if the condition matches 148 */ 149 public final String restrictionValue; 150 /** 151 * The conditions for {@link #restrictionValue} 152 */ 153 public final Collection<String> conditions; 154 155 /** 156 * Create a new {@link ConditionalValue} 157 * @param restrictionValue The value the tag should have if the condition matches 158 * @param conditions The conditions for that value 159 */ 160 public ConditionalValue(String restrictionValue, Collection<String> conditions) { 161 this.restrictionValue = restrictionValue; 162 this.conditions = conditions; 163 } 164 165 /** 166 * Parses the condition values as string. 167 * @param value value, must match {@code <restriction-value> @ <condition>[;<restriction-value> @ <condition>]} pattern 168 * @return list of {@code ConditionalValue}s 169 * @throws ConditionalParsingException if {@code value} does not match expected pattern 170 */ 171 public static List<ConditionalValue> parse(String value) { 172 // <restriction-value> @ <condition>[;<restriction-value> @ <condition>] 173 final List<ConditionalValue> r = new ArrayList<>(); 174 final Matcher m = CONDITIONAL_PATTERN.matcher(value); 175 if (!m.matches()) { 176 throw new ConditionalParsingException(tr("Does not match pattern ''restriction value @ condition''")); 177 } else { 178 int i = 2; 179 while (i + 1 <= m.groupCount() && m.group(i + 1) != null) { 180 final String restrictionValue = m.group(i); 181 final String[] conditions = m.group(i + 1).replace("(", "").replace(")", "").split("\\s+(AND|and)\\s+"); 182 r.add(new ConditionalValue(restrictionValue, Arrays.asList(conditions))); 183 i += 3; 184 } 185 } 186 return r; 187 } 188 } 189 190 /** 191 * Validate a key/value pair 192 * @param key The key 193 * @param value The value 194 * @return The error message for that value or <code>null</code> to indicate valid 195 */ 196 public String validateValue(String key, String value) { 197 try { 198 for (final ConditionalValue conditional : ConditionalValue.parse(value)) { 199 // validate restriction value 200 if (isTransportationMode(key.split(":")[0]) && !isRestrictionValue(conditional.restrictionValue)) { 201 return tr("{0} is not a valid restriction value", conditional.restrictionValue); 202 } 203 // validate opening hour if the value contains an hour (heuristic) 204 for (final String condition : conditional.conditions) { 205 if (condition.matches(".*[0-9]:[0-9]{2}.*")) { 206 final List<OpeningHourTest.OpeningHoursTestError> errors = openingHourTest.checkOpeningHourSyntax( 207 "", condition, OpeningHourTest.CheckMode.TIME_RANGE, true, LanguageInfo.getJOSMLocaleCode()); 208 if (!errors.isEmpty()) { 209 return errors.get(0).getMessage(); 210 } 211 } 212 } 213 } 214 } catch (ConditionalParsingException ex) { 215 Logging.debug(ex); 216 return ex.getMessage(); 217 } 218 return null; 219 } 220 221 /** 222 * Validate a primitive 223 * @param p The primitive 224 * @return The errors for that primitive or an empty list if there are no errors. 225 */ 226 public List<TestError> validatePrimitive(OsmPrimitive p) { 227 final List<TestError> errors = new ArrayList<>(); 228 for (final String key : SubclassFilteredCollection.filter(p.keySet(), 229 Pattern.compile(":conditional(:.*)?$").asPredicate())) { 230 if (!isKeyValid(key)) { 231 errors.add(TestError.builder(this, Severity.WARNING, 3201) 232 .message(tr("Wrong syntax in {0} key", key)) 233 .primitives(p) 234 .build()); 235 continue; 236 } 237 final String value = p.get(key); 238 final String error = validateValue(key, value); 239 if (error != null) { 240 errors.add(TestError.builder(this, Severity.WARNING, 3202) 241 .message(tr("Error in {0} value: {1}", key, error)) 242 .primitives(p) 243 .build()); 244 } 245 } 246 return errors; 247 } 248 249 @Override 250 public void check(OsmPrimitive p) { 251 if (p.isTagged()) { 252 errors.addAll(validatePrimitive(p)); 253 } 254 } 255}