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}