Skip to content

Commit

Permalink
Only verify FEEL expressions; warn that others are currently unsupported
Browse files Browse the repository at this point in the history
Currently, `dmn-check` only supports analysis of FEEL expressions. With this
commit the expression language that is specified in the DMN file is honored and
a warning is issued for all other expression languages.

Support for other expression languages is on the roadmap but with no schedule so
far. This feature makes `dmn-check` useable if other expression languages are
used in a DMN file, see issue #88.

Closes issue #83.
  • Loading branch information
pSub committed Apr 10, 2022
1 parent 4fc4d15 commit 1a69f42
Show file tree
Hide file tree
Showing 21 changed files with 441 additions and 38 deletions.
7 changes: 7 additions & 0 deletions validators/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,13 @@
<version>3.1</version>
</dependency>

<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>${mockito.version}</version>
<scope>test</scope>
</dependency>

</dependencies>

</project>
20 changes: 20 additions & 0 deletions validators/src/main/java/de/redsix/dmncheck/feel/FeelParser.java
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
package de.redsix.dmncheck.feel;

import de.redsix.dmncheck.result.Severity;
import de.redsix.dmncheck.result.ValidationResult;
import de.redsix.dmncheck.util.Either;
import de.redsix.dmncheck.util.Eithers;
import de.redsix.dmncheck.util.Expression;
import org.camunda.bpm.model.dmn.impl.DmnModelConstants;
import org.camunda.bpm.model.dmn.instance.LiteralExpression;
import org.camunda.bpm.model.dmn.instance.UnaryTests;
import org.jparsec.OperatorTable;
import org.jparsec.Parser;
import org.jparsec.Parsers;
Expand All @@ -13,6 +18,7 @@
import org.jparsec.pattern.Patterns;

import java.time.LocalDateTime;
import java.util.Arrays;

public final class FeelParser {

Expand Down Expand Up @@ -140,6 +146,11 @@ private static Parser<FeelExpression> feelExpressionParser() {
return Parsers.or(parseEmpty(), feelExpressionParser);
}

private static boolean expressionLanguageIsFeel(final String expressionLanguage) {
return Arrays.asList(DmnModelConstants.FEEL_NS, DmnModelConstants.FEEL12_NS, DmnModelConstants.FEEL13_NS).contains(expressionLanguage)
|| expressionLanguage.equalsIgnoreCase("feel");
}

static final Parser<FeelExpression> PARSER = feelExpressionParser().from(TOKENIZER, IGNORED);

public static Either<ValidationResult.Builder.ElementStep, FeelExpression> parse(final CharSequence charSequence) {
Expand All @@ -149,6 +160,15 @@ public static Either<ValidationResult.Builder.ElementStep, FeelExpression> parse
return Eithers.left(ValidationResult.init.message("Could not parse '" + charSequence + "': " + e.getMessage()));
}
}

public static Either<ValidationResult.Builder.ElementStep, FeelExpression> parse(final Expression expression) {
if (expressionLanguageIsFeel(expression.expressionLanguage)) {
return parse(expression.textContent);
} else {
return Eithers.left(ValidationResult.init.message("Expression language '" +
expression.expressionLanguage + "' not supported").severity(Severity.WARNING));
}
}
}


28 changes: 28 additions & 0 deletions validators/src/main/java/de/redsix/dmncheck/util/Expression.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package de.redsix.dmncheck.util;

import org.camunda.bpm.model.dmn.impl.DmnModelConstants;
import org.camunda.bpm.model.dmn.instance.LiteralExpression;
import org.camunda.bpm.model.dmn.instance.UnaryTests;

import java.util.Objects;

public class Expression {

public final String textContent;
public final String expressionLanguage;

public Expression(final UnaryTests unaryTests, final String toplevelExpressionLanguage) {
this.textContent = unaryTests.getTextContent();
this.expressionLanguage = decideExpressionLanguage(unaryTests.getExpressionLanguage(), toplevelExpressionLanguage);
}

public Expression(final LiteralExpression literalExpression, final String toplevelExpressionLanguage) {
this.textContent = literalExpression.getTextContent();
this.expressionLanguage = decideExpressionLanguage(literalExpression.getExpressionLanguage(), toplevelExpressionLanguage);
}

private String decideExpressionLanguage(final String localExpressionLanguage, final String toplevelExpressionLanguage) {
return Objects.requireNonNullElseGet(localExpressionLanguage,
() -> Objects.requireNonNullElse(toplevelExpressionLanguage, DmnModelConstants.FEEL_NS));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package de.redsix.dmncheck.util;

import org.camunda.bpm.model.dmn.instance.LiteralExpression;
import org.camunda.bpm.model.dmn.instance.UnaryTests;

public class TopLevelExpressionLanguage {

public final String topLevelExpressionLanguage;

public TopLevelExpressionLanguage(final String topLevelExpressionLanguage) {
this.topLevelExpressionLanguage = topLevelExpressionLanguage;
}

public Expression toExpression(final UnaryTests unaryTests) {
return new Expression(unaryTests, topLevelExpressionLanguage);
}

public Expression toExpression(final LiteralExpression literalExpression) {
return new Expression(literalExpression, topLevelExpressionLanguage);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@
import de.redsix.dmncheck.feel.FeelParser;
import de.redsix.dmncheck.result.ValidationResult;
import de.redsix.dmncheck.util.Either;
import de.redsix.dmncheck.util.Expression;
import de.redsix.dmncheck.util.TopLevelExpressionLanguage;
import de.redsix.dmncheck.validators.core.RequirementGraphValidator;
import org.camunda.bpm.model.dmn.DmnModelInstance;
import org.camunda.bpm.model.dmn.instance.Decision;
import org.camunda.bpm.model.dmn.instance.DecisionTable;
import org.camunda.bpm.model.dmn.instance.DrgElement;
Expand All @@ -24,6 +27,14 @@

public class ConnectedRequirementGraphValidator extends RequirementGraphValidator {

private TopLevelExpressionLanguage toplevelExpressionLanguage;

@Override
public List<ValidationResult> apply(final DmnModelInstance dmnModelInstance) {
toplevelExpressionLanguage = new TopLevelExpressionLanguage(dmnModelInstance.getDefinitions().getExpressionLanguage());
return super.apply(dmnModelInstance);
}

@Override
public List<ValidationResult> validate(RequirementGraph drg) {
ConnectivityInspector<DrgElement, DefaultEdge> connectivityInspector = new ConnectivityInspector<>(drg);
Expand Down Expand Up @@ -59,7 +70,7 @@ private List<ValidationResult> checkInAndOutputs(Decision sourceDecision, Decisi

final Either<ValidationResult.Builder.ElementStep, List<FeelExpression>> eitherInputExpressions = targetDecisionTable.getInputs().stream()
.map(Input::getInputExpression)
.map(InputExpression::getTextContent)
.map(toplevelExpressionLanguage::toExpression)
.map(FeelParser::parse)
.collect(Either.reduce());

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import de.redsix.dmncheck.feel.ExpressionTypeParser;
import de.redsix.dmncheck.result.ValidationResult;
import de.redsix.dmncheck.util.Either;
import de.redsix.dmncheck.util.Expression;
import de.redsix.dmncheck.validators.core.ValidationContext;
import org.camunda.bpm.model.dmn.instance.DecisionTable;
import org.camunda.bpm.model.dmn.instance.Input;
Expand Down Expand Up @@ -34,7 +35,10 @@ public List<ValidationResult> validate(DecisionTable decisionTable, ValidationCo

return eitherInputTypes.match(
validationResult -> Stream.of(validationResult.element(rule).build()),
inputTypes -> typecheck(rule, rule.getInputEntries().stream(), inputVariables, inputTypes.stream()));
inputTypes -> typecheck(rule,
rule.getInputEntries().stream().map(toplevelExpressionLanguage::toExpression),
inputVariables,
inputTypes.stream()));
}).collect(Collectors.toList());
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import de.redsix.dmncheck.feel.ExpressionTypeParser;
import de.redsix.dmncheck.result.ValidationResult;
import de.redsix.dmncheck.util.Expression;
import de.redsix.dmncheck.validators.core.ValidationContext;
import org.camunda.bpm.model.dmn.instance.Input;

Expand All @@ -25,8 +26,9 @@ public List<ValidationResult> validate(final Input input, ValidationContext vali

return ExpressionTypeParser.parse(expressionType, validationContext.getItemDefinitions())
.match(validationResult -> Collections.singletonList(validationResult.element(input).build()),
inputType -> typecheck(input, Stream.of(input.getInputValues()), Stream.of(inputType))
.collect(Collectors.toList()));
inputType -> typecheck(input,
Stream.of(input.getInputValues()).map(toplevelExpressionLanguage::toExpression),
Stream.of(inputType)).collect(Collectors.toList()));
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import de.redsix.dmncheck.feel.ExpressionTypeParser;
import de.redsix.dmncheck.result.Severity;
import de.redsix.dmncheck.result.ValidationResult;
import de.redsix.dmncheck.util.Expression;
import de.redsix.dmncheck.validators.core.ValidationContext;
import org.camunda.bpm.model.dmn.instance.ItemDefinition;

Expand Down Expand Up @@ -44,8 +45,9 @@ public List<ValidationResult> validate(ItemDefinition itemDefinition, Validation
return ExpressionTypeParser
.parse(expressionType, validationContext.getItemDefinitions())
.match(validationResult -> Stream.of(validationResult.element(itemDefinitionOrComponent).build()),
inputType -> typecheck(itemDefinitionOrComponent, Stream.of(itemDefinitionOrComponent.getAllowedValues()),
Stream.of(inputType)));
inputType -> typecheck(itemDefinitionOrComponent,
Stream.of(itemDefinitionOrComponent.getAllowedValues()).map(toplevelExpressionLanguage::toExpression),
Stream.of(inputType)));
}})
.collect(Collectors.toList());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import de.redsix.dmncheck.feel.ExpressionTypeParser;
import de.redsix.dmncheck.result.ValidationResult;
import de.redsix.dmncheck.util.Either;
import de.redsix.dmncheck.util.Expression;
import de.redsix.dmncheck.validators.core.ValidationContext;
import org.camunda.bpm.model.dmn.instance.DecisionTable;
import org.camunda.bpm.model.dmn.instance.OutputClause;
Expand Down Expand Up @@ -32,8 +33,9 @@ public List<ValidationResult> validate(DecisionTable decisionTable, ValidationCo
return decisionTable.getRules().stream().flatMap(rule ->
eitherOutputTypes.match(
validationResult -> Stream.of(validationResult.element(rule).build()),
outputTypes -> typecheck(rule, rule.getOutputEntries().stream(), outputTypes.stream())))
.collect(Collectors.toList());
outputTypes -> typecheck(rule,
rule.getOutputEntries().stream().map(toplevelExpressionLanguage::toExpression),
outputTypes.stream()))).collect(Collectors.toList());
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import de.redsix.dmncheck.feel.ExpressionTypeParser;
import de.redsix.dmncheck.result.ValidationResult;
import de.redsix.dmncheck.util.Expression;
import de.redsix.dmncheck.validators.core.ValidationContext;
import org.camunda.bpm.model.dmn.instance.Output;

Expand All @@ -25,8 +26,9 @@ public List<ValidationResult> validate(final Output output, ValidationContext va

return ExpressionTypeParser.parse(expressionType, validationContext.getItemDefinitions())
.match(validationResult -> Collections.singletonList(validationResult.element(output).build()),
inputType -> typecheck(output, Stream.of(output.getOutputValues()), Stream.of(inputType))
.collect(Collectors.toList()));
inputType -> typecheck(output,
Stream.of(output.getOutputValues()).map(toplevelExpressionLanguage::toExpression),
Stream.of(inputType)).collect(Collectors.toList()));
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,10 @@

import de.redsix.dmncheck.feel.FeelParser;
import de.redsix.dmncheck.result.ValidationResult;
import de.redsix.dmncheck.util.Either;
import de.redsix.dmncheck.util.Eithers;
import de.redsix.dmncheck.util.Util;
import de.redsix.dmncheck.util.*;
import de.redsix.dmncheck.validators.core.SimpleValidator;
import de.redsix.dmncheck.validators.core.ValidationContext;
import org.camunda.bpm.model.dmn.DmnModelInstance;
import org.camunda.bpm.model.dmn.HitPolicy;
import org.camunda.bpm.model.dmn.instance.DecisionTable;
import org.camunda.bpm.model.dmn.instance.InputEntry;
Expand All @@ -24,6 +23,14 @@

public class ShadowedRuleValidator extends SimpleValidator<DecisionTable> {

private TopLevelExpressionLanguage toplevelExpressionLanguage;

@Override
public List<ValidationResult> apply(final DmnModelInstance dmnModelInstance) {
toplevelExpressionLanguage = new TopLevelExpressionLanguage(dmnModelInstance.getDefinitions().getExpressionLanguage());
return super.apply(dmnModelInstance);
}

@Override
public Class<DecisionTable> getClassUnderValidation() {
return DecisionTable.class;
Expand Down Expand Up @@ -63,8 +70,8 @@ private Either<ValidationResult.Builder.ElementStep, List<Optional<Boolean>>> co

private Either<ValidationResult.Builder.ElementStep, Optional<Boolean>> checkInputsForSubsumption(final InputEntry input,
final InputEntry potentiallySubsumingInput) {
return FeelParser.parse(input.getTextContent()).bind(inputExpression ->
FeelParser.parse(potentiallySubsumingInput.getTextContent()).bind(potentiallySubsumingInputExpression ->
return FeelParser.parse(toplevelExpressionLanguage.toExpression(input)).bind(inputExpression ->
FeelParser.parse(toplevelExpressionLanguage.toExpression(potentiallySubsumingInput)).bind(potentiallySubsumingInputExpression ->
Eithers.right(potentiallySubsumingInputExpression.subsumes(inputExpression))));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,12 @@
import de.redsix.dmncheck.feel.FeelTypecheck;
import de.redsix.dmncheck.result.Severity;
import de.redsix.dmncheck.result.ValidationResult;
import de.redsix.dmncheck.util.Either;
import de.redsix.dmncheck.util.ProjectClassLoader;
import de.redsix.dmncheck.util.Util;
import de.redsix.dmncheck.util.*;
import de.redsix.dmncheck.validators.core.SimpleValidator;
import org.camunda.bpm.model.dmn.DmnModelInstance;
import org.camunda.bpm.model.dmn.instance.DmnElement;
import org.camunda.bpm.model.dmn.instance.LiteralExpression;
import org.camunda.bpm.model.dmn.instance.UnaryTests;
import org.camunda.bpm.model.xml.instance.ModelElementInstance;

import java.util.Arrays;
Expand All @@ -25,10 +26,18 @@

public abstract class TypeValidator<T extends ModelElementInstance> extends SimpleValidator<T> {

TopLevelExpressionLanguage toplevelExpressionLanguage;

abstract String errorMessage();

Stream<ValidationResult> typecheck(final DmnElement dmnElement, final Stream<? extends DmnElement> expressions, final Stream<String> variables,
final Stream<ExpressionType> types) {
@Override
public List<ValidationResult> apply(final DmnModelInstance dmnModelInstance) {
toplevelExpressionLanguage = new TopLevelExpressionLanguage(dmnModelInstance.getDefinitions().getExpressionLanguage());
return super.apply(dmnModelInstance);
}

Stream<ValidationResult> typecheck(final DmnElement dmnElement, final Stream<Expression> expressions, final Stream<String> variables,
final Stream<ExpressionType> types) {
final Stream<Optional<ValidationResult.Builder.ElementStep>> intermediateResults = Util
.zip(expressions, variables, types, (expression, variable, type) -> {
final FeelTypecheck.Context context = new FeelTypecheck.Context();
Expand All @@ -41,24 +50,24 @@ Stream<ValidationResult> typecheck(final DmnElement dmnElement, final Stream<? e
return buildValidationResults(intermediateResults, dmnElement);
}

Stream<ValidationResult> typecheck(final DmnElement dmnElement, final Stream<? extends DmnElement> expressions,
Stream<ValidationResult> typecheck(final DmnElement dmnElement, final Stream<Expression> expressionStream,
final Stream<ExpressionType> types) {
final Stream<Optional<ValidationResult.Builder.ElementStep>> intermediateResults = Util
.zip(expressions, types, (expression, type) -> {
.zip(expressionStream, types, (unaryTest, type) -> {
final FeelTypecheck.Context emptyContext = new FeelTypecheck.Context();

return typecheckExpression(expression, emptyContext, type);
return typecheckExpression(unaryTest, emptyContext, type);
});

return buildValidationResults(intermediateResults, dmnElement);
}

private Optional<ValidationResult.Builder.ElementStep> typecheckExpression(DmnElement dmnElement, FeelTypecheck.Context context,
ExpressionType expectedType) {
return FeelParser.parse(dmnElement.getTextContent()).bind(feelExpression -> FeelTypecheck.typecheck(context, feelExpression))
private Optional<ValidationResult.Builder.ElementStep> typecheckExpression(Expression expression, FeelTypecheck.Context context,
ExpressionType expectedType) {
return FeelParser.parse(expression).bind(feelExpression -> FeelTypecheck.typecheck(context, feelExpression))
.map(type -> {
if (type.isSubtypeOf(ExpressionTypes.STRING()) && ExpressionTypes.getClassName(expectedType).isPresent()) {
return checkEnumValue(ExpressionTypes.getClassName(expectedType).get(), dmnElement.getTextContent());
return checkEnumValue(ExpressionTypes.getClassName(expectedType).get(), expression.textContent);
} else if (type.isSubtypeOf(expectedType) || ExpressionTypes.TOP().equals(type)) {
return Optional.<ValidationResult.Builder.ElementStep>empty();
} else {
Expand Down
Loading

0 comments on commit 1a69f42

Please sign in to comment.