Ticket #21163: ConditionalKeys.java

File ConditionalKeys.java, 12.4 KB (added by DodalerAfenger, 3 years ago)

org.openstreetmap.josm.data.validation.tests

Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.data.validation.tests;
3
4import static org.openstreetmap.josm.tools.I18n.tr;
5
6import java.util.ArrayList;
7import java.util.Arrays;
8import java.util.Collection;
9import java.util.HashSet;
10import java.util.List;
11import java.util.Set;
12import java.util.regex.Matcher;
13import java.util.regex.Pattern;
14import java.util.stream.Collectors;
15
16import org.openstreetmap.josm.data.osm.OsmPrimitive;
17import org.openstreetmap.josm.data.validation.Severity;
18import org.openstreetmap.josm.data.validation.Test;
19import org.openstreetmap.josm.data.validation.TestError;
20import org.openstreetmap.josm.tools.Logging;
21
22/**
23 * Checks for <a href="http://wiki.openstreetmap.org/wiki/Conditional_restrictions">conditional restrictions</a>
24 * @since 6605
25 */
26public class ConditionalKeys extends Test.TagTest {
27
28 private final OpeningHourTest openingHourTest = new OpeningHourTest();
29 private static final Set<String> RESTRICTION_TYPES = new HashSet<>(Arrays.asList("oneway", "toll", "noexit", "maxspeed", "minspeed",
30 "maxstay", "maxweight", "maxaxleload", "maxheight", "maxwidth", "maxlength", "overtaking", "maxgcweight", "maxgcweightrating",
31 "fee", "restriction", "interval", "duration", "dog"));
32 private static final Set<String> RESTRICTION_VALUES = new HashSet<>(Arrays.asList("yes", "official", "designated", "destination",
33 "delivery", "customers", "permissive", "private", "agricultural", "forestry", "no"));
34 private static final Set<String> TRANSPORT_MODES = new HashSet<>(Arrays.asList("access", "foot", "ski", "inline_skates", "ice_skates",
35 "horse", "vehicle", "bicycle", "carriage", "trailer", "caravan", "motor_vehicle", "motorcycle", "moped", "mofa",
36 "motorcar", "motorhome", "psv", "bus", "taxi", "tourist_bus", "goods", "hgv", "agricultural", "atv", "snowmobile",
37 "hgv_articulated", "ski:nordic", "ski:alpine", "ski:telemark", "coach", "golf_cart"
38 /*,"minibus","share_taxi","hov","car_sharing","emergency","hazmat","disabled"*/));
39
40 private static final Pattern CONDITIONAL_PATTERN;
41 static {
42 final String part = Pattern.compile("([^@\\p{Space}][^@]*?)"
43 + "\\s*@\\s*" + "(\\([^)\\p{Space}][^)]+?\\)|[^();\\p{Space}][^();]*?)\\s*").toString();
44 CONDITIONAL_PATTERN = Pattern.compile('(' + part + ")(;\\s*" + part + ")*");
45 }
46
47 /**
48 * Constructs a new {@code ConditionalKeys}.
49 */
50 public ConditionalKeys() {
51 super(tr("Conditional Keys"), tr("Tests for the correct usage of ''*:conditional'' tags."));
52 }
53
54 @Override
55 public void initialize() throws Exception {
56 super.initialize();
57 openingHourTest.initialize();
58 }
59
60 /**
61 * Check if the key is a key for an access restriction
62 * @param part The key (or the restriction part of it, e.g. for lanes)
63 * @return <code>true</code> if it is a restriction
64 */
65 public static boolean isRestrictionType(String part) {
66 return RESTRICTION_TYPES.contains(part);
67 }
68
69 /**
70 * Check if the value is a valid restriction value
71 * @param part The value
72 * @return <code>true</code> for allowed restriction values
73 */
74 public static boolean isRestrictionValue(String part) {
75 return RESTRICTION_VALUES.contains(part);
76 }
77
78 /**
79 * Check if the value is a valid restriction value
80 * @param part The value
81 * @param hasLanes key has :lanes included
82 * @return <code>true</code> for allowed restriction values
83 */
84 public static boolean isRestrictionValueLanes(String part, Boolean hasLanes) {
85 if (hasLanes) {
86 final Set<Boolean> check =
87 Arrays.stream(part.split(tr("\\|")))
88 .map(restrictionValue -> isRestrictionValue(restrictionValue))
89 .collect(Collectors.toSet());
90 return check.contains(false) ? false : true;
91 }
92 else {
93 return isRestrictionValue(part);
94 }
95 }
96
97 /**
98 * Checks if the key denotes a
99 * <a href="http://wiki.openstreetmap.org/wiki/Key:access#Transport_mode_restrictions">transport access mode restriction</a>
100 * @param part The key (or the restriction part of it, e.g. for lanes)
101 * @return <code>true</code> if it is a restriction
102 */
103 public static boolean isTransportationMode(String part) {
104 return TRANSPORT_MODES.contains(part);
105 }
106
107 /**
108 * Check if a key part is a valid direction
109 * @param part The part of the key
110 * @return <code>true</code> if it is a direction
111 */
112 public static boolean isDirection(String part) {
113 return "forward".equals(part) || "backward".equals(part);
114 }
115
116 /**
117 * Checks if a given key is a valid access key
118 * @param key The conditional key
119 * @return <code>true</code> if the key is valid
120 */
121 public boolean isKeyValid(String key) {
122 // <restriction-type>[:<transportation mode>][:<direction>]:conditional
123 // -- or -- <transportation mode> [:<direction>]:conditional
124 if (!key.endsWith(":conditional")) {
125 return false;
126 }
127 final String[] parts = key.replace(":conditional", "").split(":", -1);
128
129 /*
130 * Treat cases where "lanes" is used in keys. Simply remove them.
131 */
132 final String[] partsNoLanes = dealWithLanes(parts);
133
134 return isKeyValid3Parts(partsNoLanes) || isKeyValid1Part(partsNoLanes) || isKeyValid2Parts(partsNoLanes);
135 }
136
137 /*
138 * This is not explicitly specified at <a href="https://wiki.openstreetmap.org/wiki/Conditional_restrictions#Tagging">conditional restrictions tagging</a>, but
139 * it's saying below "A conditional Lanes restriction, evaluated per-lane, overrules a non-conditional lanes restriction"
140 * Remove from "parts" using ArrayList. This approach has the advantage that the rest of the checks do not have to be altered.
141 */
142 private static String[] dealWithLanes(String... parts) {
143 List<String> checkLanes = new ArrayList<>();
144 checkLanes.addAll(Arrays.asList(parts));
145 /* lanes may only be present/removed if other keys are used, too. Additionally, they may only be used within the first two positions */
146 if(checkLanes.size() > 1 && checkLanes.indexOf("lanes") <= 1 ){
147 checkLanes.remove("lanes");
148 }
149 return checkLanes.toArray(new String[checkLanes.size()]);
150 }
151
152 private static boolean isKeyValid3Parts(String... parts) {
153 return parts.length == 3 && isRestrictionType(parts[0]) && isTransportationMode(parts[1]) && isDirection(parts[2]);
154 }
155
156 private static boolean isKeyValid2Parts(String... parts) {
157 return parts.length == 2 && ((isRestrictionType(parts[0]) && (isTransportationMode(parts[1]) || isDirection(parts[1])))
158 || (isTransportationMode(parts[0]) && isDirection(parts[1])));
159 }
160
161 private static boolean isKeyValid1Part(String... parts) {
162 return parts.length == 1 && (isRestrictionType(parts[0]) || isTransportationMode(parts[0]));
163 }
164
165 /**
166 * Check if a value is valid
167 * @param key The key the value is for
168 * @param value The value
169 * @return <code>true</code> if it is valid
170 */
171 public boolean isValueValid(String key, String value) {
172 return validateValue(key, value) == null;
173 }
174
175 static class ConditionalParsingException extends RuntimeException {
176 ConditionalParsingException(String message) {
177 super(message);
178 }
179 }
180
181 /**
182 * A conditional value is a value for the access restriction tag that depends on conditions (time, ...)
183 */
184 public static class ConditionalValue {
185 /**
186 * The value the tag should have if the condition matches
187 */
188 public final String restrictionValue;
189 /**
190 * The conditions for {@link #restrictionValue}
191 */
192 public final Collection<String> conditions;
193
194 /**
195 * Create a new {@link ConditionalValue}
196 * @param restrictionValue The value the tag should have if the condition matches
197 * @param conditions The conditions for that value
198 */
199 public ConditionalValue(String restrictionValue, Collection<String> conditions) {
200 this.restrictionValue = restrictionValue;
201 this.conditions = conditions;
202 }
203
204 /**
205 * Parses the condition values as string.
206 * @param value value, must match {@code <restriction-value> @ <condition>[;<restriction-value> @ <condition>]} pattern
207 * @return list of {@code ConditionalValue}s
208 * @throws ConditionalParsingException if {@code value} does not match expected pattern
209 */
210 public static List<ConditionalValue> parse(String value) {
211 // <restriction-value> @ <condition>[;<restriction-value> @ <condition>]
212 final List<ConditionalValue> r = new ArrayList<>();
213 final Matcher m = CONDITIONAL_PATTERN.matcher(value);
214 if (!m.matches()) {
215 throw new ConditionalParsingException(tr("Does not match pattern ''restriction value @ condition''"));
216 } else {
217 int i = 2;
218 while (i + 1 <= m.groupCount() && m.group(i + 1) != null) {
219 final String restrictionValue = m.group(i);
220 final String[] conditions = m.group(i + 1).replace("(", "").replace(")", "").split("\\s+(AND|and)\\s+", -1);
221 r.add(new ConditionalValue(restrictionValue, Arrays.asList(conditions)));
222 i += 3;
223 }
224 }
225 return r;
226 }
227 }
228
229 /**
230 * Validate a key/value pair
231 * @param key The key
232 * @param value The value
233 * @return The error message for that value or <code>null</code> to indicate valid
234 */
235 public String validateValue(String key, String value) {
236 try {
237 for (final ConditionalValue conditional : ConditionalValue.parse(value)) {
238 // validate restriction value
239 if (isTransportationMode(key.split(":", -1)[0]) && !isRestrictionValueLanes(conditional.restrictionValue, key.contains(tr(":lanes")))) {
240 return tr("{0} is not a valid restriction value", conditional.restrictionValue);
241 }
242 // validate opening hour if the value contains an hour (heuristic)
243 for (final String condition : conditional.conditions) {
244 if (condition.matches(".*[0-9]:[0-9]{2}.*")) {
245 final List<TestError> errors = openingHourTest.checkOpeningHourSyntax("", condition);
246 if (!errors.isEmpty()) {
247 return errors.get(0).getDescription();
248 }
249 }
250 }
251 }
252 } catch (ConditionalParsingException ex) {
253 Logging.debug(ex);
254 return ex.getMessage();
255 }
256 return null;
257 }
258
259 /**
260 * Validate a primitive
261 * @param p The primitive
262 * @return The errors for that primitive or an empty list if there are no errors.
263 */
264 public List<TestError> validatePrimitive(OsmPrimitive p) {
265 final List<TestError> errors = new ArrayList<>();
266 final Pattern pattern = Pattern.compile(":conditional(:.*)?$");
267 p.visitKeys((primitive, key, value) -> {
268 if (!pattern.matcher(key).find()) {
269 return;
270 }
271 if (!isKeyValid(key)) {
272 errors.add(TestError.builder(this, Severity.WARNING, 3201)
273 .message(tr("Wrong syntax in {0} key", key))
274 .primitives(p)
275 .build());
276 return;
277 }
278 final String error = validateValue(key, value);
279 if (error != null) {
280 errors.add(TestError.builder(this, Severity.WARNING, 3202)
281 .message(tr("Error in {0} value: {1}", key, error))
282 .primitives(p)
283 .build());
284 }
285 });
286 return errors;
287 }
288
289 @Override
290 public void check(OsmPrimitive p) {
291 if (p.isTagged()) {
292 errors.addAll(validatePrimitive(p));
293 }
294 }
295}