1 | // License: GPL. For details, see LICENSE file.
|
---|
2 | package org.openstreetmap.josm.data.validation.tests;
|
---|
3 |
|
---|
4 | import static org.openstreetmap.josm.tools.I18n.tr;
|
---|
5 |
|
---|
6 | import java.util.ArrayList;
|
---|
7 | import java.util.Arrays;
|
---|
8 | import java.util.Collection;
|
---|
9 | import java.util.HashSet;
|
---|
10 | import java.util.List;
|
---|
11 | import java.util.Set;
|
---|
12 | import java.util.regex.Matcher;
|
---|
13 | import java.util.regex.Pattern;
|
---|
14 |
|
---|
15 | import org.openstreetmap.josm.data.osm.OsmPrimitive;
|
---|
16 | import org.openstreetmap.josm.data.validation.Severity;
|
---|
17 | import org.openstreetmap.josm.data.validation.Test;
|
---|
18 | import org.openstreetmap.josm.data.validation.TestError;
|
---|
19 | import org.openstreetmap.josm.tools.LanguageInfo;
|
---|
20 | import org.openstreetmap.josm.tools.Logging;
|
---|
21 | import org.openstreetmap.josm.tools.SubclassFilteredCollection;
|
---|
22 |
|
---|
23 | /**
|
---|
24 | * Checks for <a href="http://wiki.openstreetmap.org/wiki/Conditional_restrictions">conditional restrictions</a>
|
---|
25 | * @since 6605
|
---|
26 | */
|
---|
27 | public class ConditionalKeys extends Test.TagTest {
|
---|
28 |
|
---|
29 | private final OpeningHourTest openingHourTest = new OpeningHourTest();
|
---|
30 | private static final Set<String> RESTRICTION_TYPES = new HashSet<>(Arrays.asList("oneway", "toll", "noexit", "maxspeed", "minspeed",
|
---|
31 | "maxstay", "maxweight", "maxaxleload", "maxheight", "maxwidth", "maxlength", "overtaking", "maxgcweight", "maxgcweightrating",
|
---|
32 | "fee", "restriction"));
|
---|
33 | private static final Set<String> RESTRICTION_VALUES = new HashSet<>(Arrays.asList("yes", "official", "designated", "destination",
|
---|
34 | "delivery", "permissive", "private", "agricultural", "forestry", "no"));
|
---|
35 | private static final Set<String> TRANSPORT_MODES = new HashSet<>(Arrays.asList("access", "foot", "ski", "inline_skates", "ice_skates",
|
---|
36 | "horse", "vehicle", "bicycle", "carriage", "trailer", "caravan", "motor_vehicle", "motorcycle", "moped", "mofa",
|
---|
37 | "motorcar", "motorhome", "psv", "bus", "taxi", "tourist_bus", "goods", "hgv", "agricultural", "atv", "snowmobile"
|
---|
38 | /*,"hov","emergency","hazmat","disabled"*/));
|
---|
39 |
|
---|
40 | /**
|
---|
41 | * Constructs a new {@code ConditionalKeys}.
|
---|
42 | */
|
---|
43 | public ConditionalKeys() {
|
---|
44 | super(tr("Conditional Keys"), tr("Tests for the correct usage of ''*:conditional'' tags."));
|
---|
45 | }
|
---|
46 |
|
---|
47 | @Override
|
---|
48 | public void initialize() throws Exception {
|
---|
49 | super.initialize();
|
---|
50 | openingHourTest.initialize();
|
---|
51 | }
|
---|
52 |
|
---|
53 | /**
|
---|
54 | * Check if the key is a key for an access restriction
|
---|
55 | * @param part The key (or the restriction part of it, e.g. for lanes)
|
---|
56 | * @return <code>true</code> if it is a restriction
|
---|
57 | */
|
---|
58 | public static boolean isRestrictionType(String part) {
|
---|
59 | return RESTRICTION_TYPES.contains(part);
|
---|
60 | }
|
---|
61 |
|
---|
62 | /**
|
---|
63 | * Check if the value is a valid restriction value
|
---|
64 | * @param part The value
|
---|
65 | * @return <code>true</code> for allowed restriction values
|
---|
66 | */
|
---|
67 | public static boolean isRestrictionValue(String part) {
|
---|
68 | return RESTRICTION_VALUES.contains(part);
|
---|
69 | }
|
---|
70 |
|
---|
71 | /**
|
---|
72 | * Checks if the key denotes a
|
---|
73 | * <a href="http://wiki.openstreetmap.org/wiki/Key:access#Transport_mode_restrictions">transport access mode restriction</a>
|
---|
74 | * @param part The key (or the restriction part of it, e.g. for lanes)
|
---|
75 | * @return <code>true</code> if it is a restriction
|
---|
76 | */
|
---|
77 | public static boolean isTransportationMode(String part) {
|
---|
78 | return TRANSPORT_MODES.contains(part);
|
---|
79 | }
|
---|
80 |
|
---|
81 | /**
|
---|
82 | * Check if a key part is a valid direction
|
---|
83 | * @param part The part of the key
|
---|
84 | * @return <code>true</code> if it is a direction
|
---|
85 | */
|
---|
86 | public static boolean isDirection(String part) {
|
---|
87 | return "forward".equals(part) || "backward".equals(part);
|
---|
88 | }
|
---|
89 |
|
---|
90 | /**
|
---|
91 | * Checks if a given key is a valid access key
|
---|
92 | * @param key The conditional key
|
---|
93 | * @return <code>true</code> if the key is valid
|
---|
94 | */
|
---|
95 | public boolean isKeyValid(String key) {
|
---|
96 | // <restriction-type>[:<transportation mode>][:<direction>]:conditional
|
---|
97 | // -- or -- <transportation mode> [:<direction>]:conditional
|
---|
98 | if (!key.endsWith(":conditional")) {
|
---|
99 | 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, 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 | errors.addAll(validatePrimitive(p));
|
---|
246 | }
|
---|
247 | }
|
---|