Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Java/Janino scripting to replace our interpreter leading to a breaking change of the format #2209

Merged
merged 108 commits into from Jan 19, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
108 commits
Select commit Hold shift + click to select a range
70f3782
basic test with janino passing
karussell Oct 7, 2020
9bf6769
move janino scripting to safe whitelisted approach
karussell Oct 10, 2020
963bcf6
create a class to add encoded values on the fly but limit user input …
karussell Oct 14, 2020
bf34c24
compilation unit via overwriting getValue method safely
karussell Oct 16, 2020
3a3779d
minor fix regarding ClassName
karussell Oct 16, 2020
1b18556
ensure it is not called twice
karussell Oct 16, 2020
0948867
separate parsing of condition and number
karussell Oct 16, 2020
6932968
CustomWeighting: ensure Map is ordered (#2162)
karussell Oct 25, 2020
234d210
change expression from factor multiplication to if-then-elseif-else a…
karussell Oct 25, 2020
2f73987
replace catch all with true and make max_speed working
karussell Oct 26, 2020
758eed9
no need to parse value which is always a number
karussell Oct 27, 2020
7228769
use global max speed
karussell Oct 27, 2020
a3eb59c
minor cleanup and fix regarding sharedEV
karussell Oct 27, 2020
6c2f77b
found a way to avoid Unparser.unparse
karussell Oct 28, 2020
2b1ec2e
use scripting in CustomWeighting. This revealed: we need if and if-el…
karussell Oct 29, 2020
77f66bc
reduce max speed and fix priority bug
karussell Oct 29, 2020
52fede2
cosmetics
karussell Oct 29, 2020
4af26d6
merged master
karussell Oct 29, 2020
1463e46
minor truck.yml tweaks
karussell Oct 30, 2020
120aa7c
adapt benchmarks
karussell Oct 30, 2020
975f21e
a bit more details in exception
karussell Oct 30, 2020
0ba0f59
see perf with ugly caching hack
karussell Oct 31, 2020
5c9ac65
revert and see bench
karussell Oct 31, 2020
56c2624
workaround bug with import conflict (two imports for enum 'NO')
karussell Nov 1, 2020
f544113
inject class for constants, avoid static imports that can easily collide
karussell Nov 1, 2020
802152a
special case of RouteNetwork for bike
karussell Nov 1, 2020
247204e
introduce thread-safe caching; minor restructuring
karussell Nov 3, 2020
d32b56f
reduce code duplication in ScriptHelper
karussell Nov 3, 2020
d532e32
move code out of ScriptHelper and rename it to ExpressionBuilder
karussell Nov 6, 2020
09b1c8c
merged master
karussell Nov 19, 2020
648a03e
less recursion for more clear protection
karussell Nov 19, 2020
55e6fd5
further cleanup and improved naming
karussell Nov 20, 2020
d2e1b75
adapt CustomModel.merge to scripting and throw exception if query-Cus…
karussell Nov 20, 2020
d0b8087
improved docs about custom model
karussell Nov 21, 2020
e8d69b2
adapted docs to new format
karussell Nov 21, 2020
2ea7f15
removed CustomWeightingOld and all related classes
karussell Nov 21, 2020
2b65279
minor fixes
karussell Nov 21, 2020
e634545
make debugging possible; every dynamic class has a different name now…
karussell Nov 24, 2020
83a4758
improve caching: remove oldest accessed entry, instead oldest inserted
karussell Nov 24, 2020
79e15b0
introduce cache to keep certain classes in cache forever
karussell Nov 25, 2020
85c9743
try perf with disabled cache to see CH numbers again
karussell Nov 25, 2020
1744afd
Revert "try perf with disabled cache to see CH numbers again"
karussell Nov 25, 2020
df9eff6
merged master
karussell Dec 2, 2020
7dbca99
make it possible to disable dyn cache; clarify about the static cache…
karussell Dec 3, 2020
2998c87
allow multiple first_match and make names of encoded values even stri…
karussell Dec 3, 2020
5a72244
non-shared EncodedValue is too complex due to '.' we should use '$' o…
karussell Dec 5, 2020
f40e284
merged master
karussell Dec 8, 2020
ea97c53
merged master
karussell Dec 9, 2020
564f342
use non-shared encoded values
karussell Dec 9, 2020
5cb073c
test again without cache
karussell Dec 9, 2020
dc4268a
call in-area method only when necessary, improves speed where pre-con…
karussell Dec 12, 2020
8d2e9ee
make area names stricter
karussell Dec 12, 2020
f4478bb
already consider parts of the pull request reviews
karussell Dec 12, 2020
591317a
Custom weighting helper cleanup (#2211)
easbar Dec 14, 2020
3762a59
More fine-grained usage of try statement
easbar Dec 13, 2020
49d51ae
revert DEFAULT to true
karussell Dec 14, 2020
63bd616
docs: remove confusing statement
karussell Dec 14, 2020
e2d74dc
better name for subclass
karussell Dec 14, 2020
449278b
no need for the extra line in test
karussell Dec 14, 2020
7bf5b21
simple max_speed_fallback handling, still explicit
karussell Dec 14, 2020
c40d078
Merge branch 'master' into janino_scripting
karussell Dec 16, 2020
69db7b3
migrate to if-then notation
karussell Dec 17, 2020
97d6be5
fix benchmarks for if-then notation
karussell Dec 17, 2020
0c2eecc
made Clause methods shorter
karussell Dec 18, 2020
2f087cc
introduce different Op
karussell Dec 18, 2020
66bbb9c
merge speed_factor and max_speed into speed via introduced 'multiply …
karussell Dec 18, 2020
43bab56
further simplify custom model: remove max_speed_fallback
karussell Dec 19, 2020
3185010
bug fix
karussell Dec 19, 2020
d880afc
docs: adapt to know notation
karussell Dec 19, 2020
b8425bd
multiply with -> by
karussell Dec 20, 2020
c3fa7c8
minor cleanup
karussell Dec 20, 2020
0f75328
Merge branch 'master' into janino_scripting
karussell Dec 20, 2020
576f2f1
support for StringEncodedValue
karussell Dec 26, 2020
52ca223
UI fixes for new format; make isSharedEncodedValues method private again
karussell Dec 26, 2020
01582a4
bug fix for findMaxSpeed calculation
karussell Dec 26, 2020
3708c22
test perf: disable cache for comparison
karussell Dec 26, 2020
1d90439
test perf: again
karussell Dec 26, 2020
bd92b3c
enable cache to permanently store internal Weighting classes
karussell Dec 26, 2020
6bb9fd0
uhm, test perf: without cache but push separately
karussell Dec 26, 2020
e8ee463
Revert "uhm, test perf: without cache but push separately"
karussell Dec 26, 2020
ac46dad
minor improvements
karussell Dec 26, 2020
e7725ca
removed SpeedAndAccessProvider
karussell Dec 31, 2020
1cbfb01
make sure that the internal cache will never be too big
karussell Dec 31, 2020
9759c41
fix package of JaninoCustomWeightingHelperSubclass
karussell Dec 31, 2020
a5a497c
rename ExpressionBuilder to CustomModelParser
karussell Dec 31, 2020
46b4c94
made CustomModelParser parser public and move create method to it fro…
karussell Dec 31, 2020
f1cc5ec
comment regarding internal cache method
karussell Dec 31, 2020
b7fef8b
create -> createWeighting
karussell Dec 31, 2020
e090791
move CustomModel to different package to hide useInternalCache methods
karussell Dec 31, 2020
17f764e
merged master
karussell Jan 6, 2021
ca6b736
limit size of the whole script, not single expressions
karussell Jan 7, 2021
8c1b25e
Merge branch 'master' into janino_scripting
easbar Jan 8, 2021
4e35826
Rename CustomModelParser#create etc.
easbar Jan 8, 2021
a4324f5
Merge branch 'master' into janino_scripting
easbar Jan 8, 2021
ee7461e
minor
easbar Jan 8, 2021
30d9cdc
rename internal and throw exception if found in query
karussell Jan 8, 2021
2812d7c
fix contentString
karussell Jan 8, 2021
5cd2762
Improve profiles.md
easbar Jan 8, 2021
0aff1e1
Fix road_surface->surface and else: "" -> else: null
easbar Jan 8, 2021
a6853d0
Explain how to use else statement in JSON
easbar Jan 8, 2021
b81b1cf
Fix GH maps example
easbar Jan 8, 2021
e1cd19d
Warn instead of error in case the edge distance of very long edges di…
easbar Jan 10, 2021
3352262
rename
karussell Jan 11, 2021
d6552b2
error message e.g. in case of encoded value wasn't added
karussell Jan 12, 2021
c113442
distanceInfluence: use default value only if not initialized
karussell Jan 12, 2021
f2292d3
Merge branch 'master' into janino_scripting
karussell Jan 12, 2021
5c2d670
less cryptic error message
karussell Jan 15, 2021
0a0a1db
Merge branch 'master' into janino_scripting
karussell Jan 18, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Expand Up @@ -213,7 +213,7 @@ Here is a list of the more detailed features:
* [Alternative routes](https://discuss.graphhopper.com/t/alternative-routes/424)
* [Turn costs and restrictions](../stable/docs/core/turn-restrictions.md)
* Country specific routing via SpatialRules
* The core uses only a few dependencies (hppc, jts and slf4j)
* The core uses only a few dependencies (hppc, jts, janino and slf4j)
* Scales from small indoor-sized to world-wide-sized graphs
* Finds nearest point on street e.g. to get elevation or 'snap to road' or being used as spatial index (see [#1485](https://github.com/graphhopper/graphhopper/pull/1485))
* Calculates isochrones and [shortest path trees](https://github.com/graphhopper/graphhopper/pull/1577)
Expand Down
104 changes: 104 additions & 0 deletions api/src/main/java/com/graphhopper/json/Statement.java
@@ -0,0 +1,104 @@
/*
* Licensed to GraphHopper GmbH under one or more contributor
* license agreements. See the NOTICE file distributed with this work for
* additional information regarding copyright ownership.
*
* GraphHopper GmbH licenses this file to you under the Apache License,
* Version 2.0 (the "License"); you may not use this file except in
* compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.graphhopper.json;

public class Statement {
private final Keyword keyword;
private final String expression;
private final Op operation;
private final double value;
karussell marked this conversation as resolved.
Show resolved Hide resolved

private Statement(Keyword keyword, String expression, Op operation, double value) {
this.keyword = keyword;
this.expression = expression;
this.value = value;
this.operation = operation;
}

public Keyword getKeyword() {
return keyword;
}

public String getExpression() {
return expression;
}

public Op getOperation() {
return operation;
}

public double getValue() {
return value;
}

public enum Keyword {
IF("if"), ELSEIF("else if"), ELSE("else");

String name;

Keyword(String name) {
this.name = name;
}

public String getName() {
return name;
}
}

public enum Op {
MULTIPLY("multiply by"), LIMIT("limit to");

String name;

Op(String name) {
this.name = name;
}

public String getName() {
return name;
}

public String build(double value) {
switch (this) {
case MULTIPLY:
return "value *= " + value;
case LIMIT:
return "value = Math.min(value," + value + ")";
default:
throw new IllegalArgumentException();
}
}
}

@Override
public String toString() {
return keyword.getName() + ": " + expression + ", " + operation.getName() + ": " + value;
}

public static Statement If(String expression, Op op, double value) {
return new Statement(Keyword.IF, expression, op, value);
}

public static Statement ElseIf(String expression, Op op, double value) {
return new Statement(Keyword.ELSEIF, expression, op, value);
}

public static Statement Else(Op op, double value) {
return new Statement(Keyword.ELSE, null, op, value);
}
}
202 changes: 65 additions & 137 deletions api/src/main/java/com/graphhopper/util/CustomModel.java
Expand Up @@ -17,6 +17,8 @@
*/
package com.graphhopper.util;

import com.graphhopper.json.Statement;

import java.util.*;

/**
Expand All @@ -26,32 +28,43 @@ public class CustomModel {

public static final String KEY = "custom_model";

static double DEFAULT_D_I = 70;
// optional:
private Double maxSpeedFallback;
private Double headingPenalty = Parameters.Routing.DEFAULT_HEADING_PENALTY;
// default value derived from the cost for time e.g. 25€/hour and for distance 0.5€/km, for trucks this is usually larger
private double distanceInfluence = DEFAULT_D_I;
private Map<String, Object> speedFactorMap = new LinkedHashMap<>();
private Map<String, Object> maxSpeedMap = new LinkedHashMap<>();
private Map<String, Object> priorityMap = new LinkedHashMap<>();
// e.g. 70 means that the time costs are 25€/hour and for the distance 0.5€/km (for trucks this is usually larger)
static double DEFAULT_DISTANCE_INFLUENCE = 70;
private Double distanceInfluence;
private double headingPenalty = Parameters.Routing.DEFAULT_HEADING_PENALTY;
private boolean internal;
private List<Statement> speedStatements = new ArrayList<>();
private List<Statement> priorityStatements = new ArrayList<>();
private Map<String, JsonFeature> areas = new HashMap<>();

public CustomModel() {
}

public CustomModel(CustomModel toCopy) {
this.maxSpeedFallback = toCopy.maxSpeedFallback;
this.headingPenalty = toCopy.headingPenalty;
this.distanceInfluence = toCopy.distanceInfluence;
// do not copy "internal"

speedFactorMap = deepCopy(toCopy.getSpeedFactor());
maxSpeedMap = deepCopy(toCopy.getMaxSpeed());
priorityMap = deepCopy(toCopy.getPriority());
speedStatements = deepCopy(toCopy.getSpeed());
priorityStatements = deepCopy(toCopy.getPriority());

areas.putAll(toCopy.getAreas());
}

/**
* This method is for internal usage only! Parsing a CustomModel is expensive and so we cache the result, which is
* especially important for fast landmark queries (hybrid mode). Now this method ensures that all server-side custom
* models are cached in a special internal cache which does not remove seldom accessed entries.
*/
public CustomModel internal() {
this.internal = true;
return this;
}

public boolean isInternal() {
return internal;
}

private <T> T deepCopy(T originalObject) {
if (originalObject instanceof List) {
List<Object> newList = new ArrayList<>(((List) originalObject).size());
Expand All @@ -72,25 +85,22 @@ private <T> T deepCopy(T originalObject) {
}
}

public Map<String, Object> getSpeedFactor() {
return speedFactorMap;
public List<Statement> getSpeed() {
return speedStatements;
}

public Map<String, Object> getMaxSpeed() {
return maxSpeedMap;
}

public CustomModel setMaxSpeedFallback(Double maxSpeedFallback) {
this.maxSpeedFallback = maxSpeedFallback;
public CustomModel addToSpeed(Statement st) {
getSpeed().add(st);
return this;
}

public Double getMaxSpeedFallback() {
return maxSpeedFallback;
public List<Statement> getPriority() {
return priorityStatements;
}

public Map<String, Object> getPriority() {
return priorityMap;
public CustomModel addToPriority(Statement st) {
getPriority().add(st);
return this;
}

public CustomModel setAreas(Map<String, JsonFeature> areas) {
Expand All @@ -108,7 +118,7 @@ public CustomModel setDistanceInfluence(double distanceFactor) {
}

public double getDistanceInfluence() {
return distanceInfluence;
return distanceInfluence == null ? DEFAULT_DISTANCE_INFLUENCE : distanceInfluence;
}

public void setHeadingPenalty(double headingPenalty) {
Expand All @@ -126,43 +136,38 @@ public String toString() {

private String createContentString() {
// used to check against stored custom models, see #2026
return "distanceInfluence=" + distanceInfluence + "|speedFactor=" + speedFactorMap + "|maxSpeed=" + maxSpeedMap +
"|maxSpeedFallback=" + maxSpeedFallback + "|priorityMap=" + priorityMap + "|areas=" + areas;
return "distanceInfluence=" + distanceInfluence + "|headingPenalty=" + headingPenalty
+ "|speedStatements=" + speedStatements + "|priorityStatements=" + priorityStatements + "|areas=" + areas;
}

/**
* A new CustomModel is created from the baseModel merged with the specified queryModel.
*/
public static CustomModel merge(CustomModel baseModel, CustomModel queryModel) {
// avoid changing the specified CustomModel via deep copy otherwise the server-side CustomModel would be modified (same problem if queryModel would be used as target)
if (queryModel.isInternal())
throw new IllegalArgumentException("CustomModel in query cannot be internal");

// avoid changing the specified CustomModel via deep copy otherwise the server-side CustomModel would be
// modified (same problem if queryModel would be used as target)
CustomModel mergedCM = new CustomModel(baseModel);
if (queryModel.maxSpeedFallback != null) {
if (mergedCM.maxSpeedFallback != null && mergedCM.maxSpeedFallback > queryModel.maxSpeedFallback)
throw new IllegalArgumentException("CustomModel in query can only use max_speed_fallback bigger or equal to " + mergedCM.maxSpeedFallback);
mergedCM.maxSpeedFallback = queryModel.maxSpeedFallback;
}
if (Math.abs(queryModel.distanceInfluence - CustomModel.DEFAULT_D_I) > 0.01) {
if (mergedCM.distanceInfluence > queryModel.distanceInfluence)
throw new IllegalArgumentException("CustomModel in query can only use distance_influence bigger or equal to " + mergedCM.distanceInfluence);
// we only overwrite the distance influence if a non-default value was used
if (queryModel.distanceInfluence != null) {
if (queryModel.distanceInfluence < mergedCM.getDistanceInfluence())
throw new IllegalArgumentException("CustomModel in query can only use " +
"distance_influence bigger or equal to " + mergedCM.getDistanceInfluence() +
", given: " + queryModel.distanceInfluence);
mergedCM.distanceInfluence = queryModel.distanceInfluence;
}

// example
// max_speed: { road_class: { secondary : 0.4 } }
// or
// priority: { max_weight: { "<3.501": 0.7 } }
for (Map.Entry<String, Object> queryEntry : queryModel.getMaxSpeed().entrySet()) {
Object value = mergedCM.maxSpeedMap.get(queryEntry.getKey());
applyChange(mergedCM.maxSpeedMap, value, queryEntry);
}
for (Map.Entry<String, Object> queryEntry : queryModel.getSpeedFactor().entrySet()) {
Object value = mergedCM.speedFactorMap.get(queryEntry.getKey());
applyChange(mergedCM.speedFactorMap, value, queryEntry);
}
for (Map.Entry<String, Object> queryEntry : queryModel.getPriority().entrySet()) {
Object value = mergedCM.priorityMap.get(queryEntry.getKey());
applyChange(mergedCM.priorityMap, value, queryEntry);
}
checkFirst(queryModel.getSpeed());
checkFirst(queryModel.getPriority());

check(queryModel.getPriority());
check(queryModel.getSpeed());

mergedCM.speedStatements.addAll(queryModel.getSpeed());
mergedCM.priorityStatements.addAll(queryModel.getPriority());

for (Map.Entry<String, JsonFeature> entry : queryModel.getAreas().entrySet()) {
if (mergedCM.areas.containsKey(entry.getKey()))
throw new IllegalArgumentException("area " + entry.getKey() + " already exists");
Expand All @@ -172,92 +177,15 @@ public static CustomModel merge(CustomModel baseModel, CustomModel queryModel) {
return mergedCM;
}

private static void applyChange(Map<String, Object> mergedSuperMap,
Object mergedObj, Map.Entry<String, Object> querySuperEntry) {
if (mergedObj == null) {
// no need for a merge
mergedSuperMap.put(querySuperEntry.getKey(), querySuperEntry.getValue());
return;
}
if (!(mergedObj instanceof Map))
throw new IllegalArgumentException(querySuperEntry.getKey() + ": entry is not a map: " + mergedObj);
Object queryObj = querySuperEntry.getValue();
if (!(queryObj instanceof Map))
throw new IllegalArgumentException(querySuperEntry.getKey() + ": query entry is not a map: " + queryObj);

Map<Object, Object> mergedMap = (Map) mergedObj;
Map<Object, Object> queryMap = (Map) queryObj;
for (Map.Entry queryEntry : queryMap.entrySet()) {
if (queryEntry.getKey() == null || queryEntry.getKey().toString().isEmpty())
throw new IllegalArgumentException(querySuperEntry.getKey() + ": key cannot be null or empty");
String key = queryEntry.getKey().toString();
if (isComparison(key))
continue;

Object mergedValue = mergedMap.get(key);
if (mergedValue == null) {
mergedMap.put(key, queryEntry.getValue());
} else if (multiply(queryEntry.getValue(), mergedValue) != null) {
// existing value needs to be multiplied
mergedMap.put(key, multiply(queryEntry.getValue(), mergedValue));
} else {
throw new IllegalArgumentException(querySuperEntry.getKey() + ": cannot merge value " + queryEntry.getValue() + " for key " + key + ", merged value: " + mergedValue);
}
}

// now special handling for comparison keys start e.g. <2 or >3.0, see testMergeComparisonKeys
// this could be simplified if CustomModel would be already an abstract syntax tree :)
List<String> queryComparisonKeys = getComparisonKeys(queryMap);
if (queryComparisonKeys.isEmpty())
return;
if (queryComparisonKeys.size() > 1)
throw new IllegalArgumentException(querySuperEntry.getKey() + ": entry in " + querySuperEntry.getValue() + " must not contain more than one key comparison but contained " + queryComparisonKeys);
char opChar = queryComparisonKeys.get(0).charAt(0);
List<String> mergedComparisonKeys = getComparisonKeys(mergedMap);
if (mergedComparisonKeys.isEmpty()) {
mergedMap.put(queryComparisonKeys.get(0), queryMap.get(queryComparisonKeys.get(0)));
} else if (mergedComparisonKeys.get(0).charAt(0) == opChar) {
if (multiply(queryMap.get(queryComparisonKeys.get(0)), mergedMap.get(mergedComparisonKeys.get(0))) != 0)
throw new IllegalArgumentException(querySuperEntry.getKey() + ": currently only blocking comparisons are allowed, but query was " + queryMap.get(queryComparisonKeys.get(0)) + " and server side: " + mergedMap.get(mergedComparisonKeys.get(0)));

try {
double comparisonMergedValue = Double.parseDouble(mergedComparisonKeys.get(0).substring(1));
double comparisonQueryValue = Double.parseDouble(queryComparisonKeys.get(0).substring(1));
if (opChar == '<') {
if (comparisonMergedValue > comparisonQueryValue)
throw new IllegalArgumentException(querySuperEntry.getKey() + ": only use a comparison key with a bigger value than " + comparisonMergedValue + " but was " + comparisonQueryValue);
} else if (opChar == '>') {
if (comparisonMergedValue < comparisonQueryValue)
throw new IllegalArgumentException(querySuperEntry.getKey() + ": only use a comparison key with a smaller value than " + comparisonMergedValue + " but was " + comparisonQueryValue);
} else {
throw new IllegalArgumentException(querySuperEntry.getKey() + ": only use a comparison key with < or > as operator but was " + opChar);
}
mergedMap.remove(mergedComparisonKeys.get(0));
mergedMap.put(queryComparisonKeys.get(0), queryMap.get(queryComparisonKeys.get(0)));
} catch (NumberFormatException ex) {
throw new IllegalArgumentException(querySuperEntry.getKey() + ": number in one of the 'comparison' keys for " + querySuperEntry.getKey() + " wasn't parsable: " + queryComparisonKeys + " (" + mergedComparisonKeys + ")");
}
} else {
throw new IllegalArgumentException(querySuperEntry.getKey() + ": comparison keys must match but did not: " + queryComparisonKeys.get(0) + " vs " + mergedComparisonKeys.get(0));
}
}

static Double multiply(Object queryValue, Object mergedValue) {
if (queryValue instanceof Number && mergedValue instanceof Number)
return ((Number) queryValue).doubleValue() * ((Number) mergedValue).doubleValue();
return null;
}

static boolean isComparison(String key) {
return key.startsWith("<") || key.startsWith(">");
private static void checkFirst(List<Statement> priority) {
if (!priority.isEmpty() && priority.get(0).getKeyword() != Statement.Keyword.IF)
throw new IllegalArgumentException("First statement needs to be an if statement but was " + priority.get(0).getKeyword().getName());
}

private static List<String> getComparisonKeys(Map<Object, Object> map) {
List<String> list = new ArrayList<>();
for (Map.Entry queryEntry : map.entrySet()) {
String key = queryEntry.getKey().toString();
if (isComparison(key)) list.add(key);
private static void check(List<Statement> list) {
for (Statement statement : list) {
if (statement.getOperation() == Statement.Op.MULTIPLY && statement.getValue() > 1)
throw new IllegalArgumentException("factor cannot be larger than 1 but was " + statement.getValue());
}
return list;
}
}