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.Main;
016import org.openstreetmap.josm.data.osm.OsmPrimitive;
017import org.openstreetmap.josm.data.validation.Severity;
018import org.openstreetmap.josm.data.validation.Test;
019import org.openstreetmap.josm.data.validation.TestError;
020import org.openstreetmap.josm.tools.LanguageInfo;
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"));
033    private static final Set<String> RESTRICTION_VALUES = new HashSet<>(Arrays.asList("yes", "official", "designated", "destination",
034            "delivery", "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    public static boolean isRestrictionType(String part) {
054        return RESTRICTION_TYPES.contains(part);
055    }
056
057    public static boolean isRestrictionValue(String part) {
058        return RESTRICTION_VALUES.contains(part);
059    }
060
061    public static boolean isTransportationMode(String part) {
062        // http://wiki.openstreetmap.org/wiki/Key:access#Transport_mode_restrictions
063        return TRANSPORT_MODES.contains(part);
064    }
065
066    public static boolean isDirection(String part) {
067        return "forward".equals(part) || "backward".equals(part);
068    }
069
070    public boolean isKeyValid(String key) {
071        // <restriction-type>[:<transportation mode>][:<direction>]:conditional
072        // -- or --            <transportation mode> [:<direction>]:conditional
073        if (!key.endsWith(":conditional")) {
074            return false;
075        }
076        final String[] parts = key.replaceAll(":conditional", "").split(":");
077        return isKeyValid3Parts(parts) || isKeyValid1Part(parts) || isKeyValid2Parts(parts);
078    }
079
080    private static boolean isKeyValid3Parts(String ... parts) {
081        return parts.length == 3 && isRestrictionType(parts[0]) && isTransportationMode(parts[1]) && isDirection(parts[2]);
082    }
083
084    private static boolean isKeyValid2Parts(String ... parts) {
085        return parts.length == 2 && ((isRestrictionType(parts[0]) && (isTransportationMode(parts[1]) || isDirection(parts[1])))
086                                  || (isTransportationMode(parts[0]) && isDirection(parts[1])));
087    }
088
089    private static boolean isKeyValid1Part(String ... parts) {
090        return parts.length == 1 && (isRestrictionType(parts[0]) || isTransportationMode(parts[0]));
091    }
092
093    public boolean isValueValid(String key, String value) {
094        return validateValue(key, value) == null;
095    }
096
097    static class ConditionalParsingException extends RuntimeException {
098        ConditionalParsingException(String message) {
099            super(message);
100        }
101    }
102
103    public static class ConditionalValue {
104        public final String restrictionValue;
105        public final Collection<String> conditions;
106
107        public ConditionalValue(String restrictionValue, Collection<String> conditions) {
108            this.restrictionValue = restrictionValue;
109            this.conditions = conditions;
110        }
111
112        /**
113         * Parses the condition values as string.
114         * @param value value, must match {@code <restriction-value> @ <condition>[;<restriction-value> @ <condition>]} pattern
115         * @return list of {@code ConditionalValue}s
116         * @throws ConditionalParsingException if {@code value} does not match expected pattern
117         */
118        public static List<ConditionalValue> parse(String value) {
119            // <restriction-value> @ <condition>[;<restriction-value> @ <condition>]
120            final List<ConditionalValue> r = new ArrayList<>();
121            final String part = Pattern.compile("([^@\\p{Space}][^@]*?)"
122                    + "\\s*@\\s*" + "(\\([^)\\p{Space}][^)]+?\\)|[^();\\p{Space}][^();]*?)\\s*").toString();
123            final Matcher m = Pattern.compile('(' + part + ")(;\\s*" + part + ")*").matcher(value);
124            if (!m.matches()) {
125                throw new ConditionalParsingException(tr("Does not match pattern ''restriction value @ condition''"));
126            } else {
127                int i = 2;
128                while (i + 1 <= m.groupCount() && m.group(i + 1) != null) {
129                    final String restrictionValue = m.group(i);
130                    final String[] conditions = m.group(i + 1).replace("(", "").replace(")", "").split("\\s+(AND|and)\\s+");
131                    r.add(new ConditionalValue(restrictionValue, Arrays.asList(conditions)));
132                    i += 3;
133                }
134            }
135            return r;
136        }
137    }
138
139    public String validateValue(String key, String value) {
140        try {
141            for (final ConditionalValue conditional : ConditionalValue.parse(value)) {
142                // validate restriction value
143                if (isTransportationMode(key.split(":")[0]) && !isRestrictionValue(conditional.restrictionValue)) {
144                    return tr("{0} is not a valid restriction value", conditional.restrictionValue);
145                }
146                // validate opening hour if the value contains an hour (heuristic)
147                for (final String condition : conditional.conditions) {
148                    if (condition.matches(".*[0-9]:[0-9]{2}.*")) {
149                        final List<OpeningHourTest.OpeningHoursTestError> errors = openingHourTest.checkOpeningHourSyntax(
150                                "", condition, OpeningHourTest.CheckMode.TIME_RANGE, true, LanguageInfo.getJOSMLocaleCode());
151                        if (!errors.isEmpty()) {
152                            return errors.get(0).getMessage();
153                        }
154                    }
155                }
156            }
157        } catch (ConditionalParsingException ex) {
158            Main.debug(ex);
159            return ex.getMessage();
160        }
161        return null;
162    }
163
164    public List<TestError> validatePrimitive(OsmPrimitive p) {
165        final List<TestError> errors = new ArrayList<>();
166        for (final String key : SubclassFilteredCollection.filter(p.keySet(),
167                Pattern.compile(":conditional(:.*)?$").asPredicate())) {
168            if (!isKeyValid(key)) {
169                errors.add(TestError.builder(this, Severity.WARNING, 3201)
170                        .message(tr("Wrong syntax in {0} key", key))
171                        .primitives(p)
172                        .build());
173                continue;
174            }
175            final String value = p.get(key);
176            final String error = validateValue(key, value);
177            if (error != null) {
178                errors.add(TestError.builder(this, Severity.WARNING, 3202)
179                        .message(tr("Error in {0} value: {1}", key, error))
180                        .primitives(p)
181                        .build());
182            }
183        }
184        return errors;
185    }
186
187    @Override
188    public void check(OsmPrimitive p) {
189        errors.addAll(validatePrimitive(p));
190    }
191}