kopia lustrzana https://github.com/onthegomap/planetiler
135 wiersze
5.3 KiB
Java
135 wiersze
5.3 KiB
Java
package com.onthegomap.planetiler.custommap;
|
|
|
|
import static com.onthegomap.planetiler.expression.Expression.matchAnyTyped;
|
|
import static com.onthegomap.planetiler.expression.Expression.matchField;
|
|
import static com.onthegomap.planetiler.expression.Expression.not;
|
|
|
|
import com.onthegomap.planetiler.custommap.expression.BooleanExpressionScript;
|
|
import com.onthegomap.planetiler.custommap.expression.ConfigExpressionScript;
|
|
import com.onthegomap.planetiler.custommap.expression.ParseException;
|
|
import com.onthegomap.planetiler.custommap.expression.ScriptContext;
|
|
import com.onthegomap.planetiler.custommap.expression.ScriptEnvironment;
|
|
import com.onthegomap.planetiler.expression.Expression;
|
|
import java.util.Collection;
|
|
import java.util.List;
|
|
import java.util.Map;
|
|
import java.util.function.Function;
|
|
import java.util.function.Predicate;
|
|
import java.util.regex.Pattern;
|
|
|
|
/**
|
|
* Parses user-defined YAML into boolean {@link Expression expressions} that can be evaluated against an input feature.
|
|
*
|
|
* @param <T> Input type of the expression
|
|
*/
|
|
public class BooleanExpressionParser<T extends ScriptContext> {
|
|
|
|
private static final Pattern ESCAPED =
|
|
Pattern.compile("^([\\s\\\\]*)\\\\(__any__|__all__)", Pattern.CASE_INSENSITIVE);
|
|
|
|
private static final Predicate<String> IS_ANY =
|
|
Pattern.compile("^\\s*__any__\\s*$", Pattern.CASE_INSENSITIVE).asMatchPredicate();
|
|
private static final Predicate<String> IS_ALL =
|
|
Pattern.compile("^\\s*__all__\\s*$", Pattern.CASE_INSENSITIVE).asMatchPredicate();
|
|
private static final Predicate<String> IS_NOT =
|
|
Pattern.compile("^\\s*__not__\\s*$", Pattern.CASE_INSENSITIVE).asMatchPredicate();
|
|
private final TagValueProducer tagValueProducer;
|
|
private final ScriptEnvironment<T> context;
|
|
|
|
private BooleanExpressionParser(TagValueProducer tagValueProducer, ScriptEnvironment<T> context) {
|
|
this.tagValueProducer = tagValueProducer;
|
|
this.context = context;
|
|
}
|
|
|
|
/**
|
|
* Returns a boolean expression that determines whether a source feature matches a criteria defined in yaml config.
|
|
*
|
|
* @param <T> Type of input the expression takes
|
|
* @param object a map or list of tag criteria
|
|
* @param tagValueProducer a TagValueProducer
|
|
* @return a predicate which returns true if this criteria matches
|
|
*/
|
|
public static <T extends ScriptContext> Expression parse(Object object, TagValueProducer tagValueProducer,
|
|
ScriptEnvironment<T> context) {
|
|
return new BooleanExpressionParser<>(tagValueProducer, context).parse(object);
|
|
}
|
|
|
|
private static boolean isListOrMap(Object object) {
|
|
return object instanceof Map<?, ?> || object instanceof Collection<?>;
|
|
}
|
|
|
|
private static String unescape(String s) {
|
|
var matcher = ESCAPED.matcher(s);
|
|
if (matcher.matches()) {
|
|
return matcher.replaceFirst("$1$2");
|
|
}
|
|
return s;
|
|
}
|
|
|
|
private static Object unescape(Object o) {
|
|
if (o instanceof String s) {
|
|
return unescape(s);
|
|
}
|
|
return o;
|
|
}
|
|
|
|
private Expression parse(Object object) {
|
|
return parse(object, Expression::or);
|
|
}
|
|
|
|
private Expression parse(Object object, Function<List<Expression>, Expression> collector) {
|
|
if (object == null) {
|
|
return Expression.FALSE;
|
|
} else if (object instanceof String s && s.trim().equalsIgnoreCase("__any__")) {
|
|
return Expression.TRUE;
|
|
} else if (ConfigExpressionScript.isScript(object)) {
|
|
return BooleanExpressionScript.script(ConfigExpressionScript.extractScript(object), context);
|
|
} else if (object instanceof Map<?, ?> map) {
|
|
return parseMapMatch(map, collector);
|
|
} else if (object instanceof Collection<?> list) {
|
|
return collector.apply(list.stream().map(this::parse).toList());
|
|
} else {
|
|
throw new ParseException("Unsupported object for matcher input: " + object);
|
|
}
|
|
}
|
|
|
|
private Expression parseMapMatch(Map<?, ?> map, Function<List<Expression>, Expression> collector) {
|
|
return collector.apply(map.entrySet()
|
|
.stream()
|
|
.map(entry -> tagCriterionToExpression(entry.getKey().toString(), entry.getValue()))
|
|
.toList());
|
|
}
|
|
|
|
private Expression tagCriterionToExpression(String key, Object value) {
|
|
if (IS_ANY.test(key) && isListOrMap(value)) {
|
|
// __any__ ors together its children
|
|
return parse(value, Expression::or);
|
|
} else if (IS_ALL.test(key) && isListOrMap(value)) {
|
|
// __all__ ands together its children
|
|
return parse(value, Expression::and);
|
|
} else if (IS_NOT.test(key)) {
|
|
// __not__ negates its children
|
|
return not(parse(value));
|
|
} else if (value == null || IS_ANY.test(value.toString()) ||
|
|
(value instanceof Collection<?> values &&
|
|
values.stream().anyMatch(d -> d != null && IS_ANY.test(d.toString().trim())))) {
|
|
//If only a key is provided, with no value, match any object tagged with that key.
|
|
return matchField(unescape(key));
|
|
|
|
} else if (value instanceof Collection<?> values) {
|
|
//If a collection is provided, match any of these values.
|
|
return matchAnyTyped(
|
|
unescape(key),
|
|
tagValueProducer.valueGetterForKey(key),
|
|
values.stream().map(BooleanExpressionParser::unescape).toList());
|
|
|
|
} else {
|
|
//Otherwise, a key and single value were passed, so match that exact tag
|
|
return matchAnyTyped(
|
|
unescape(key),
|
|
tagValueProducer.valueGetterForKey(key),
|
|
unescape(value));
|
|
}
|
|
}
|
|
}
|