Skip to content

Add Filter function #51

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

Merged
merged 3 commits into from
Apr 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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:info@geowerkstatt.ch" 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: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.
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: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.
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