From cc6102ac26c46a4c6b78c9150fcf1b9db7db58cf Mon Sep 17 00:00:00 2001 From: Dominic Burger Date: Wed, 3 Apr 2024 16:27:26 +0200 Subject: [PATCH 1/3] Add InterlisExpressionParser for selections --- build.gradle | 1 + .../functions/InterlisExpressionParser.java | 51 +++++++++++++ .../ExpressionParser/ExpressionParser.ili | 14 ++++ .../InterlisExpressionParserTest.java | 75 +++++++++++++++++++ 4 files changed, 141 insertions(+) create mode 100644 src/main/java/ch/geowerkstatt/ilivalidator/extensions/functions/InterlisExpressionParser.java create mode 100644 src/test/data/ExpressionParser/ExpressionParser.ili create mode 100644 src/test/java/ch/geowerkstatt/ilivalidator/extensions/functions/InterlisExpressionParserTest.java diff --git a/build.gradle b/build.gradle index a2c1361..fdc9b95 100644 --- a/build.gradle +++ b/build.gradle @@ -18,6 +18,7 @@ repositories { } dependencies { + implementation group: 'antlr', name: 'antlr', version: '2.7.7' implementation group: 'ch.interlis', name: 'iox-api', version: '1.0.4' implementation group: 'ch.interlis', name: 'iox-ili', version: '1.22.0' implementation group: 'ch.interlis', name: 'ili2c-tool', version: "5.4.0" diff --git a/src/main/java/ch/geowerkstatt/ilivalidator/extensions/functions/InterlisExpressionParser.java b/src/main/java/ch/geowerkstatt/ilivalidator/extensions/functions/InterlisExpressionParser.java new file mode 100644 index 0000000..2595cb4 --- /dev/null +++ b/src/main/java/ch/geowerkstatt/ilivalidator/extensions/functions/InterlisExpressionParser.java @@ -0,0 +1,51 @@ +package ch.geowerkstatt.ilivalidator.extensions.functions; + +import antlr.RecognitionException; +import antlr.TokenStream; +import antlr.TokenStreamException; +import ch.interlis.ili2c.metamodel.Evaluable; +import ch.interlis.ili2c.metamodel.ExpressionSelection; +import ch.interlis.ili2c.metamodel.Selection; +import ch.interlis.ili2c.metamodel.TransferDescription; +import ch.interlis.ili2c.metamodel.Viewable; +import ch.interlis.ili2c.parser.Ili24Lexer; +import ch.interlis.ili2c.parser.Ili24Parser; + +import java.io.StringReader; + +public final class InterlisExpressionParser extends Ili24Parser { + private InterlisExpressionParser(TransferDescription td, TokenStream lexer) { + super(lexer); + this.td = td; + } + + /** + * Create a new parser instance. + * + * @param td the {@link TransferDescription} instance. + * @param expression the INTERLIS expression to parse. + * + * @return the new parser instance. + */ + public static InterlisExpressionParser createParser(TransferDescription td, String expression) { + Ili24Lexer lexer = new Ili24Lexer(new StringReader(expression)); + return new InterlisExpressionParser(td, lexer); + } + + /** + * Parse the given expression to an {@link Evaluable} condition. + * The expression is expected to be a selection ({@code WHERE ;}). + * + * @param viewable the {@link Viewable} that represents the context of {@code THIS}. + * + * @return the parsed {@link Evaluable} condition. + */ + public Evaluable parseWhereExpression(Viewable viewable) { + try { + Selection selection = selection(viewable, viewable); + return ((ExpressionSelection) selection).getCondition(); + } catch (RecognitionException | TokenStreamException e) { + throw new RuntimeException(e); + } + } +} diff --git a/src/test/data/ExpressionParser/ExpressionParser.ili b/src/test/data/ExpressionParser/ExpressionParser.ili new file mode 100644 index 0000000..f09bf38 --- /dev/null +++ b/src/test/data/ExpressionParser/ExpressionParser.ili @@ -0,0 +1,14 @@ +INTERLIS 2.4; + +MODEL TestSuite + AT "mailto:info@geowerkstatt.ch" VERSION "2024-04-03" = + + TOPIC FunctionTestTopic = + + CLASS TestClass = + enumAttr: (a, b, c); + END TestClass; + + END FunctionTestTopic; + +END TestSuite. diff --git a/src/test/java/ch/geowerkstatt/ilivalidator/extensions/functions/InterlisExpressionParserTest.java b/src/test/java/ch/geowerkstatt/ilivalidator/extensions/functions/InterlisExpressionParserTest.java new file mode 100644 index 0000000..b608b72 --- /dev/null +++ b/src/test/java/ch/geowerkstatt/ilivalidator/extensions/functions/InterlisExpressionParserTest.java @@ -0,0 +1,75 @@ +package ch.geowerkstatt.ilivalidator.extensions.functions; + +import ch.ehi.basics.settings.Settings; +import ch.interlis.ili2c.Ili2c; +import ch.interlis.ili2c.Ili2cFailure; +import ch.interlis.ili2c.metamodel.Evaluable; +import ch.interlis.ili2c.metamodel.Expression; +import ch.interlis.ili2c.metamodel.TransferDescription; +import ch.interlis.ili2c.metamodel.Viewable; +import ch.interlis.iom.IomObject; +import ch.interlis.iom_j.Iom_jObject; +import ch.interlis.iox_j.PipelinePool; +import ch.interlis.iox_j.logging.LogEventFactory; +import ch.interlis.iox_j.validator.ValidationConfig; +import ch.interlis.iox_j.validator.Validator; +import ch.interlis.iox_j.validator.Value; +import com.vividsolutions.jts.util.Assert; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; + +class InterlisExpressionParserTest { + private static final String ILI_FILE = "src/test/data/ExpressionParser/ExpressionParser.ili"; + private static final String CLASS_NAME = "TestSuite.FunctionTestTopic.TestClass"; + private TransferDescription td; + private Viewable viewable; + private LogCollector logCollector; + private Validator validator; + + @BeforeEach + public void setUp() throws Ili2cFailure { + ArrayList modelFiles = new ArrayList<>(); + modelFiles.add(ILI_FILE); + td = Ili2c.compileIliFiles(modelFiles, new ArrayList()); + viewable = (Viewable) td.getElement(CLASS_NAME); + + logCollector = new LogCollector(); + validator = new Validator(td, new ValidationConfig(), logCollector, new LogEventFactory(), new PipelinePool(), new Settings()); + } + + @Test + public void parseWhereExpressionTrueResult() { + InterlisExpressionParser parser = InterlisExpressionParser.createParser(td, "WHERE THIS->enumAttr == #a;"); + Evaluable evaluable = parser.parseWhereExpression(viewable); + + Assert.equals(Expression.Equality.class, evaluable.getClass()); + + IomObject iomObject = createIomObject("1", "a"); + Value value = validator.evaluateExpression(null, "test", "test", iomObject, evaluable, null); + + Assert.equals(0, logCollector.getErrs().size()); + Assert.isTrue(value.isTrue()); + } + + @Test + public void parseWhereExpressionFalseResult() { + InterlisExpressionParser parser = InterlisExpressionParser.createParser(td, "WHERE enumAttr != #c;"); + Evaluable evaluable = parser.parseWhereExpression(viewable); + + Assert.equals(Expression.Inequality.class, evaluable.getClass()); + + IomObject iomObject = createIomObject("2", "c"); + Value value = validator.evaluateExpression(null, "test", "test", iomObject, evaluable, null); + + Assert.equals(0, logCollector.getErrs().size()); + Assert.equals(false, value.isTrue()); + } + + private static IomObject createIomObject(String oid, String enumAttrValue) { + IomObject iomObject = new Iom_jObject(CLASS_NAME, oid); + iomObject.setattrvalue("enumAttr", enumAttrValue); + return iomObject; + } +} From 25433c558839f00d374cef44487c9b6aa6577180 Mon Sep 17 00:00:00 2001 From: Dominic Burger Date: Thu, 4 Apr 2024 14:55:03 +0200 Subject: [PATCH 2/3] Add Filter function --- .../functions/EvaluationHelper.java | 42 +++++++++ .../extensions/functions/FilterIoxPlugin.java | 86 +++++++++++++++++++ src/model/GeoW_FunctionsExt.ili | 8 ++ src/model/GeoW_FunctionsExt_23.ili | 8 ++ 4 files changed, 144 insertions(+) create mode 100644 src/main/java/ch/geowerkstatt/ilivalidator/extensions/functions/FilterIoxPlugin.java diff --git a/src/main/java/ch/geowerkstatt/ilivalidator/extensions/functions/EvaluationHelper.java b/src/main/java/ch/geowerkstatt/ilivalidator/extensions/functions/EvaluationHelper.java index e9d955d..ee73ffc 100644 --- a/src/main/java/ch/geowerkstatt/ilivalidator/extensions/functions/EvaluationHelper.java +++ b/src/main/java/ch/geowerkstatt/ilivalidator/extensions/functions/EvaluationHelper.java @@ -14,7 +14,9 @@ import java.util.ArrayList; import java.util.Collection; import java.util.Iterator; +import java.util.Set; import java.util.function.Function; +import java.util.stream.Collectors; public final class EvaluationHelper { @@ -62,6 +64,46 @@ public static Viewable getContextClass(TransferDescription td, IomObject iomObje return null; } + /** + * Get the common base class of the given {@code objects}. + * + * @param td the {@link TransferDescription} instance. + * @param objects the collection of {@link IomObject} to find the common base class of. + * + * @return the common base class of the given {@code objects} or {@code null} if no common base class could be found. + */ + public static Viewable getCommonBaseClass(TransferDescription td, Collection objects) { + if (objects.isEmpty()) { + return null; + } + + Set classNames = objects.stream() + .map(IomObject::getobjecttag) + .collect(Collectors.toSet()); + Viewable firstClass = (Viewable) td.getElement(classNames.iterator().next()); + if (classNames.size() == 1) { + return firstClass; + } + + return classNames.stream() + .map(className -> (Viewable) td.getElement(className)) + .reduce(firstClass, EvaluationHelper::getCommonBaseClass); + } + + private static Viewable getCommonBaseClass(Viewable classA, Viewable classB) { + if (classA == null || classB == null) { + return null; + } + Viewable currentClass = classA; + while (currentClass != null) { + if (currentClass == classB || classB.isExtending(currentClass)) { + return currentClass; + } + currentClass = (Viewable) currentClass.getExtending(); + } + return null; + } + /** * Get the collection of {@link IomObject} inside {@code argObjects} by following the provided {@code attributePath}. */ diff --git a/src/main/java/ch/geowerkstatt/ilivalidator/extensions/functions/FilterIoxPlugin.java b/src/main/java/ch/geowerkstatt/ilivalidator/extensions/functions/FilterIoxPlugin.java new file mode 100644 index 0000000..e9a0dbe --- /dev/null +++ b/src/main/java/ch/geowerkstatt/ilivalidator/extensions/functions/FilterIoxPlugin.java @@ -0,0 +1,86 @@ +package ch.geowerkstatt.ilivalidator.extensions.functions; + +import ch.interlis.ili2c.metamodel.Evaluable; +import ch.interlis.ili2c.metamodel.Viewable; +import ch.interlis.iom.IomObject; +import ch.interlis.iox_j.validator.Value; + +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +public final class FilterIoxPlugin extends BaseInterlisFunction { + private static final HashMap EXPRESSION_CACHE = new HashMap<>(); + + @Override + public String getQualifiedIliName() { + return "GeoW_FunctionsExt.Filter"; + } + + @Override + protected Value evaluateInternal(String validationKind, String usageScope, IomObject contextObject, Value[] arguments) { + Value argObjects = arguments[0]; + Value argFilter = arguments[1]; + + if (argObjects.isUndefined() || argObjects.getComplexObjects() == null) { + return Value.createUndefined(); + } + + Collection objects = argObjects.getComplexObjects(); + if (objects.isEmpty()) { + return argObjects; + } + + Viewable objectClass = EvaluationHelper.getCommonBaseClass(td, objects); + if (objectClass == null) { + throw new IllegalStateException("Objects have no common base class in " + usageScope); + } + + ExpressionKey expressionKey = new ExpressionKey(objectClass, argFilter.getValue()); + Evaluable filter = EXPRESSION_CACHE.computeIfAbsent(expressionKey, key -> parseFilterExpression(key.objectClass, key.filter, usageScope)); + + List filteredObjects = objects.stream() + .filter(object -> { + Value value = validator.evaluateExpression(null, validationKind, usageScope, object, filter, null); + return value.skipEvaluation() || value.isTrue(); + }) + .collect(Collectors.toList()); + + return new Value(filteredObjects); + } + + private Evaluable parseFilterExpression(Viewable objectClass, String filter, String usageScope) { + InterlisExpressionParser parser = InterlisExpressionParser.createParser(td, filter); + parser.setFilename(getQualifiedIliName() + ":" + usageScope); + return parser.parseWhereExpression(objectClass); + } + + private static final class ExpressionKey { + private final Viewable objectClass; + private final String filter; + + private ExpressionKey(Viewable objectClass, String filter) { + this.objectClass = objectClass; + this.filter = filter; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof ExpressionKey)) { + return false; + } + ExpressionKey that = (ExpressionKey) o; + return Objects.equals(objectClass, that.objectClass) && Objects.equals(filter, that.filter); + } + + @Override + public int hashCode() { + return Objects.hash(objectClass, filter); + } + } +} diff --git a/src/model/GeoW_FunctionsExt.ili b/src/model/GeoW_FunctionsExt.ili index ab9c693..2f7ce84 100644 --- a/src/model/GeoW_FunctionsExt.ili +++ b/src/model/GeoW_FunctionsExt.ili @@ -91,4 +91,12 @@ MODEL GeoW_FunctionsExt !!@ fn.since = "2024-01-10"; !!sample = "MANDATORY CONSTRAINT INTERLIS.elementCount(GeoW_FunctionsExt.FindObjects("ZG_Nutzungsplanung_V1_1.TransferMetadaten.Amt", "Name", "Gemeinde Walchwil")) == 1"; FUNCTION FindObjects(ObjectClass: CLASS; FilterAttr: TEXT; FilterValue: ANYSTRUCTURE): BAG OF ANYSTRUCTURE; + + !!@ fn.description = "Filtert die Eingabemenge nach dem übergebenen Filterkriterium. Für 'Filter' soll eine Selection in INTERLIS 2 Syntax angegeben werden."; + !!@ fn.param = "Objects: Eingabemenge der Objekte."; + !!@ fn.param = "Filter: Filterkriterium in INTERLIS-Syntax (WHERE ;). THIS verweist jeweils auf das aktuelle Objekt."; + !!@ fn.return = "Alle Objekte, welche das Filterkriterium erfüllen"; + !!@ fn.since = "2024-04-04"; + !!sample = "MANDATORY CONSTRAINT INTERLIS.elementCount(GeoW_FunctionsExt.Filter(THIS->references, "WHERE active == #true;")) >= 1"; + FUNCTION Filter(Objects: BAG OF ANYSTRUCTURE; Filter: TEXT): BAG OF ANYSTRUCTURE; END GeoW_FunctionsExt. \ No newline at end of file diff --git a/src/model/GeoW_FunctionsExt_23.ili b/src/model/GeoW_FunctionsExt_23.ili index 03f9165..4659b3b 100644 --- a/src/model/GeoW_FunctionsExt_23.ili +++ b/src/model/GeoW_FunctionsExt_23.ili @@ -92,4 +92,12 @@ CONTRACTED MODEL GeoW_FunctionsExt !!@ fn.since = "2024-01-10"; !!sample = "MANDATORY CONSTRAINT INTERLIS.elementCount(GeoW_FunctionsExt.FindObjects("ZG_Nutzungsplanung_V1_1.TransferMetadaten.Amt", "Name", "Gemeinde Walchwil")) == 1"; FUNCTION FindObjects(ObjectClass: CLASS; FilterAttr: TEXT; FilterValue: ANYSTRUCTURE): BAG OF ANYSTRUCTURE; + + !!@ fn.description = "Filtert die Eingabemenge nach dem übergebenen Filterkriterium. Für 'Filter' soll eine Selection in INTERLIS 2 Syntax angegeben werden."; + !!@ fn.param = "Objects: Eingabemenge der Objekte."; + !!@ fn.param = "Filter: Filterkriterium in INTERLIS-Syntax (WHERE ;). THIS verweist jeweils auf das aktuelle Objekt."; + !!@ fn.return = "Alle Objekte, welche das Filterkriterium erfüllen"; + !!@ fn.since = "2024-04-04"; + !!sample = "MANDATORY CONSTRAINT INTERLIS.elementCount(GeoW_FunctionsExt.Filter(THIS->references, "WHERE active == #true;")) >= 1"; + FUNCTION Filter(Objects: BAG OF ANYSTRUCTURE; Filter: TEXT): BAG OF ANYSTRUCTURE; END GeoW_FunctionsExt. From 7a2402e36b1a58b1e629b8d82929e64131d5c388 Mon Sep 17 00:00:00 2001 From: Dominic Burger Date: Thu, 4 Apr 2024 16:00:32 +0200 Subject: [PATCH 3/3] Add tests for Filter function --- src/test/data/Filter/MandatoryConstraints.ili | 30 +++++++++++++ .../data/Filter/MandatoryConstraintsText.ili | 28 ++++++++++++ src/test/data/Filter/TestData.xtf | 45 +++++++++++++++++++ .../functions/FilterIoxPluginTest.java | 38 ++++++++++++++++ 4 files changed, 141 insertions(+) create mode 100644 src/test/data/Filter/MandatoryConstraints.ili create mode 100644 src/test/data/Filter/MandatoryConstraintsText.ili create mode 100644 src/test/data/Filter/TestData.xtf create mode 100644 src/test/java/ch/geowerkstatt/ilivalidator/extensions/functions/FilterIoxPluginTest.java diff --git a/src/test/data/Filter/MandatoryConstraints.ili b/src/test/data/Filter/MandatoryConstraints.ili new file mode 100644 index 0000000..546e917 --- /dev/null +++ b/src/test/data/Filter/MandatoryConstraints.ili @@ -0,0 +1,30 @@ +INTERLIS 2.4; + +MODEL TestSuite + AT "mailto:info@geowerkstatt.ch" VERSION "2024-04-04" = + IMPORTS GeoW_FunctionsExt; + + TOPIC FunctionTestTopic = + + STRUCTURE ReferencedStruct = + textAttr: TEXT*16; + enumAttr: (val1,val2,val3); + numberAttr: 0..10; + END ReferencedStruct; + + STRUCTURE ExtendedStruct EXTENDS ReferencedStruct = + newAttr: BOOLEAN; + END ExtendedStruct; + + CLASS BaseClass = + references: BAG {1..*} OF ReferencedStruct; + + MANDATORY CONSTRAINT trueConstraintEnumAttr: INTERLIS.elementCount(GeoW_FunctionsExt.Filter(THIS->references, "WHERE enumAttr == #val2;")) == 3; + MANDATORY CONSTRAINT trueConstraintNumberAttr: INTERLIS.elementCount(GeoW_FunctionsExt.Filter(references, "WHERE numberAttr >= 3 AND enumAttr != #val3;")) == 1; + MANDATORY CONSTRAINT falseConstraintEnumAttr: INTERLIS.elementCount(GeoW_FunctionsExt.Filter(references, "WHERE enumAttr == #val2;")) == 0; + MANDATORY CONSTRAINT falseConstraintNumberAttr: INTERLIS.elementCount(GeoW_FunctionsExt.Filter(THIS->references, "WHERE numberAttr == 3 AND enumAttr == #val3;")) > 0; + END BaseClass; + + END FunctionTestTopic; + +END TestSuite. diff --git a/src/test/data/Filter/MandatoryConstraintsText.ili b/src/test/data/Filter/MandatoryConstraintsText.ili new file mode 100644 index 0000000..e0e57cb --- /dev/null +++ b/src/test/data/Filter/MandatoryConstraintsText.ili @@ -0,0 +1,28 @@ +INTERLIS 2.4; + +MODEL TestSuite + AT "mailto:info@geowerkstatt.ch" VERSION "2024-04-04" = + IMPORTS GeoW_FunctionsExt; + + TOPIC FunctionTestTopic = + + STRUCTURE ReferencedStruct = + textAttr: TEXT*16; + enumAttr: (val1,val2,val3); + numberAttr: 0..10; + END ReferencedStruct; + + STRUCTURE ExtendedStruct EXTENDS ReferencedStruct = + newAttr: BOOLEAN; + END ExtendedStruct; + + CLASS BaseClass = + references: BAG {1..*} OF ReferencedStruct; + + MANDATORY CONSTRAINT trueConstraintTextAttr: INTERLIS.elementCount(GeoW_FunctionsExt.Filter(THIS->references, "WHERE textAttr == \"Some Value\";")) == 2; + MANDATORY CONSTRAINT falseConstraintTextAttr: INTERLIS.elementCount(GeoW_FunctionsExt.Filter(references, "WHERE textAttr == \"Value that does not exist\";")) > 0; + END BaseClass; + + END FunctionTestTopic; + +END TestSuite. diff --git a/src/test/data/Filter/TestData.xtf b/src/test/data/Filter/TestData.xtf new file mode 100644 index 0000000..db84c90 --- /dev/null +++ b/src/test/data/Filter/TestData.xtf @@ -0,0 +1,45 @@ + + + + + GeoW_FunctionsExt + TestSuite + + ili2gpkg-4.6.1-63db90def1260a503f0f2d4cb846686cd4851184 + + + + + + + Some Value + val2 + 2 + + + aaa + val2 + 2 + + + aaa + val2 + 2 + + + Some Value + val3 + 1 + false + + + bbb + val1 + 3 + + + + + + diff --git a/src/test/java/ch/geowerkstatt/ilivalidator/extensions/functions/FilterIoxPluginTest.java b/src/test/java/ch/geowerkstatt/ilivalidator/extensions/functions/FilterIoxPluginTest.java new file mode 100644 index 0000000..498015d --- /dev/null +++ b/src/test/java/ch/geowerkstatt/ilivalidator/extensions/functions/FilterIoxPluginTest.java @@ -0,0 +1,38 @@ +package ch.geowerkstatt.ilivalidator.extensions.functions; + +import ch.interlis.ili2c.Ili2cFailure; +import ch.interlis.iox.IoxException; +import com.vividsolutions.jts.util.Assert; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +class FilterIoxPluginTest { + protected static final String TEST_DATA = "Filter/TestData.xtf"; + private ValidationTestHelper vh; + + @BeforeEach + public void setUp() { + vh = new ValidationTestHelper(); + vh.addFunction(new FilterIoxPlugin()); + } + + @Test + public void mandatoryConstraint() throws Ili2cFailure, IoxException { + vh.runValidation(new String[]{TEST_DATA}, new String[]{"Filter/MandatoryConstraints.ili"}); + Assert.equals(2, vh.getErrs().size()); + AssertionHelper.assertNoConstraintError(vh, "trueConstraintEnumAttr"); + AssertionHelper.assertNoConstraintError(vh, "trueConstraintNumberAttr"); + AssertionHelper.assertConstraintErrors(vh, 1, "base", "falseConstraintEnumAttr"); + AssertionHelper.assertConstraintErrors(vh, 1, "base", "falseConstraintNumberAttr"); + } + + @Test + @Disabled("Escape sequences in strings (https://github.com/claeis/ili2c/issues/124)") + public void filterTextAttr() throws Ili2cFailure, IoxException { + vh.runValidation(new String[]{TEST_DATA}, new String[]{"Filter/MandatoryConstraintsText.ili"}); + Assert.equals(1, vh.getErrs().size()); + AssertionHelper.assertNoConstraintError(vh, "trueConstraintTextAttr"); + AssertionHelper.assertConstraintErrors(vh, 1, "base", "falseConstraintTextAttr"); + } +}