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}