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