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