| 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.Logging;
|
|---|
| 20 |
|
|---|
| 21 | /**
|
|---|
| 22 | * Checks for <a href="http://wiki.openstreetmap.org/wiki/Key:parking:lane">parking lanes</a>
|
|---|
| 23 | * @since 18400
|
|---|
| 24 | */
|
|---|
| 25 | public class ParkingLanesConditional extends Test.TagTest {
|
|---|
| 26 |
|
|---|
| 27 | private final OpeningHourTest openingHourTest = new OpeningHourTest();
|
|---|
| 28 | // private static final Set<String> CONDITIONAL_PARKING_CONDITION_TYPES = new HashSet<>(Arrays.asList("parking:condition:both", "parking:condition:left",
|
|---|
| 29 | // "parking:condition:right", "parking:condition:both:maxstay", "parking:condition:left:maxstay", "parking:condition:right:maxstay"));
|
|---|
| 30 | private static final Set<String> CONDITIONAL_PARKING_VALUES = new HashSet<>(Arrays.asList("no", "free", "ticket", "disc", "disabled", "residents",
|
|---|
| 31 | "no_parking", "no_standing", "no_stopping"));
|
|---|
| 32 | private static final Set<String> TRANSPORT_TYPES = new HashSet<>(Arrays.asList("vehicle", "bicycle", "carriage", "trailer", "caravan", "motor_vehicle",
|
|---|
| 33 | "motorcycle", "moped", "mofa", "motorcar", "motorhome", "psv", "bus", "taxi", "tourist_bus", "goods", "hgv", "agricultural", "atv", "snowmobile",
|
|---|
| 34 | "hgv_articulated", "coach"));
|
|---|
| 35 |
|
|---|
| 36 | private static final Pattern CONDITIONAL_PATTERN;
|
|---|
| 37 | static {
|
|---|
| 38 | final String part = Pattern.compile("([^@\\p{Space}][^@]*?)"
|
|---|
| 39 | + "\\s*@\\s*" + "(\\([^)\\p{Space}][^)]+?\\)|[^();\\p{Space}][^();]*?)\\s*").toString();
|
|---|
| 40 | CONDITIONAL_PATTERN = Pattern.compile('(' + part + ")(;\\s*" + part + ")*");
|
|---|
| 41 | }
|
|---|
| 42 | private static final Pattern MAXSTAY_PATTERN = Pattern.compile("^(([1-9][0-9]*(\.[0-9]+)?( (minute|minutes|hour|hours|day|days|week|weeks|month|months|year|years))))$");
|
|---|
| 43 |
|
|---|
| 44 | /**
|
|---|
| 45 | * Constructs a new {@code ParkingLanes}.
|
|---|
| 46 | */
|
|---|
| 47 | public ParkingLanesConditional() {
|
|---|
| 48 | super(tr("Parking Lanes"), tr("Tests for the correct usage of ''*:conditional'' tags in parking lanes."));
|
|---|
| 49 | }
|
|---|
| 50 |
|
|---|
| 51 | @Override
|
|---|
| 52 | public void initialize() throws Exception {
|
|---|
| 53 | super.initialize();
|
|---|
| 54 | openingHourTest.initialize();
|
|---|
| 55 | }
|
|---|
| 56 |
|
|---|
| 57 | /**
|
|---|
| 58 | * Check if the value is a valid restriction value
|
|---|
| 59 | * @param part The value
|
|---|
| 60 | * @return <code>true</code> for allowed restriction values
|
|---|
| 61 | */
|
|---|
| 62 | public static boolean isConditionalParkingValue(String part) {
|
|---|
| 63 | return CONDITIONAL_PARKING_VALUES.contains(part);
|
|---|
| 64 | }
|
|---|
| 65 |
|
|---|
| 66 | /**
|
|---|
| 67 | * Checks if the key denotes a
|
|---|
| 68 | * <a href="http://wiki.openstreetmap.org/wiki/Key:access#Transport_mode_restrictions">transport access mode restriction</a>
|
|---|
| 69 | * @param part The key (or the restriction part of it, e.g. for lanes)
|
|---|
| 70 | * @return <code>true</code> if it is a restriction
|
|---|
| 71 | */
|
|---|
| 72 | public static boolean isTransportationMode(String part) {
|
|---|
| 73 | return TRANSPORT_TYPES.contains(part);
|
|---|
| 74 | }
|
|---|
| 75 |
|
|---|
| 76 | /**
|
|---|
| 77 | * Check if a key part is a valid side
|
|---|
| 78 | * @param part The part of the key
|
|---|
| 79 | * @return <code>true</code> if it is a side
|
|---|
| 80 | */
|
|---|
| 81 | public static boolean isSide(String part) {
|
|---|
| 82 | return "left".equals(part) || "right".equals(part) || "both".equals(part);
|
|---|
| 83 | }
|
|---|
| 84 |
|
|---|
| 85 | /**
|
|---|
| 86 | * Check if a key part is the maxstay value
|
|---|
| 87 | * @param part The part of the key
|
|---|
| 88 | * @return <code>true</code> if is the maxstay string
|
|---|
| 89 | */
|
|---|
| 90 | public static boolean isMaxstay(String part) {
|
|---|
| 91 | return "maxstay".equals(part);
|
|---|
| 92 | }
|
|---|
| 93 |
|
|---|
| 94 | /**
|
|---|
| 95 | * Checks if a given key is a valid access key
|
|---|
| 96 | * @param key The conditional key
|
|---|
| 97 | * @return <code>true</code> if the key is valid
|
|---|
| 98 | */
|
|---|
| 99 | public boolean isKeyValid(String key) {
|
|---|
| 100 | // parking:condition:<side>[:<transportation mode>]:conditional or
|
|---|
| 101 | // parking:condition:<side>[:<transportation mode>]:maxstay:conditional
|
|---|
| 102 | if (!key.startsWith("parking:condition:") && !key.endsWith(":conditional")) {
|
|---|
| 103 | return false;
|
|---|
| 104 | }
|
|---|
| 105 | final String[] parts = key.replace("parking:condition:", "").replace(":conditional", "").split(":", -1);
|
|---|
| 106 | return isKeyValid3Parts(parts) || isKeyValid1Part(parts) || isKeyValid2Parts(parts);
|
|---|
| 107 | }
|
|---|
| 108 |
|
|---|
| 109 | private static boolean isKeyValid3Parts(String... parts) {
|
|---|
| 110 | return parts.length == 3 && isSide(parts[0]) && isTransportationMode(parts[1]) && isMaxstay(parts[2]);
|
|---|
| 111 | }
|
|---|
| 112 |
|
|---|
| 113 | private static boolean isKeyValid2Parts(String... parts) {
|
|---|
| 114 | return parts.length == 2 && ((isSide(parts[0]) && isTransportationMode(parts[1])) || (isSide(parts[0]) && isMaxstay(parts[1])));
|
|---|
| 115 | }
|
|---|
| 116 |
|
|---|
| 117 | private static boolean isKeyValid1Part(String... parts) {
|
|---|
| 118 | return parts.length == 1 && (isSide(parts[0]));
|
|---|
| 119 | }
|
|---|
| 120 |
|
|---|
| 121 | /**
|
|---|
| 122 | * Check if a value is valid
|
|---|
| 123 | * @param key The key the value is for
|
|---|
| 124 | * @param value The value
|
|---|
| 125 | * @return <code>true</code> if it is valid
|
|---|
| 126 | */
|
|---|
| 127 | public boolean isValueValid(String key, String value) {
|
|---|
| 128 | return validateValue(key, value) == null;
|
|---|
| 129 | }
|
|---|
| 130 |
|
|---|
| 131 | static class ConditionalParsingException extends RuntimeException {
|
|---|
| 132 | ConditionalParsingException(String message) {
|
|---|
| 133 | super(message);
|
|---|
| 134 | }
|
|---|
| 135 | }
|
|---|
| 136 |
|
|---|
| 137 | /**
|
|---|
| 138 | * A conditional value is a value for the access restriction tag that depends on conditions (time, ...)
|
|---|
| 139 | */
|
|---|
| 140 | public static class ConditionalValue {
|
|---|
| 141 | /**
|
|---|
| 142 | * The value the tag should have if the condition matches
|
|---|
| 143 | */
|
|---|
| 144 | public final String conditionalParkingValue;
|
|---|
| 145 | /**
|
|---|
| 146 | * The conditions for {@link #conditionalParkingValue}
|
|---|
| 147 | */
|
|---|
| 148 | public final Collection<String> conditions;
|
|---|
| 149 |
|
|---|
| 150 | /**
|
|---|
| 151 | * Create a new {@link ConditionalValue}
|
|---|
| 152 | * @param conditionalParkingValue The value the tag should have if the condition matches
|
|---|
| 153 | * @param conditions The conditions for that value
|
|---|
| 154 | */
|
|---|
| 155 | public ConditionalValue(String conditionalParkingValue, Collection<String> conditions) {
|
|---|
| 156 | this.conditionalParkingValue = conditionalParkingValue;
|
|---|
| 157 | this.conditions = conditions;
|
|---|
| 158 | }
|
|---|
| 159 |
|
|---|
| 160 | /**
|
|---|
| 161 | * Parses the condition values as string.
|
|---|
| 162 | * @param value value, must match {@code <restriction-value> @ <condition>[;<restriction-value> @ <condition>]} pattern
|
|---|
| 163 | * @return list of {@code ConditionalValue}s
|
|---|
| 164 | * @throws ConditionalParsingException if {@code value} does not match expected pattern
|
|---|
| 165 | */
|
|---|
| 166 | public static List<ConditionalValue> parse(String value) {
|
|---|
| 167 | // <restriction-value> @ <condition>[;<restriction-value> @ <condition>]
|
|---|
| 168 | final List<ConditionalValue> r = new ArrayList<>();
|
|---|
| 169 | final Matcher m = CONDITIONAL_PATTERN.matcher(value);
|
|---|
| 170 | // FIXME: Add MAXSTAY_PATTERN somehow
|
|---|
| 171 | if (!m.matches()) {
|
|---|
| 172 | throw new ConditionalParsingException(tr("Does not match pattern ''restriction value @ condition''"));
|
|---|
| 173 | } else {
|
|---|
| 174 | int i = 2;
|
|---|
| 175 | while (i + 1 <= m.groupCount() && m.group(i + 1) != null) {
|
|---|
| 176 | final String restrictionValue = m.group(i);
|
|---|
| 177 | final String[] conditions = m.group(i + 1).replace("(", "").replace(")", "").split("\\s+(AND|and)\\s+", -1);
|
|---|
| 178 | r.add(new ConditionalValue(restrictionValue, Arrays.asList(conditions)));
|
|---|
| 179 | i += 3;
|
|---|
| 180 | }
|
|---|
| 181 | }
|
|---|
| 182 | return r;
|
|---|
| 183 | }
|
|---|
| 184 | }
|
|---|
| 185 |
|
|---|
| 186 | /**
|
|---|
| 187 | * Validate a key/value pair
|
|---|
| 188 | * @param key The key
|
|---|
| 189 | * @param value The value
|
|---|
| 190 | * @return The error message for that value or <code>null</code> to indicate valid
|
|---|
| 191 | */
|
|---|
| 192 | public String validateValue(String key, String value) {
|
|---|
| 193 | try {
|
|---|
| 194 | for (final ConditionalValue conditional : ConditionalValue.parse(value)) {
|
|---|
| 195 | // validate restriction value
|
|---|
| 196 | if (!isConditionalParkingValue(conditional.conditionalParkingValue)) {
|
|---|
| 197 | return tr("{0} is not a valid parking condition value", conditional.conditionalParkingValue);
|
|---|
| 198 | }
|
|---|
| 199 | // validate opening hour if the value contains an hour (heuristic)
|
|---|
| 200 | for (final String condition : conditional.conditions) {
|
|---|
| 201 | if (condition.matches(".*[0-9]:[0-9]{2}.*")) {
|
|---|
| 202 | final List<TestError> errors = openingHourTest.checkOpeningHourSyntax("", condition);
|
|---|
| 203 | if (!errors.isEmpty()) {
|
|---|
| 204 | return errors.get(0).getDescription();
|
|---|
| 205 | }
|
|---|
| 206 | }
|
|---|
| 207 | }
|
|---|
| 208 | }
|
|---|
| 209 | } catch (ConditionalParsingException ex) {
|
|---|
| 210 | Logging.debug(ex);
|
|---|
| 211 | return ex.getMessage();
|
|---|
| 212 | }
|
|---|
| 213 | return null;
|
|---|
| 214 | }
|
|---|
| 215 |
|
|---|
| 216 | /**
|
|---|
| 217 | * Validate a primitive
|
|---|
| 218 | * @param p The primitive
|
|---|
| 219 | * @return The errors for that primitive or an empty list if there are no errors.
|
|---|
| 220 | */
|
|---|
| 221 | public List<TestError> validatePrimitive(OsmPrimitive p) {
|
|---|
| 222 | final List<TestError> errors = new ArrayList<>();
|
|---|
| 223 | final Pattern pattern = Pattern.compile(":conditional(:.*)?$");
|
|---|
| 224 | p.visitKeys((primitive, key, value) -> {
|
|---|
| 225 | // Only validate parking condition keys
|
|---|
| 226 | if (!key.startsWith("parking:condition") || !pattern.matcher(key).find()) {
|
|---|
| 227 | return;
|
|---|
| 228 | }
|
|---|
| 229 | if (!isKeyValid(key)) {
|
|---|
| 230 | errors.add(TestError.builder(this, Severity.WARNING, 4101)
|
|---|
| 231 | .message(tr("Wrong syntax in {0} key", key))
|
|---|
| 232 | .primitives(p)
|
|---|
| 233 | .build());
|
|---|
| 234 | return;
|
|---|
| 235 | }
|
|---|
| 236 | final String error = validateValue(key, value);
|
|---|
| 237 | if (error != null) {
|
|---|
| 238 | errors.add(TestError.builder(this, Severity.WARNING, 4102)
|
|---|
| 239 | .message(tr("Error in {0} value: {1}", key, error))
|
|---|
| 240 | .primitives(p)
|
|---|
| 241 | .build());
|
|---|
| 242 | }
|
|---|
| 243 | });
|
|---|
| 244 | return errors;
|
|---|
| 245 | }
|
|---|
| 246 |
|
|---|
| 247 | @Override
|
|---|
| 248 | public void check(OsmPrimitive p) {
|
|---|
| 249 | if (p.isTagged()) {
|
|---|
| 250 | errors.addAll(validatePrimitive(p));
|
|---|
| 251 | }
|
|---|
| 252 | }
|
|---|
| 253 | }
|
|---|