// License: GPL. For details, see LICENSE file.
package org.openstreetmap.josm.data.validation.tests;
import static org.openstreetmap.josm.tools.I18n.tr;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.openstreetmap.josm.Main;
import org.openstreetmap.josm.data.osm.OsmPrimitive;
import org.openstreetmap.josm.data.validation.Severity;
import org.openstreetmap.josm.data.validation.Test;
import org.openstreetmap.josm.data.validation.TestError;
import org.openstreetmap.josm.tools.LanguageInfo;
import org.openstreetmap.josm.tools.SubclassFilteredCollection;
/**
* Checks for <a href="http://wiki.openstreetmap.org/wiki/Conditional_restrictions">conditional restrictions</a>
* @since 6605
*/
public class ConditionalKeys extends Test.TagTest {
private final OpeningHourTest openingHourTest = new OpeningHourTest();
private static final Set<String> RESTRICTION_TYPES = new HashSet<>(Arrays.asList("oneway", "toll", "noexit", "maxspeed", "minspeed",
"maxstay", "maxweight", "maxaxleload", "maxheight", "maxwidth", "maxlength", "overtaking", "maxgcweight", "maxgcweightrating",
"fee"));
private static final Set<String> RESTRICTION_VALUES = new HashSet<>(Arrays.asList("yes", "official", "designated", "destination",
"delivery", "permissive", "private", "agricultural", "forestry", "no"));
private static final Set<String> TRANSPORT_MODES = new HashSet<>(Arrays.asList("access", "foot", "ski", "inline_skates", "ice_skates",
"horse", "vehicle", "bicycle", "carriage", "trailer", "caravan", "motor_vehicle", "motorcycle", "moped", "mofa",
"motorcar", "motorhome", "psv", "bus", "taxi", "tourist_bus", "goods", "hgv", "agricultural", "atv", "snowmobile"
/*,"hov","emergency","hazmat","disabled"*/));
/**
* Constructs a new {@code ConditionalKeys}.
*/
public ConditionalKeys() {
super(tr("Conditional Keys"), tr("Tests for the correct usage of ''*:conditional'' tags."));
}
@Override
public void initialize() throws Exception {
super.initialize();
openingHourTest.initialize();
}
public static boolean isRestrictionType(String part) {
return RESTRICTION_TYPES.contains(part);
}
public static boolean isRestrictionValue(String part) {
return RESTRICTION_VALUES.contains(part);
}
public static boolean isTransportationMode(String part) {
// http://wiki.openstreetmap.org/wiki/Key:access#Transport_mode_restrictions
return TRANSPORT_MODES.contains(part);
}
public static boolean isDirection(String part) {
return "forward".equals(part) || "backward".equals(part);
}
public boolean isKeyValid(String key) {
// <restriction-type>[:<transportation mode>][:<direction>]:conditional
// -- or -- <transportation mode> [:<direction>]:conditional
if (!key.endsWith(":conditional")) {
return false;
}
final String[] parts = key.replaceAll(":conditional", "").split(":");
return isKeyValid3Parts(parts) || isKeyValid1Part(parts) || isKeyValid2Parts(parts);
}
private static boolean isKeyValid3Parts(String... parts) {
return parts.length == 3 && isRestrictionType(parts[0]) && isTransportationMode(parts[1]) && isDirection(parts[2]);
}
private static boolean isKeyValid2Parts(String... parts) {
return parts.length == 2 && ((isRestrictionType(parts[0]) && (isTransportationMode(parts[1]) || isDirection(parts[1])))
|| (isTransportationMode(parts[0]) && isDirection(parts[1])));
}
private static boolean isKeyValid1Part(String... parts) {
return parts.length == 1 && (isRestrictionType(parts[0]) || isTransportationMode(parts[0]));
}
public boolean isValueValid(String key, String value) {
return validateValue(key, value) == null;
}
static class ConditionalParsingException extends RuntimeException {
ConditionalParsingException(String message) {
super(message);
}
}
public static class ConditionalValue {
public final String restrictionValue;
public final Collection<String> conditions;
public ConditionalValue(String restrictionValue, Collection<String> conditions) {
this.restrictionValue = restrictionValue;
this.conditions = conditions;
}
/**
* Parses the condition values as string.
* @param value value, must match {@code <restriction-value> @ <condition>[;<restriction-value> @ <condition>]} pattern
* @return list of {@code ConditionalValue}s
* @throws ConditionalParsingException if {@code value} does not match expected pattern
*/
public static List<ConditionalValue> parse(String value) {
// <restriction-value> @ <condition>[;<restriction-value> @ <condition>]
final List<ConditionalValue> r = new ArrayList<>();
final String part = Pattern.compile("([^@\\p{Space}][^@]*?)"
+ "\\s*@\\s*" + "(\\([^)\\p{Space}][^)]+?\\)|[^();\\p{Space}][^();]*?)\\s*").toString();
final Matcher m = Pattern.compile('(' + part + ")(;\\s*" + part + ")*").matcher(value);
if (!m.matches()) {
throw new ConditionalParsingException(tr("Does not match pattern ''restriction value @ condition''"));
} else {
int i = 2;
while (i + 1 <= m.groupCount() && m.group(i + 1) != null) {
final String restrictionValue = m.group(i);
final String[] conditions = m.group(i + 1).replace("(", "").replace(")", "").split("\\s+(AND|and)\\s+");
r.add(new ConditionalValue(restrictionValue, Arrays.asList(conditions)));
i += 3;
}
}
return r;
}
}
public String validateValue(String key, String value) {
try {
for (final ConditionalValue conditional : ConditionalValue.parse(value)) {
// validate restriction value
if (isTransportationMode(key.split(":")[0]) && !isRestrictionValue(conditional.restrictionValue)) {
return tr("{0} is not a valid restriction value", conditional.restrictionValue);
}
// validate opening hour if the value contains an hour (heuristic)
for (final String condition : conditional.conditions) {
if (condition.matches(".*[0-9]:[0-9]{2}.*")) {
final List<OpeningHourTest.OpeningHoursTestError> errors = openingHourTest.checkOpeningHourSyntax(
"", condition, OpeningHourTest.CheckMode.TIME_RANGE, true, LanguageInfo.getJOSMLocaleCode());
if (!errors.isEmpty()) {
return errors.get(0).getMessage();
}
}
}
}
} catch (ConditionalParsingException ex) {
Main.debug(ex);
return ex.getMessage();
}
return null;
}
public List<TestError> validatePrimitive(OsmPrimitive p) {
final List<TestError> errors = new ArrayList<>();
for (final String key : SubclassFilteredCollection.filter(p.keySet(),
Pattern.compile(":conditional(:.*)?$").asPredicate())) {
if (!isKeyValid(key)) {
errors.add(TestError.builder(this, Severity.WARNING, 3201)
.message(tr("Wrong syntax in {0} key", key))
.primitives(p)
.build());
continue;
}
final String value = p.get(key);
final String error = validateValue(key, value);
if (error != null) {
errors.add(TestError.builder(this, Severity.WARNING, 3202)
.message(tr("Error in {0} value: {1}", key, error))
.primitives(p)
.build());
}
}
return errors;
}
@Override
public void check(OsmPrimitive p) {
errors.addAll(validatePrimitive(p));
}
}