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