Skip to content

Commit

Permalink
Merge pull request #51 Add Filter function
Browse files Browse the repository at this point in the history
  • Loading branch information
domi-b authored Apr 10, 2024
2 parents b3278a2 + 7a2402e commit 08c7bfd
Show file tree
Hide file tree
Showing 12 changed files with 426 additions and 0 deletions.
1 change: 1 addition & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {

Expand Down Expand Up @@ -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<IomObject> objects) {
if (objects.isEmpty()) {
return null;
}

Set<String> 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}.
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -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<ExpressionKey, Evaluable> 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<IomObject> 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<IomObject> 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);
}
}
}
Original file line number Diff line number Diff line change
@@ -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 <logical-expression>;}).
*
* @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);
}
}
}
8 changes: 8 additions & 0 deletions src/model/GeoW_FunctionsExt.ili
Original file line number Diff line number Diff line change
Expand Up @@ -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 <logical-expression>;). 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.
8 changes: 8 additions & 0 deletions src/model/GeoW_FunctionsExt_23.ili
Original file line number Diff line number Diff line change
Expand Up @@ -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 <logical-expression>;). 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.
14 changes: 14 additions & 0 deletions src/test/data/ExpressionParser/ExpressionParser.ili
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
INTERLIS 2.4;

MODEL TestSuite
AT "mailto:[email protected]" VERSION "2024-04-03" =

TOPIC FunctionTestTopic =

CLASS TestClass =
enumAttr: (a, b, c);
END TestClass;

END FunctionTestTopic;

END TestSuite.
30 changes: 30 additions & 0 deletions src/test/data/Filter/MandatoryConstraints.ili
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
INTERLIS 2.4;

MODEL TestSuite
AT "mailto:[email protected]" 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.
28 changes: 28 additions & 0 deletions src/test/data/Filter/MandatoryConstraintsText.ili
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
INTERLIS 2.4;

MODEL TestSuite
AT "mailto:[email protected]" 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.
45 changes: 45 additions & 0 deletions src/test/data/Filter/TestData.xtf
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<?xml version="1.0" encoding="UTF-8"?>
<ili:transfer xmlns:ili="http://www.interlis.ch/xtf/2.4/INTERLIS" xmlns:geom="http://www.interlis.ch/geometry/1.0"
xmlns:TestSuite="http://www.interlis.ch/xtf/2.4/TestSuite">
<ili:headersection>
<ili:models>
<ili:model>GeoW_FunctionsExt</ili:model>
<ili:model>TestSuite</ili:model>
</ili:models>
<ili:sender>ili2gpkg-4.6.1-63db90def1260a503f0f2d4cb846686cd4851184</ili:sender>
</ili:headersection>
<ili:datasection>
<TestSuite:FunctionTestTopic ili:bid="TestSuite.FunctionTestTopic">
<TestSuite:BaseClass ili:tid="base">
<TestSuite:references>
<TestSuite:ReferencedStruct>
<TestSuite:textAttr>Some Value</TestSuite:textAttr>
<TestSuite:enumAttr>val2</TestSuite:enumAttr>
<TestSuite:numberAttr>2</TestSuite:numberAttr>
</TestSuite:ReferencedStruct>
<TestSuite:ReferencedStruct>
<TestSuite:textAttr>aaa</TestSuite:textAttr>
<TestSuite:enumAttr>val2</TestSuite:enumAttr>
<TestSuite:numberAttr>2</TestSuite:numberAttr>
</TestSuite:ReferencedStruct>
<TestSuite:ReferencedStruct>
<TestSuite:textAttr>aaa</TestSuite:textAttr>
<TestSuite:enumAttr>val2</TestSuite:enumAttr>
<TestSuite:numberAttr>2</TestSuite:numberAttr>
</TestSuite:ReferencedStruct>
<TestSuite:ExtendedStruct>
<TestSuite:textAttr>Some Value</TestSuite:textAttr>
<TestSuite:enumAttr>val3</TestSuite:enumAttr>
<TestSuite:numberAttr>1</TestSuite:numberAttr>
<TestSuite:newAttr>false</TestSuite:newAttr>
</TestSuite:ExtendedStruct>
<TestSuite:ReferencedStruct>
<TestSuite:textAttr>bbb</TestSuite:textAttr>
<TestSuite:enumAttr>val1</TestSuite:enumAttr>
<TestSuite:numberAttr>3</TestSuite:numberAttr>
</TestSuite:ReferencedStruct>
</TestSuite:references>
</TestSuite:BaseClass>
</TestSuite:FunctionTestTopic>
</ili:datasection>
</ili:transfer>
Original file line number Diff line number Diff line change
@@ -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");
}
}
Loading

0 comments on commit 08c7bfd

Please sign in to comment.