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.Predicates;
022import org.openstreetmap.josm.tools.Utils;
023
024/**
025 * Checks for <a href="http://wiki.openstreetmap.org/wiki/Conditional_restrictions">conditional restrictions</a>
026 * @since 6605
027 */
028public class ConditionalKeys extends Test.TagTest {
029
030    private final OpeningHourTest openingHourTest = new OpeningHourTest();
031    private static final Set<String> RESTRICTION_TYPES = new HashSet<>(Arrays.asList("oneway", "toll", "noexit", "maxspeed", "minspeed",
032            "maxstay", "maxweight", "maxaxleload", "maxheight", "maxwidth", "maxlength", "overtaking", "maxgcweight", "maxgcweightrating",
033            "fee"));
034    private static final Set<String> RESTRICTION_VALUES = new HashSet<>(Arrays.asList("yes", "official", "designated", "destination",
035            "delivery", "permissive", "private", "agricultural", "forestry", "no"));
036    private static final Set<String> TRANSPORT_MODES = new HashSet<>(Arrays.asList("access", "foot", "ski", "inline_skates", "ice_skates",
037            "horse", "vehicle", "bicycle", "carriage", "trailer", "caravan", "motor_vehicle", "motorcycle", "moped", "mofa",
038            "motorcar", "motorhome", "psv", "bus", "taxi", "tourist_bus", "goods", "hgv", "agricultural", "atv", "snowmobile"
039            /*,"hov","emergency","hazmat","disabled"*/));
040
041    /**
042     * Constructs a new {@code ConditionalKeys}.
043     */
044    public ConditionalKeys() {
045        super(tr("Conditional Keys"), tr("Tests for the correct usage of ''*:conditional'' tags."));
046    }
047
048    @Override
049    public void initialize() throws Exception {
050        super.initialize();
051        openingHourTest.initialize();
052    }
053
054    public static boolean isRestrictionType(String part) {
055        return RESTRICTION_TYPES.contains(part);
056    }
057
058    public static boolean isRestrictionValue(String part) {
059        return RESTRICTION_VALUES.contains(part);
060    }
061
062    public static boolean isTransportationMode(String part) {
063        // http://wiki.openstreetmap.org/wiki/Key:access#Transport_mode_restrictions
064        return TRANSPORT_MODES.contains(part);
065    }
066
067    public static boolean isDirection(String part) {
068        return "forward".equals(part) || "backward".equals(part);
069    }
070
071    public boolean isKeyValid(String key) {
072        // <restriction-type>[:<transportation mode>][:<direction>]:conditional
073        // -- or --            <transportation mode> [:<direction>]:conditional
074        if (!key.endsWith(":conditional")) {
075            return false;
076        }
077        final String[] parts = key.replaceAll(":conditional", "").split(":");
078        return parts.length == 3 && isRestrictionType(parts[0]) && isTransportationMode(parts[1]) && isDirection(parts[2])
079                || parts.length == 1 && (isRestrictionType(parts[0]) || isTransportationMode(parts[0]))
080                || parts.length == 2 && (
081                isRestrictionType(parts[0]) && (isTransportationMode(parts[1]) || isDirection(parts[1]))
082                        || isTransportationMode(parts[0]) && isDirection(parts[1]));
083    }
084
085    public boolean isValueValid(String key, String value) {
086        return validateValue(key, value) == null;
087    }
088
089    static class ConditionalParsingException extends RuntimeException {
090        ConditionalParsingException(String message) {
091            super(message);
092        }
093    }
094
095    public static class ConditionalValue {
096        public final String restrictionValue;
097        public final Collection<String> conditions;
098
099        public ConditionalValue(String restrictionValue, Collection<String> conditions) {
100            this.restrictionValue = restrictionValue;
101            this.conditions = conditions;
102        }
103
104        public static List<ConditionalValue> parse(String value) throws ConditionalParsingException {
105            // <restriction-value> @ <condition>[;<restriction-value> @ <condition>]
106            final List<ConditionalValue> r = new ArrayList<>();
107            final String part = Pattern.compile("([^@\\p{Space}][^@]*?)"
108                    + "\\s*@\\s*" + "(\\([^)\\p{Space}][^)]+?\\)|[^();\\p{Space}][^();]*?)\\s*").toString();
109            final Matcher m = Pattern.compile('(' + part + ")(;\\s*" + part + ")*").matcher(value);
110            if (!m.matches()) {
111                throw new ConditionalParsingException(tr("Does not match pattern ''restriction value @ condition''"));
112            } else {
113                int i = 2;
114                while (i + 1 <= m.groupCount() && m.group(i + 1) != null) {
115                    final String restrictionValue = m.group(i);
116                    final String[] conditions = m.group(i + 1).replace("(", "").replace(")", "").split("\\s+(AND|and)\\s+");
117                    r.add(new ConditionalValue(restrictionValue, Arrays.asList(conditions)));
118                    i += 3;
119                }
120            }
121            return r;
122        }
123    }
124
125    public String validateValue(String key, String value) {
126        try {
127            for (final ConditionalValue conditional : ConditionalValue.parse(value)) {
128                // validate restriction value
129                if (isTransportationMode(key.split(":")[0]) && !isRestrictionValue(conditional.restrictionValue)) {
130                    return tr("{0} is not a valid restriction value", conditional.restrictionValue);
131                }
132                // validate opening hour if the value contains an hour (heuristic)
133                for (final String condition : conditional.conditions) {
134                    if (condition.matches(".*[0-9]:[0-9]{2}.*")) {
135                        final List<OpeningHourTest.OpeningHoursTestError> errors = openingHourTest.checkOpeningHourSyntax(
136                                "", condition, OpeningHourTest.CheckMode.TIME_RANGE, true, LanguageInfo.getJOSMLocaleCode());
137                        if (!errors.isEmpty()) {
138                            return errors.get(0).getMessage();
139                        }
140                    }
141                }
142            }
143        } catch (ConditionalParsingException ex) {
144            Main.debug(ex);
145            return ex.getMessage();
146        }
147        return null;
148    }
149
150    public List<TestError> validatePrimitive(OsmPrimitive p) {
151        final List<TestError> errors = new ArrayList<>();
152        for (final String key : Utils.filter(p.keySet(), Predicates.stringMatchesPattern(Pattern.compile(".*:conditional(:.*)?$")))) {
153            if (!isKeyValid(key)) {
154                errors.add(new TestError(this, Severity.WARNING, tr("Wrong syntax in {0} key", key), 3201, p));
155                continue;
156            }
157            final String value = p.get(key);
158            final String error = validateValue(key, value);
159            if (error != null) {
160                errors.add(new TestError(this, Severity.WARNING, tr("Error in {0} value: {1}", key, error), 3202, p));
161            }
162        }
163        return errors;
164    }
165
166    @Override
167    public void check(OsmPrimitive p) {
168        errors.addAll(validatePrimitive(p));
169    }
170}