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