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}