diff --git a/drools-test-coverage/test-compiler-integration/src/test/java/org/drools/mvel/integrationtests/ExecutionFlowControlTest.java b/drools-test-coverage/test-compiler-integration/src/test/java/org/drools/mvel/integrationtests/ExecutionFlowControlTest.java index ebbd2d9f352..8c0b40dcb94 100644 --- a/drools-test-coverage/test-compiler-integration/src/test/java/org/drools/mvel/integrationtests/ExecutionFlowControlTest.java +++ b/drools-test-coverage/test-compiler-integration/src/test/java/org/drools/mvel/integrationtests/ExecutionFlowControlTest.java @@ -71,19 +71,26 @@ public static Collection getParameters() { @Test(timeout = 10000) public void testSalienceIntegerAndLoadOrder() throws Exception { KieBase kbase = KieBaseUtil.getKieBaseFromClasspathResources(this.getClass(), kieBaseTestConfiguration, "test_salienceIntegerRule.drl"); - KieSession ksession = kbase.newKieSession(); - final List list = new ArrayList(); - ksession.setGlobal( "list", list ); + KieSession ksession = null; + try { + ksession = kbase.newKieSession(); + final List list = new ArrayList<>(); + ksession.setGlobal("list", list); - final PersonInterface person = new Person( "Edson", "cheese" ); - ksession.insert( person ); + final PersonInterface person = new Person("Edson", "cheese"); + ksession.insert(person); - ksession.fireAllRules(); + ksession.fireAllRules(); - assertThat(list.size()).as("Three rules should have been fired").isEqualTo(3); - assertThat(list.get(0)).as("Rule 4 should have been fired first").isEqualTo("Rule 4"); - assertThat(list.get(1)).as("Rule 2 should have been fired second").isEqualTo("Rule 2"); - assertThat(list.get(2)).as("Rule 3 should have been fired third").isEqualTo("Rule 3"); + assertThat(list.size()).as("Three rules should have been fired").isEqualTo(3); + assertThat(list.get(0)).as("Rule 4 should have been fired first").isEqualTo("Rule 4"); + assertThat(list.get(1)).as("Rule 2 should have been fired second").isEqualTo("Rule 2"); + assertThat(list.get(2)).as("Rule 3 should have been fired third").isEqualTo("Rule 3"); + } finally { + if (ksession != null) { + ksession.dispose(); + } + } } @Test diff --git a/kie-dmn/kie-dmn-core/src/main/java/org/kie/dmn/core/compiler/DMNCompilerImpl.java b/kie-dmn/kie-dmn-core/src/main/java/org/kie/dmn/core/compiler/DMNCompilerImpl.java index 823084f4b6f..078a1158036 100644 --- a/kie-dmn/kie-dmn-core/src/main/java/org/kie/dmn/core/compiler/DMNCompilerImpl.java +++ b/kie-dmn/kie-dmn-core/src/main/java/org/kie/dmn/core/compiler/DMNCompilerImpl.java @@ -387,7 +387,7 @@ private void processItemDefinitions(DMNCompilerContext ctx, DMNModelImpl model, if (id.getItemComponent() != null && !id.getItemComponent().isEmpty()) { DMNCompilerHelper.checkVariableName(model, id, id.getName()); CompositeTypeImpl compType = new CompositeTypeImpl(model.getNamespace(), id.getName(), id.getId(), id.isIsCollection()); - DMNType preregistered = model.getTypeRegistry().registerType(compType); + model.getTypeRegistry().registerType(compType); } } @@ -598,7 +598,31 @@ public void linkRequirements(DMNModelImpl model, DMNBaseNode node) { */ public static String getId(DMNElementReference er) { String href = er.getHref(); - return href.startsWith("#") ? href.substring(1) : href; + if (href.startsWith("#")) { + return href.substring(1); + } else { + Definitions rootElement = getRootElement(er); + String toRemove = String.format("%s#", rootElement.getNamespace()); + return href.replace(toRemove, ""); + } + } + + /** + * Recursively navigate the given DMNModelInstrumentedBase until it gets to the root Definitions element. + * it throws a RuntimeException if such element could not be found. + * + * @param toNavigate + * @return + * @throws RuntimeException + */ + public static Definitions getRootElement(DMNModelInstrumentedBase toNavigate) { + if ( toNavigate instanceof Definitions ) { + return (Definitions) toNavigate; + } else if ( toNavigate.getParent() != null ) { + return getRootElement(toNavigate.getParent()); + } else { + throw new RuntimeException("Failed to get Definitions parent for " + toNavigate); + } } /** diff --git a/kie-dmn/kie-dmn-core/src/test/java/org/kie/dmn/core/DMNCompilerTest.java b/kie-dmn/kie-dmn-core/src/test/java/org/kie/dmn/core/DMNCompilerTest.java index 8b3776593ed..daf7518dbb9 100644 --- a/kie-dmn/kie-dmn-core/src/test/java/org/kie/dmn/core/DMNCompilerTest.java +++ b/kie-dmn/kie-dmn-core/src/test/java/org/kie/dmn/core/DMNCompilerTest.java @@ -25,13 +25,13 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; import org.kie.dmn.api.core.DMNContext; -import org.kie.dmn.api.core.DMNDecisionResult; import org.kie.dmn.api.core.DMNMessage; import org.kie.dmn.api.core.DMNModel; import org.kie.dmn.api.core.DMNResult; import org.kie.dmn.api.core.DMNRuntime; import org.kie.dmn.api.core.DMNType; import org.kie.dmn.api.core.FEELPropertyAccessible; +import org.kie.dmn.api.core.ast.DecisionNode; import org.kie.dmn.api.core.ast.ItemDefNode; import org.kie.dmn.core.api.DMNFactory; import org.kie.dmn.core.compiler.DMNTypeRegistry; @@ -39,18 +39,19 @@ import org.kie.dmn.core.impl.CompositeTypeImpl; import org.kie.dmn.core.impl.DMNContextFPAImpl; import org.kie.dmn.core.impl.DMNModelImpl; -import org.kie.dmn.core.impl.DMNResultImpl; import org.kie.dmn.core.impl.SimpleTypeImpl; import org.kie.dmn.core.util.DMNRuntimeUtil; import org.kie.dmn.feel.lang.EvaluationContext; import org.kie.dmn.feel.lang.types.AliasFEELType; import org.kie.dmn.feel.lang.types.BuiltInType; +import org.kie.dmn.model.api.Decision; +import org.kie.dmn.model.api.InformationRequirement; +import org.kie.dmn.model.api.KnowledgeRequirement; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.fail; -import static org.assertj.core.api.InstanceOfAssertFactories.map; import static org.kie.dmn.api.core.DMNDecisionResult.DecisionEvaluationStatus.SUCCEEDED; import static org.kie.dmn.core.util.DynamicTypeUtils.entry; import static org.kie.dmn.core.util.DynamicTypeUtils.mapOf; @@ -470,6 +471,32 @@ void allowedValuesForComplexTypeInherited(VariantTestConf conf) { assertThat(dmnAdultBobPerson.isAssignableValue(instanceYoungJoe)).isFalse(); } + @ParameterizedTest + @MethodSource("params") + void localHrefs(VariantTestConf conf) { + testConfig = conf; + String nameSpace = "http://www.montera.com.au/spec/DMN/local-hrefs"; + final DMNRuntime runtime = DMNRuntimeUtil.createRuntime("valid_models/DMNv1_5/LocalHrefs.dmn", this.getClass()); + final DMNModel dmnModel = runtime.getModel( + nameSpace, + "LocalHrefs"); + assertThat(dmnModel).isNotNull(); + assertThat(dmnModel.hasErrors()).as(DMNRuntimeUtil.formatMessages(dmnModel.getMessages())).isFalse(); + DecisionNode retrievedDecisionNode = dmnModel.getDecisionByName("decision_002"); + assertThat(retrievedDecisionNode).isNotNull(); + Decision retrievedDecision = retrievedDecisionNode.getDecision(); + assertThat(retrievedDecision).isNotNull(); + assertThat(retrievedDecision.getInformationRequirement()) + .isNotNull() + .isNotEmpty() + .allSatisfy((Consumer) informationRequirement -> assertThat(informationRequirement).isNotNull()); + assertThat(retrievedDecision.getKnowledgeRequirement()) + .isNotNull() + .isNotEmpty() + .allSatisfy((Consumer) knowledgeRequirement -> assertThat(knowledgeRequirement).isNotNull()); + + } + private void commonValidateUnnamedImport(String importingModelRef, String importedModelRef) { final DMNRuntime runtime = createRuntimeWithAdditionalResources(importingModelRef, this.getClass(), diff --git a/kie-dmn/kie-dmn-core/src/test/java/org/kie/dmn/core/compiler/DMNCompilerImplTest.java b/kie-dmn/kie-dmn-core/src/test/java/org/kie/dmn/core/compiler/DMNCompilerImplTest.java new file mode 100644 index 00000000000..f67f8fc1557 --- /dev/null +++ b/kie-dmn/kie-dmn-core/src/test/java/org/kie/dmn/core/compiler/DMNCompilerImplTest.java @@ -0,0 +1,79 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.kie.dmn.core.compiler; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.kie.dmn.model.api.DMNElementReference; +import org.kie.dmn.model.api.Definitions; +import org.kie.dmn.model.api.InformationRequirement; +import org.kie.dmn.model.v1_5.TDMNElementReference; +import org.kie.dmn.model.v1_5.TDefinitions; +import org.kie.dmn.model.v1_5.TInformationRequirement; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.*; + +class DMNCompilerImplTest { + + private static final String nameSpace = "http://www.montera.com.au/spec/DMN/local-hrefs"; + private static Definitions parent; + + @BeforeAll + static void setup() { + String modelName = "LocalHrefs"; + parent = new TDefinitions(); + parent.setName(modelName); + parent.setNamespace(nameSpace); + } + + @Test + void getId() { + String localPart = "reference"; + DMNElementReference elementReference = new TDMNElementReference(); + elementReference.setHref(String.format("%s#%s", nameSpace, localPart)); + elementReference.setParent(parent); + String retrieved = DMNCompilerImpl.getId(elementReference); + assertThat(retrieved).isNotNull().isEqualTo(localPart); + + String expected = String.format("%s#%s", "http://a-different-namespace", localPart); + elementReference.setHref(expected); + retrieved = DMNCompilerImpl.getId(elementReference); + assertThat(retrieved).isNotNull().isEqualTo(expected); + } + + @Test + void getRootElement() { + String localPart = "reference"; + DMNElementReference elementReference = new TDMNElementReference(); + String href = String.format("%s#%s", nameSpace, localPart); + elementReference.setHref(href); + elementReference.setParent(parent); + Definitions retrieved = DMNCompilerImpl.getRootElement(elementReference); + assertThat(retrieved).isNotNull().isEqualTo(parent); + + InformationRequirement informationRequirement = new TInformationRequirement(); + elementReference.setParent(informationRequirement); + assertThrows(RuntimeException.class, () -> DMNCompilerImpl.getRootElement(elementReference)); + + informationRequirement.setParent(parent); + retrieved = DMNCompilerImpl.getRootElement(elementReference); + assertThat(retrieved).isNotNull().isEqualTo(parent); + } +} \ No newline at end of file diff --git a/kie-dmn/kie-dmn-feel/src/main/java/org/kie/dmn/feel/lang/ast/ForExpressionNode.java b/kie-dmn/kie-dmn-feel/src/main/java/org/kie/dmn/feel/lang/ast/ForExpressionNode.java index f25aa0ca569..1229c354457 100644 --- a/kie-dmn/kie-dmn-feel/src/main/java/org/kie/dmn/feel/lang/ast/ForExpressionNode.java +++ b/kie-dmn/kie-dmn-feel/src/main/java/org/kie/dmn/feel/lang/ast/ForExpressionNode.java @@ -25,6 +25,8 @@ import org.kie.dmn.feel.lang.Type; import org.kie.dmn.feel.lang.ast.forexpressioniterators.ForIteration; import org.kie.dmn.feel.lang.types.BuiltInType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.util.ArrayList; import java.util.Collections; @@ -35,6 +37,7 @@ public class ForExpressionNode extends BaseNode { + private static final Logger LOG = LoggerFactory.getLogger(ForExpressionNode.class); private List iterationContexts; private BaseNode expression; @@ -74,16 +77,11 @@ public void setExpression(BaseNode expression) { public Object evaluate(EvaluationContext ctx) { try { ctx.enterFrame(); - List results = new ArrayList(); - ctx.setValue("partial", results); - ForIteration[] ictx = initializeContexts(ctx, iterationContexts); - - while (nextIteration(ctx, ictx)) { - Object result = expression.evaluate(ctx); - results.add(result); - ctx.exitFrame(); // last i-th scope unrolled, see also ForExpressionNode.nextIteration(...) - } - return results; + List toReturn = new ArrayList<>(); + ctx.setValue("partial", toReturn); + populateToReturn(0, ctx, toReturn); + LOG.trace("returning {}", toReturn); + return toReturn; } catch (EndpointOfRangeNotValidTypeException | EndpointOfRangeOfDifferentTypeException e) { // ast error already reported return null; @@ -92,28 +90,34 @@ public Object evaluate(EvaluationContext ctx) { } } - public static boolean nextIteration(EvaluationContext ctx, ForIteration[] ictx) { - int i = ictx.length - 1; - while (i >= 0 && i < ictx.length) { - if (ictx[i].hasNextValue()) { - ctx.enterFrame(); // on first iter, open last scope frame; or new ones when prev unrolled - setValueIntoContext(ctx, ictx[i]); - i++; - } else { - if (i > 0) { - // end of iter loop for this i-th scope; i-th scope is always unrolled as part of the - // for-loop cycle, so here must unroll the _prev_ scope; - // the if-guard for this code block makes sure NOT to unroll bottom one. - ctx.exitFrame(); - } - i--; + private void populateToReturn(int k, EvaluationContext ctx, List toPopulate) { + LOG.trace("populateToReturn at index {}", k); + if (k > iterationContexts.size() - 1) { + LOG.trace("Index {} out of range, returning", k); + return; + } + IterationContextNode iterationContextNode = iterationContexts.get(k); + ForIteration forIteration = createForIteration(ctx, iterationContextNode); + while (forIteration.hasNextValue()) { + LOG.trace("{} has next value", forIteration); + ctx.enterFrame(); // open loop scope frame, for every iter ctx, except last one as guarded by if clause + // above + setValueIntoContext(ctx, forIteration.getName(), forIteration.getNextValue()); + if (k == iterationContexts.size() - 1) { + LOG.trace("i == iterationContexts.size() -1: this is the last iteration context; evaluating {}", + expression); + Object result = expression.evaluate(ctx); + LOG.trace("add {} to toReturn", result); + toPopulate.add(result); + } else if (k < iterationContexts.size() - 1) { + populateToReturn(k + 1, ctx, toPopulate); } } - return i >= 0; + ctx.exitFrame(); } - public static void setValueIntoContext(EvaluationContext ctx, ForIteration forIteration) { - ctx.setValue(forIteration.getName(), forIteration.getNextValue()); + static void setValueIntoContext(EvaluationContext ctx, String name, Object value) { + ctx.setValue(name, value); } @Override @@ -121,32 +125,19 @@ public Type getResultType() { return BuiltInType.LIST; } - private ForIteration[] initializeContexts(EvaluationContext ctx, List iterationContexts) { - ForIteration[] ictx = new ForIteration[iterationContexts.size()]; - int i = 0; - for (IterationContextNode icn : iterationContexts) { - ictx[i] = createQuantifiedExpressionIterationContext(ctx, icn); - if (i < iterationContexts.size() - 1 && ictx[i].hasNextValue()) { - ctx.enterFrame(); // open loop scope frame, for every iter ctx, except last one as guarded by if clause above - setValueIntoContext(ctx, ictx[i]); - } - i++; - } - return ictx; - } - - private ForIteration createQuantifiedExpressionIterationContext(EvaluationContext ctx, IterationContextNode icn) { - ForIteration fi; - String name = icn.evaluateName(ctx); - Object result = icn.evaluate(ctx); - Object rangeEnd = icn.evaluateRangeEnd(ctx); + private ForIteration createForIteration(EvaluationContext ctx, IterationContextNode iterationContextNode) { + LOG.trace("Creating ForIteration for {}", iterationContextNode); + ForIteration toReturn; + String name = iterationContextNode.evaluateName(ctx); + Object result = iterationContextNode.evaluate(ctx); + Object rangeEnd = iterationContextNode.evaluateRangeEnd(ctx); if (rangeEnd == null) { - Iterable values = result instanceof Iterable ? (Iterable) result : Collections.singletonList(result); - fi = new ForIteration(name, values); + Iterable values = result instanceof Iterable iterable? iterable : Collections.singletonList(result); + toReturn = new ForIteration(name, values); } else { - fi = getForIteration(ctx, name, result, rangeEnd); + toReturn = getForIteration(ctx, name, result, rangeEnd); } - return fi; + return toReturn; } @Override @@ -161,5 +152,4 @@ public ASTNode[] getChildrenNode() { public T accept(Visitor v) { return v.visit(this); } - } diff --git a/kie-dmn/kie-dmn-feel/src/main/java/org/kie/dmn/feel/lang/ast/IterationContextNode.java b/kie-dmn/kie-dmn-feel/src/main/java/org/kie/dmn/feel/lang/ast/IterationContextNode.java index 70cfa00c9fa..0c11f2e71a6 100644 --- a/kie-dmn/kie-dmn-feel/src/main/java/org/kie/dmn/feel/lang/ast/IterationContextNode.java +++ b/kie-dmn/kie-dmn-feel/src/main/java/org/kie/dmn/feel/lang/ast/IterationContextNode.java @@ -20,10 +20,14 @@ import org.antlr.v4.runtime.ParserRuleContext; import org.kie.dmn.feel.lang.EvaluationContext; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; public class IterationContextNode extends BaseNode { + private static final Logger LOG = LoggerFactory.getLogger(IterationContextNode.class); + private NameDefNode name; private BaseNode expression; private BaseNode rangeEndExpr = null; @@ -69,15 +73,18 @@ public void setExpression(BaseNode expression) { } public String evaluateName(EvaluationContext ctx) { + LOG.trace("evaluateName {}", name); return this.name.evaluate(ctx); } @Override public Object evaluate(EvaluationContext ctx) { + LOG.trace("evaluate {}", expression); return expression != null ? expression.evaluate( ctx ) : null; } public Object evaluateRangeEnd(EvaluationContext ctx) { + LOG.trace("evaluateRangeEnd {}", rangeEndExpr); return rangeEndExpr != null ? rangeEndExpr.evaluate(ctx) : null; } diff --git a/kie-dmn/kie-dmn-feel/src/main/java/org/kie/dmn/feel/lang/ast/forexpressioniterators/ForIteration.java b/kie-dmn/kie-dmn-feel/src/main/java/org/kie/dmn/feel/lang/ast/forexpressioniterators/ForIteration.java index 43f4f576c03..bd8719f2ef4 100644 --- a/kie-dmn/kie-dmn-feel/src/main/java/org/kie/dmn/feel/lang/ast/forexpressioniterators/ForIteration.java +++ b/kie-dmn/kie-dmn-feel/src/main/java/org/kie/dmn/feel/lang/ast/forexpressioniterators/ForIteration.java @@ -64,4 +64,12 @@ public Object getNextValue() { public String getName() { return name; } + + @Override + public String toString() { + return "ForIteration{" + + "values=" + values + + ", name='" + name + '\'' + + '}'; + } } \ No newline at end of file diff --git a/kie-dmn/kie-dmn-feel/src/main/java/org/kie/dmn/feel/lang/impl/EvaluationContextImpl.java b/kie-dmn/kie-dmn-feel/src/main/java/org/kie/dmn/feel/lang/impl/EvaluationContextImpl.java index 47759d0aefa..8e1e75fa77c 100644 --- a/kie-dmn/kie-dmn-feel/src/main/java/org/kie/dmn/feel/lang/impl/EvaluationContextImpl.java +++ b/kie-dmn/kie-dmn-feel/src/main/java/org/kie/dmn/feel/lang/impl/EvaluationContextImpl.java @@ -32,9 +32,13 @@ import org.kie.dmn.feel.lang.EvaluationContext; import org.kie.dmn.feel.lang.FEELDialect; import org.kie.dmn.feel.util.NumberEvalHelper; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; public class EvaluationContextImpl implements EvaluationContext { + private static final Logger LOG = LoggerFactory.getLogger(EvaluationContextImpl.class); + private final FEELEventListenersManager eventsManager; private ArrayDeque stack; private DMNRuntime dmnRuntime; @@ -102,6 +106,7 @@ public Deque getStack() { @Override public void enterFrame() { + LOG.trace("Creating new head element in stack"); push( new ExecutionFrameImpl( peek() /*, symbols, scope*/ ) ); } @@ -111,11 +116,13 @@ public void enterFrame(int size) { @Override public void exitFrame() { + LOG.trace("Removing head element from stack"); pop(); } @Override public void setValue(String name, Object value) { + LOG.trace("put {} -> {} in head stack element", name, value); peek().setValue(name, NumberEvalHelper.coerceNumber(value ) ); } diff --git a/kie-dmn/kie-dmn-feel/src/main/java/org/kie/dmn/feel/runtime/functions/MatchesFunction.java b/kie-dmn/kie-dmn-feel/src/main/java/org/kie/dmn/feel/runtime/functions/MatchesFunction.java index f6b1806ca20..79ff2a01368 100644 --- a/kie-dmn/kie-dmn-feel/src/main/java/org/kie/dmn/feel/runtime/functions/MatchesFunction.java +++ b/kie-dmn/kie-dmn-feel/src/main/java/org/kie/dmn/feel/runtime/functions/MatchesFunction.java @@ -18,16 +18,21 @@ */ package org.kie.dmn.feel.runtime.functions; +import java.security.InvalidParameterException; +import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.regex.PatternSyntaxException; +import java.util.stream.Collectors; import org.kie.dmn.api.feel.runtime.events.FEELEvent.Severity; import org.kie.dmn.feel.runtime.events.InvalidParametersEvent; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; public class MatchesFunction extends BaseFEELFunction { - + private static final Logger log = LoggerFactory.getLogger(MatchesFunction.class); public static final MatchesFunction INSTANCE = new MatchesFunction(); private MatchesFunction() { @@ -39,40 +44,58 @@ public FEELFnResult invoke(@ParameterName("input") String input, @Param } public FEELFnResult invoke(@ParameterName("input") String input, @ParameterName("pattern") String pattern, @ParameterName("flags") String flags) { - if ( input == null ) { - return FEELFnResult.ofError( new InvalidParametersEvent( Severity.ERROR, "input", "cannot be null" ) ); - } - if ( pattern == null ) { - return FEELFnResult.ofError( new InvalidParametersEvent( Severity.ERROR, "pattern", "cannot be null" ) ); - } try { - int f = processFlags( flags ); - Pattern p = Pattern.compile( pattern, f ); - Matcher m = p.matcher( input ); - return FEELFnResult.ofResult( m.find() ); - } catch ( PatternSyntaxException e ) { - return FEELFnResult.ofError( new InvalidParametersEvent( Severity.ERROR, "pattern", "is invalid and can not be compiled", e ) ); - } catch ( IllegalArgumentException t ) { + return matchFunctionWithFlags(input,pattern,flags); + } catch ( PatternSyntaxException t ) { + return FEELFnResult.ofError( new InvalidParametersEvent( Severity.ERROR, "pattern", "is invalid and can not be compiled", t ) ); + } catch (InvalidParameterException t ) { + return FEELFnResult.ofError( new InvalidParametersEvent( Severity.ERROR, t.getMessage(), "cannot be null", t ) ); + } catch (IllegalArgumentException t ) { return FEELFnResult.ofError( new InvalidParametersEvent( Severity.ERROR, "flags", "contains unknown flags", t ) ); - } catch ( Throwable t) { + } catch (Throwable t) { return FEELFnResult.ofError( new InvalidParametersEvent( Severity.ERROR, "pattern", "is invalid and can not be compiled", t ) ); } } - private int processFlags(String flags) { - int f = 0; - if( flags != null ) { - if( flags.contains( "s" ) ) { - f |= Pattern.DOTALL; - } - if( flags.contains( "m" ) ) { - f |= Pattern.MULTILINE; - } - if( flags.contains( "i" ) ) { - f |= Pattern.CASE_INSENSITIVE; - } + static FEELFnResult matchFunctionWithFlags(String input, String pattern, String flags) { + log.debug("Input: {} , Pattern: {}, Flags: {}", input, pattern, flags); + if ( input == null ) { + throw new InvalidParameterException("input"); + } + if ( pattern == null ) { + throw new InvalidParameterException("pattern"); } - return f; + final String flagsString; + if (flags != null && !flags.isEmpty()) { + checkFlags(flags); + if(!flags.contains("U")){ + flags += "U"; + } + flagsString = String.format("(?%s)", flags); + } else { + flagsString = ""; + } + log.debug("flagsString: {}", flagsString); + String stringToBeMatched = flagsString + pattern; + log.debug("stringToBeMatched: {}", stringToBeMatched); + Pattern p=Pattern.compile(stringToBeMatched); + Matcher m = p.matcher( input ); + boolean matchFound=m.find(); + log.debug("matchFound: {}", matchFound); + return FEELFnResult.ofResult(matchFound); } + static void checkFlags(String flags) { + Set allowedChars = Set.of('s','i','x','m'); + boolean isValidFlag= flags.chars() + .mapToObj(c -> (char) c) + .allMatch(allowedChars::contains) + && flags.chars() + .mapToObj(c -> (char) c) + .collect(Collectors.toSet()) + .size() == flags.length(); + if(!isValidFlag){ + throw new IllegalArgumentException("Not a valid flag parameter " +flags); + } + } } diff --git a/kie-dmn/kie-dmn-feel/src/test/java/org/kie/dmn/feel/codegen/feel11/DirectCompilerTest.java b/kie-dmn/kie-dmn-feel/src/test/java/org/kie/dmn/feel/codegen/feel11/DirectCompilerTest.java index 8cb00e16551..9231a8b1d5d 100644 --- a/kie-dmn/kie-dmn-feel/src/test/java/org/kie/dmn/feel/codegen/feel11/DirectCompilerTest.java +++ b/kie-dmn/kie-dmn-feel/src/test/java/org/kie/dmn/feel/codegen/feel11/DirectCompilerTest.java @@ -20,7 +20,9 @@ import java.math.BigDecimal; import java.time.LocalDate; +import java.util.Arrays; import java.util.Collections; +import java.util.List; import org.junit.jupiter.api.Test; import org.kie.dmn.feel.lang.EvaluationContext; @@ -33,12 +35,13 @@ import org.kie.dmn.feel.runtime.FEELConditionsAndLoopsTest; import org.kie.dmn.feel.runtime.FEELTernaryLogicTest; import org.kie.dmn.feel.runtime.functions.CustomFEELFunction; +import org.kie.dmn.feel.util.CompilerUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import static org.assertj.core.api.Assertions.assertThat; -import static org.kie.dmn.feel.util.CompilerUtils.parse; -import static org.kie.dmn.feel.util.CompilerUtils.parseCompileEvaluate; +import static org.kie.dmn.feel.util.CompilerUtils.parseCodegen; +import static org.kie.dmn.feel.util.CompilerUtils.parseCodegenCompileEvaluate; import static org.kie.dmn.feel.util.DynamicTypeUtils.entry; import static org.kie.dmn.feel.util.DynamicTypeUtils.mapOf; @@ -49,47 +52,47 @@ public class DirectCompilerTest { @Test void feel_number() { - assertThat(parseCompileEvaluate("10")).isEqualTo(BigDecimal.valueOf(10)); + assertThat(parseCodegenCompileEvaluate("10")).isEqualTo(BigDecimal.valueOf(10)); } @Test void feel_negative_number() { - assertThat(parseCompileEvaluate("-10")).isEqualTo(BigDecimal.valueOf(-10)); + assertThat(parseCodegenCompileEvaluate("-10")).isEqualTo(BigDecimal.valueOf(-10)); } @Test void feel_drools_2143() { // DROOLS-2143: Allow ''--1' expression as per FEEL grammar rule 26 - assertThat(parseCompileEvaluate("--10")).isEqualTo(BigDecimal.valueOf(10)); - assertThat(parseCompileEvaluate("---10")).isEqualTo(BigDecimal.valueOf(-10)); - assertThat(parseCompileEvaluate("+10")).isEqualTo(BigDecimal.valueOf(10)); + assertThat(parseCodegenCompileEvaluate("--10")).isEqualTo(BigDecimal.valueOf(10)); + assertThat(parseCodegenCompileEvaluate("---10")).isEqualTo(BigDecimal.valueOf(-10)); + assertThat(parseCodegenCompileEvaluate("+10")).isEqualTo(BigDecimal.valueOf(10)); } @Test void feel_boolean() { - assertThat(parseCompileEvaluate("false")).isEqualTo(Boolean.FALSE); - assertThat(parseCompileEvaluate("true")).isEqualTo(Boolean.TRUE); - assertThat(parseCompileEvaluate("null")).isNull(); + assertThat(parseCodegenCompileEvaluate("false")).isEqualTo(Boolean.FALSE); + assertThat(parseCodegenCompileEvaluate("true")).isEqualTo(Boolean.TRUE); + assertThat(parseCodegenCompileEvaluate("null")).isNull(); } @Test void feel_null() { - assertThat(parseCompileEvaluate("null")).isNull(); + assertThat(parseCodegenCompileEvaluate("null")).isNull(); } @Test void feel_string() { - assertThat(parseCompileEvaluate("\"some string\"")).isEqualTo("some string" ); + assertThat(parseCodegenCompileEvaluate("\"some string\"")).isEqualTo("some string" ); } @Test void primary_parens() { - assertThat(parseCompileEvaluate("(\"some string\")")).isEqualTo("some string" ); - assertThat(parseCompileEvaluate("(123)")).isEqualTo(BigDecimal.valueOf(123)); - assertThat(parseCompileEvaluate("(-123)")).isEqualTo(BigDecimal.valueOf(-123)); - assertThat(parseCompileEvaluate("-(123)")).isEqualTo(BigDecimal.valueOf(-123)); - assertThat(parseCompileEvaluate("(false)")).isEqualTo(Boolean.FALSE); - assertThat(parseCompileEvaluate("(true)")).isEqualTo(Boolean.TRUE); + assertThat(parseCodegenCompileEvaluate("(\"some string\")")).isEqualTo("some string" ); + assertThat(parseCodegenCompileEvaluate("(123)")).isEqualTo(BigDecimal.valueOf(123)); + assertThat(parseCodegenCompileEvaluate("(-123)")).isEqualTo(BigDecimal.valueOf(-123)); + assertThat(parseCodegenCompileEvaluate("-(123)")).isEqualTo(BigDecimal.valueOf(-123)); + assertThat(parseCodegenCompileEvaluate("(false)")).isEqualTo(Boolean.FALSE); + assertThat(parseCodegenCompileEvaluate("(true)")).isEqualTo(Boolean.TRUE); } /** @@ -97,29 +100,29 @@ void primary_parens() { */ @Test void ternary_logic() { - assertThat(parseCompileEvaluate( "true and true")).isEqualTo(Boolean.TRUE); - assertThat(parseCompileEvaluate( "true and false")).isEqualTo(Boolean.FALSE); - assertThat(parseCompileEvaluate( "true and null")).isNull(); - assertThat(parseCompileEvaluate( "false and true")).isEqualTo(Boolean.FALSE); - assertThat(parseCompileEvaluate( "false and false")).isEqualTo(Boolean.FALSE); - assertThat(parseCompileEvaluate( "false and null")).isEqualTo(Boolean.FALSE); - assertThat(parseCompileEvaluate( "null and true")).isNull(); - assertThat(parseCompileEvaluate( "null and false")).isEqualTo(Boolean.FALSE); - assertThat(parseCompileEvaluate( "null and null")).isNull(); - assertThat(parseCompileEvaluate( "true or true")).isEqualTo(Boolean.TRUE); - assertThat(parseCompileEvaluate( "true or false")).isEqualTo(Boolean.TRUE); - assertThat(parseCompileEvaluate( "true or null")).isEqualTo(Boolean.TRUE); - assertThat(parseCompileEvaluate( "false or true")).isEqualTo(Boolean.TRUE); - assertThat(parseCompileEvaluate( "false or false")).isEqualTo(Boolean.FALSE); - assertThat(parseCompileEvaluate( "false or null")).isNull(); - assertThat(parseCompileEvaluate( "null or true")).isEqualTo(Boolean.TRUE); - assertThat(parseCompileEvaluate( "null or false")).isNull(); - assertThat(parseCompileEvaluate( "null or null")).isNull(); + assertThat(parseCodegenCompileEvaluate("true and true")).isEqualTo(Boolean.TRUE); + assertThat(parseCodegenCompileEvaluate("true and false")).isEqualTo(Boolean.FALSE); + assertThat(parseCodegenCompileEvaluate("true and null")).isNull(); + assertThat(parseCodegenCompileEvaluate("false and true")).isEqualTo(Boolean.FALSE); + assertThat(parseCodegenCompileEvaluate("false and false")).isEqualTo(Boolean.FALSE); + assertThat(parseCodegenCompileEvaluate("false and null")).isEqualTo(Boolean.FALSE); + assertThat(parseCodegenCompileEvaluate("null and true")).isNull(); + assertThat(parseCodegenCompileEvaluate("null and false")).isEqualTo(Boolean.FALSE); + assertThat(parseCodegenCompileEvaluate("null and null")).isNull(); + assertThat(parseCodegenCompileEvaluate("true or true")).isEqualTo(Boolean.TRUE); + assertThat(parseCodegenCompileEvaluate("true or false")).isEqualTo(Boolean.TRUE); + assertThat(parseCodegenCompileEvaluate("true or null")).isEqualTo(Boolean.TRUE); + assertThat(parseCodegenCompileEvaluate("false or true")).isEqualTo(Boolean.TRUE); + assertThat(parseCodegenCompileEvaluate("false or false")).isEqualTo(Boolean.FALSE); + assertThat(parseCodegenCompileEvaluate("false or null")).isNull(); + assertThat(parseCodegenCompileEvaluate("null or true")).isEqualTo(Boolean.TRUE); + assertThat(parseCodegenCompileEvaluate("null or false")).isNull(); + assertThat(parseCodegenCompileEvaluate("null or null")).isNull(); // logical operator priority - assertThat(parseCompileEvaluate( "false and false or true")).isEqualTo(Boolean.TRUE); - assertThat(parseCompileEvaluate( "false and (false or true)")).isEqualTo(Boolean.FALSE); - assertThat(parseCompileEvaluate( "true or false and false")).isEqualTo(Boolean.TRUE); - assertThat(parseCompileEvaluate( "(true or false) and false")).isEqualTo(Boolean.FALSE); + assertThat(parseCodegenCompileEvaluate("false and false or true")).isEqualTo(Boolean.TRUE); + assertThat(parseCodegenCompileEvaluate("false and (false or true)")).isEqualTo(Boolean.FALSE); + assertThat(parseCodegenCompileEvaluate("true or false and false")).isEqualTo(Boolean.TRUE); + assertThat(parseCodegenCompileEvaluate("(true or false) and false")).isEqualTo(Boolean.FALSE); } /** @@ -127,100 +130,100 @@ void ternary_logic() { */ @Test void test_if() { - assertThat(parseCompileEvaluate( "if true then 15 else 5")).isEqualTo(BigDecimal.valueOf( 15 )); - assertThat(parseCompileEvaluate( "if false then 15 else 5")).isEqualTo(BigDecimal.valueOf( 5 )); - assertThat(parseCompileEvaluate("if null then 15 else 5")).isEqualTo(BigDecimal.valueOf(5)); - assertThat(parseCompileEvaluate("if \"hello\" then 15 else 5")).isEqualTo(BigDecimal.valueOf(5)); + assertThat(parseCodegenCompileEvaluate("if true then 15 else 5")).isEqualTo(BigDecimal.valueOf(15 )); + assertThat(parseCodegenCompileEvaluate("if false then 15 else 5")).isEqualTo(BigDecimal.valueOf(5 )); + assertThat(parseCodegenCompileEvaluate("if null then 15 else 5")).isEqualTo(BigDecimal.valueOf(5)); + assertThat(parseCodegenCompileEvaluate("if \"hello\" then 15 else 5")).isEqualTo(BigDecimal.valueOf(5)); } @Test void additive_expression() { - assertThat(parseCompileEvaluate( "1 + 2")).isEqualTo(BigDecimal.valueOf( 3 )); - assertThat(parseCompileEvaluate( "1 + null")).isNull(); - assertThat(parseCompileEvaluate( "1 - 2")).isEqualTo(BigDecimal.valueOf( -1 )); - assertThat(parseCompileEvaluate( "1 - null")).isNull(); - assertThat(parseCompileEvaluate( "\"Hello, \" + \"World\"")).isEqualTo("Hello, World"); + assertThat(parseCodegenCompileEvaluate("1 + 2")).isEqualTo(BigDecimal.valueOf(3 )); + assertThat(parseCodegenCompileEvaluate("1 + null")).isNull(); + assertThat(parseCodegenCompileEvaluate("1 - 2")).isEqualTo(BigDecimal.valueOf(-1 )); + assertThat(parseCodegenCompileEvaluate("1 - null")).isNull(); + assertThat(parseCodegenCompileEvaluate("\"Hello, \" + \"World\"")).isEqualTo("Hello, World"); } @Test void multiplicative_expression() { - assertThat(parseCompileEvaluate("3 * 5")).isEqualTo(BigDecimal.valueOf(15)); - assertThat(parseCompileEvaluate("3 * null")).isNull(); - assertThat(parseCompileEvaluate("10 / 2")).isEqualTo(BigDecimal.valueOf(5)); - assertThat(parseCompileEvaluate("10 / null")).isNull(); + assertThat(parseCodegenCompileEvaluate("3 * 5")).isEqualTo(BigDecimal.valueOf(15)); + assertThat(parseCodegenCompileEvaluate("3 * null")).isNull(); + assertThat(parseCodegenCompileEvaluate("10 / 2")).isEqualTo(BigDecimal.valueOf(5)); + assertThat(parseCodegenCompileEvaluate("10 / null")).isNull(); } @Test void exponentiation_expression() { - assertThat(parseCompileEvaluate("3 ** 3")).isEqualTo(BigDecimal.valueOf(27)); - assertThat(parseCompileEvaluate("3 ** null")).isNull(); + assertThat(parseCodegenCompileEvaluate("3 ** 3")).isEqualTo(BigDecimal.valueOf(27)); + assertThat(parseCodegenCompileEvaluate("3 ** null")).isNull(); } @Test void logical_negation_expression() { // this is all invalid syntax - assertThat(parseCompileEvaluate("not true")).isNull(); - assertThat(parseCompileEvaluate("not false")).isNull(); - assertThat(parseCompileEvaluate("not null")).isNull(); - assertThat(parseCompileEvaluate("not 3")).isNull(); + assertThat(parseCodegenCompileEvaluate("not true")).isNull(); + assertThat(parseCodegenCompileEvaluate("not false")).isNull(); + assertThat(parseCodegenCompileEvaluate("not null")).isNull(); + assertThat(parseCodegenCompileEvaluate("not 3")).isNull(); } @Test void list_expression() { - assertThat(parseCompileEvaluate("[]")).asList().isEmpty(); - assertThat(parseCompileEvaluate("[ ]")).asList().isEmpty(); - assertThat(parseCompileEvaluate("[1]")).asList().containsExactly(BigDecimal.valueOf(1)); - assertThat(parseCompileEvaluate("[1, 2,3]")).asList().containsExactly(BigDecimal.valueOf(1), BigDecimal.valueOf(2), BigDecimal.valueOf(3)); + assertThat(parseCodegenCompileEvaluate("[]")).asList().isEmpty(); + assertThat(parseCodegenCompileEvaluate("[ ]")).asList().isEmpty(); + assertThat(parseCodegenCompileEvaluate("[1]")).asList().containsExactly(BigDecimal.valueOf(1)); + assertThat(parseCodegenCompileEvaluate("[1, 2,3]")).asList().containsExactly(BigDecimal.valueOf(1), BigDecimal.valueOf(2), BigDecimal.valueOf(3)); } @Test void instance_of_expression() { - assertThat(parseCompileEvaluate("123 instance of number")).isEqualTo(Boolean.TRUE); - assertThat(parseCompileEvaluate("\"ciao\" instance of number")).isEqualTo(Boolean.FALSE); - assertThat(parseCompileEvaluate("123 instance of string")).isEqualTo(Boolean.FALSE); - assertThat(parseCompileEvaluate("\"ciao\" instance of string")).isEqualTo(Boolean.TRUE); + assertThat(parseCodegenCompileEvaluate("123 instance of number")).isEqualTo(Boolean.TRUE); + assertThat(parseCodegenCompileEvaluate("\"ciao\" instance of number")).isEqualTo(Boolean.FALSE); + assertThat(parseCodegenCompileEvaluate("123 instance of string")).isEqualTo(Boolean.FALSE); + assertThat(parseCodegenCompileEvaluate("\"ciao\" instance of string")).isEqualTo(Boolean.TRUE); } @Test void between() { - assertThat(parseCompileEvaluate("10 between 5 and 12")).isEqualTo(Boolean.TRUE); - assertThat(parseCompileEvaluate("10 between 20 and 30")).isEqualTo(Boolean.FALSE); - assertThat(parseCompileEvaluate("10 between 5 and \"foo\"")).isNull(); - assertThat(parseCompileEvaluate("\"foo\" between 5 and 12")).isNull(); - assertThat(parseCompileEvaluate("\"foo\" between \"bar\" and \"zap\"")).isEqualTo(Boolean.TRUE); - assertThat(parseCompileEvaluate("\"foo\" between null and \"zap\"")).isNull(); + assertThat(parseCodegenCompileEvaluate("10 between 5 and 12")).isEqualTo(Boolean.TRUE); + assertThat(parseCodegenCompileEvaluate("10 between 20 and 30")).isEqualTo(Boolean.FALSE); + assertThat(parseCodegenCompileEvaluate("10 between 5 and \"foo\"")).isNull(); + assertThat(parseCodegenCompileEvaluate("\"foo\" between 5 and 12")).isNull(); + assertThat(parseCodegenCompileEvaluate("\"foo\" between \"bar\" and \"zap\"")).isEqualTo(Boolean.TRUE); + assertThat(parseCodegenCompileEvaluate("\"foo\" between null and \"zap\"")).isNull(); } @Test void filter_path() { // Filtering by index - assertThat(parseCompileEvaluate("[\"a\", \"b\", \"c\"][1]")).isEqualTo("a"); - assertThat(parseCompileEvaluate("[\"a\", \"b\", \"c\"][2]")).isEqualTo("b"); - assertThat(parseCompileEvaluate("[\"a\", \"b\", \"c\"][3]")).isEqualTo("c"); - assertThat(parseCompileEvaluate("[\"a\", \"b\", \"c\"][-1]")).isEqualTo("c"); - assertThat(parseCompileEvaluate("[\"a\", \"b\", \"c\"][-2]")).isEqualTo("b"); - assertThat(parseCompileEvaluate("[\"a\", \"b\", \"c\"][-3]")).isEqualTo("a"); - assertThat(parseCompileEvaluate("[\"a\", \"b\", \"c\"][4]")).isNull(); - assertThat(parseCompileEvaluate("[\"a\", \"b\", \"c\"][984]")).isNull(); - assertThat(parseCompileEvaluate("[\"a\", \"b\", \"c\"][-4]")).isNull(); - assertThat(parseCompileEvaluate("[\"a\", \"b\", \"c\"][-984]")).isNull(); - assertThat(parseCompileEvaluate("\"a\"[1]")).isEqualTo("a"); - assertThat(parseCompileEvaluate("\"a\"[2]")).isNull(); - assertThat(parseCompileEvaluate("\"a\"[-1]")).isEqualTo("a"); - assertThat(parseCompileEvaluate("\"a\"[-2]")).isNull(); + assertThat(parseCodegenCompileEvaluate("[\"a\", \"b\", \"c\"][1]")).isEqualTo("a"); + assertThat(parseCodegenCompileEvaluate("[\"a\", \"b\", \"c\"][2]")).isEqualTo("b"); + assertThat(parseCodegenCompileEvaluate("[\"a\", \"b\", \"c\"][3]")).isEqualTo("c"); + assertThat(parseCodegenCompileEvaluate("[\"a\", \"b\", \"c\"][-1]")).isEqualTo("c"); + assertThat(parseCodegenCompileEvaluate("[\"a\", \"b\", \"c\"][-2]")).isEqualTo("b"); + assertThat(parseCodegenCompileEvaluate("[\"a\", \"b\", \"c\"][-3]")).isEqualTo("a"); + assertThat(parseCodegenCompileEvaluate("[\"a\", \"b\", \"c\"][4]")).isNull(); + assertThat(parseCodegenCompileEvaluate("[\"a\", \"b\", \"c\"][984]")).isNull(); + assertThat(parseCodegenCompileEvaluate("[\"a\", \"b\", \"c\"][-4]")).isNull(); + assertThat(parseCodegenCompileEvaluate("[\"a\", \"b\", \"c\"][-984]")).isNull(); + assertThat(parseCodegenCompileEvaluate("\"a\"[1]")).isEqualTo("a"); + assertThat(parseCodegenCompileEvaluate("\"a\"[2]")).isNull(); + assertThat(parseCodegenCompileEvaluate("\"a\"[-1]")).isEqualTo("a"); + assertThat(parseCodegenCompileEvaluate("\"a\"[-2]")).isNull(); // Filtering by boolean expression - assertThat(parseCompileEvaluate("[1, 2, 3, 4][item = 4]")).asList().containsExactly(BigDecimal.valueOf(4)); - assertThat(parseCompileEvaluate("[1, 2, 3, 4][item > 2]")).asList().containsExactly(BigDecimal.valueOf(3), BigDecimal.valueOf(4)); - assertThat(parseCompileEvaluate("[1, 2, 3, 4][item > 5]")).asList().isEmpty(); - assertThat(parseCompileEvaluate("[ {x:1, y:2}, {x:2, y:3} ][x = 1]")).asList().containsExactly(mapOf(entry("x", new BigDecimal(1)), entry("y", new BigDecimal(2)))); - assertThat(parseCompileEvaluate("[ {x:1, y:2}, {x:2, y:3} ][x > 1]")).asList().containsExactly(mapOf(entry("x", new BigDecimal(2)), entry("y", new BigDecimal(3)))); - assertThat(parseCompileEvaluate("[ {x:1, y:2}, {x:2, y:3} ][x = 0]")).asList().isEmpty(); + assertThat(parseCodegenCompileEvaluate("[1, 2, 3, 4][item = 4]")).asList().containsExactly(BigDecimal.valueOf(4)); + assertThat(parseCodegenCompileEvaluate("[1, 2, 3, 4][item > 2]")).asList().containsExactly(BigDecimal.valueOf(3), BigDecimal.valueOf(4)); + assertThat(parseCodegenCompileEvaluate("[1, 2, 3, 4][item > 5]")).asList().isEmpty(); + assertThat(parseCodegenCompileEvaluate("[ {x:1, y:2}, {x:2, y:3} ][x = 1]")).asList().containsExactly(mapOf(entry("x", new BigDecimal(1)), entry("y", new BigDecimal(2)))); + assertThat(parseCodegenCompileEvaluate("[ {x:1, y:2}, {x:2, y:3} ][x > 1]")).asList().containsExactly(mapOf(entry("x", new BigDecimal(2)), entry("y", new BigDecimal(3)))); + assertThat(parseCodegenCompileEvaluate("[ {x:1, y:2}, {x:2, y:3} ][x = 0]")).asList().isEmpty(); } @Test void filter_path_tricky1() { - CompiledFEELExpression nameRef = parse( "[ {x:1, y:2}, {x:2, y:3} ][x]"); + CompiledFEELExpression nameRef = CompilerUtils.parseCodegen("[ {x:1, y:2}, {x:2, y:3} ][x]"); LOG.debug("{}", nameRef); EvaluationContext context = CodegenTestUtil.newEmptyEvaluationContext(); @@ -233,7 +236,7 @@ void filter_path_tricky1() { @Test void filter_path_tricky2() { - CompiledFEELExpression nameRef = parse("[ {x:1, y:2}, {x:2, y:3} ][x]"); + CompiledFEELExpression nameRef = CompilerUtils.parseCodegen("[ {x:1, y:2}, {x:2, y:3} ][x]"); LOG.debug("{}", nameRef); EvaluationContext context = CodegenTestUtil.newEmptyEvaluationContext(); @@ -247,85 +250,103 @@ void filter_path_tricky2() { @Test void filter_path_selection() { // Selection - assertThat(parseCompileEvaluate("[ {x:1, y:2}, {x:2, y:3} ].y")).asList().containsExactly(BigDecimal.valueOf(2), BigDecimal.valueOf(3)); - assertThat(parseCompileEvaluate("[ {x:1, y:2}, {x:2} ].y")).asList().containsExactly(BigDecimal.valueOf(2), null); - assertThat(parseCompileEvaluate("[ {x:1, y:2}, {x:2, y:3} ].z")).asList().containsExactly(null, null); + assertThat(parseCodegenCompileEvaluate("[ {x:1, y:2}, {x:2, y:3} ].y")).asList().containsExactly(BigDecimal.valueOf(2), BigDecimal.valueOf(3)); + assertThat(parseCodegenCompileEvaluate("[ {x:1, y:2}, {x:2} ].y")).asList().containsExactly(BigDecimal.valueOf(2), null); + assertThat(parseCodegenCompileEvaluate("[ {x:1, y:2}, {x:2, y:3} ].z")).asList().containsExactly(null, null); } @Test void test_for() { // for - Object parseCompileEvaluate = parseCompileEvaluate("for x in [ 10, 20, 30 ], y in [ 1, 2, 3 ] return x * y"); + Object parseCompileEvaluate = parseCodegenCompileEvaluate("for x in [ 10, 20, 30 ], y in [ 1, 2, 3 ] return x * y"); assertThat(parseCompileEvaluate).asList(). containsExactly(BigDecimal.valueOf(10), BigDecimal.valueOf(20), BigDecimal.valueOf(30), BigDecimal.valueOf(20), BigDecimal.valueOf(40), BigDecimal.valueOf(60), BigDecimal.valueOf(30), BigDecimal.valueOf(60), BigDecimal.valueOf(90)); // normal: - assertThat(parseCompileEvaluate("for x in [1, 2, 3] return x+1")).asList(). + assertThat(parseCodegenCompileEvaluate("for x in [1, 2, 3] return x+1")).asList(). containsExactly(BigDecimal.valueOf(2), BigDecimal.valueOf(3), BigDecimal.valueOf(4)); // TODO in order to parse correctly the enhanced for loop it is required to configure the FEEL Profiles } + @Test + void test_nested_for() { + List firstExpected = Arrays.asList(BigDecimal.ONE, BigDecimal.valueOf(2)); + List secondExpected = Arrays.asList(BigDecimal.valueOf(3), BigDecimal.valueOf(4)); + Object parseCompileEvaluate = parseCodegenCompileEvaluate("for x in [ [1, 2], [3, 4] ] return x"); + assertThat(parseCompileEvaluate).asList(). + containsExactly(firstExpected, secondExpected); + parseCompileEvaluate = parseCodegenCompileEvaluate("for x in [ [1,2], [3,4] ] return for y in x return y"); + assertThat(parseCompileEvaluate).asList(). + containsExactly(firstExpected, secondExpected); + parseCompileEvaluate = CompilerUtils.parseCodegenCompileEvaluate("for x in [ 1, 2, 3, 4 ], y in x return y"); + assertThat(parseCompileEvaluate).asList(). + containsExactly(BigDecimal.ONE, BigDecimal.valueOf(2), BigDecimal.valueOf(3), BigDecimal.valueOf(4)); + parseCompileEvaluate = parseCodegenCompileEvaluate("for x in [ [1,2], [3,4] ], y in x return y"); + assertThat(parseCompileEvaluate).asList(). + containsExactly(BigDecimal.ONE, BigDecimal.valueOf(2), BigDecimal.valueOf(3), BigDecimal.valueOf(4)); + } + @Test void quantified_expressions() { // quantified expressions - assertThat(parseCompileEvaluate("some price in [ 80, 11, 110 ] satisfies price > 100")).isEqualTo(Boolean.TRUE); - assertThat(parseCompileEvaluate("some price in [ 80, 11, 90 ] satisfies price > 100")).isEqualTo(Boolean.FALSE); - assertThat(parseCompileEvaluate("some x in [ 5, 6, 7 ], y in [ 10, 11, 6 ] satisfies x > y")).isEqualTo(Boolean.TRUE); - assertThat(parseCompileEvaluate("every price in [ 80, 11, 90 ] satisfies price > 10")).isEqualTo(Boolean.TRUE); - assertThat(parseCompileEvaluate("every price in [ 80, 11, 90 ] satisfies price > 70")).isEqualTo(Boolean.FALSE); - assertThat(parseCompileEvaluate("some x in [ 5, 6, 7 ], y in [ 10, 11, 12 ] satisfies x < y")).isEqualTo(Boolean.TRUE); - assertThat(parseCompileEvaluate("some price in [ 80, 11, 110 ] satisfies price > max(100, 50, 10)")).isEqualTo(Boolean.TRUE); + assertThat(parseCodegenCompileEvaluate("some price in [ 80, 11, 110 ] satisfies price > 100")).isEqualTo(Boolean.TRUE); + assertThat(parseCodegenCompileEvaluate("some price in [ 80, 11, 90 ] satisfies price > 100")).isEqualTo(Boolean.FALSE); + assertThat(parseCodegenCompileEvaluate("some x in [ 5, 6, 7 ], y in [ 10, 11, 6 ] satisfies x > y")).isEqualTo(Boolean.TRUE); + assertThat(parseCodegenCompileEvaluate("every price in [ 80, 11, 90 ] satisfies price > 10")).isEqualTo(Boolean.TRUE); + assertThat(parseCodegenCompileEvaluate("every price in [ 80, 11, 90 ] satisfies price > 70")).isEqualTo(Boolean.FALSE); + assertThat(parseCodegenCompileEvaluate("some x in [ 5, 6, 7 ], y in [ 10, 11, 12 ] satisfies x < y")).isEqualTo(Boolean.TRUE); + assertThat(parseCodegenCompileEvaluate("some price in [ 80, 11, 110 ] satisfies price > max(100, 50, 10)")).isEqualTo(Boolean.TRUE); } @Test void basic_function_invocation() { - assertThat(parseCompileEvaluate("max(1, 2, 3)")).isEqualTo(new BigDecimal(3)); + assertThat(parseCodegenCompileEvaluate("max(1, 2, 3)")).isEqualTo(new BigDecimal(3)); } @Test void basic_function_definition() { - assertThat(parseCompileEvaluate("function (a, b) a + b")).isInstanceOf(CustomFEELFunction.class); - assertThat(parseCompileEvaluate("{ s : function (a, b) a + b, x : 1, y : 2, r : s(x,y) }.r")).isEqualTo(new BigDecimal(3)); + assertThat(parseCodegenCompileEvaluate("function (a, b) a + b")).isInstanceOf(CustomFEELFunction.class); + assertThat(parseCodegenCompileEvaluate("{ s : function (a, b) a + b, x : 1, y : 2, r : s(x,y) }.r")).isEqualTo(new BigDecimal(3)); } @Test void named_function_invocation() { - assertThat(parseCompileEvaluate("substring(start position: 2, string: \"FOOBAR\")")).isEqualTo("OOBAR"); - assertThat(parseCompileEvaluate("ceiling( n : 1.5 )")).isEqualTo(new BigDecimal("2")); + assertThat(parseCodegenCompileEvaluate("substring(start position: 2, string: \"FOOBAR\")")).isEqualTo("OOBAR"); + assertThat(parseCodegenCompileEvaluate("ceiling( n : 1.5 )")).isEqualTo(new BigDecimal("2")); } @Test void misc_from_original_feelinterpreted_test_suite() { - assertThat(parseCompileEvaluate("if null then \"foo\" else \"bar\"")).isEqualTo("bar"); - assertThat(parseCompileEvaluate("{ hello world : function() \"Hello World!\", message : hello world() }.message")).isEqualTo("Hello World!"); - assertThat(parseCompileEvaluate("1 + if true then 1 else 2")).isEqualTo(new BigDecimal("2")); - assertThat(parseCompileEvaluate("\"string with \\\"quotes\\\"\"")).isEqualTo("string with \"quotes\""); - assertThat(parseCompileEvaluate("date( -0105, 8, 2 )")).isEqualTo(LocalDate.of(-105, 8, 2)); - assertThat(parseCompileEvaluate("string(null)")).isNull(); - assertThat(parseCompileEvaluate("[ null ]")).asList().containsExactly(new Object[]{null}); - assertThat(parseCompileEvaluate("[ null, null ]")).asList().containsExactly(null, null); - assertThat(parseCompileEvaluate("[ null, 47, null ]")).asList().containsExactly(null, BigDecimal.valueOf(47), null); + assertThat(parseCodegenCompileEvaluate("if null then \"foo\" else \"bar\"")).isEqualTo("bar"); + assertThat(parseCodegenCompileEvaluate("{ hello world : function() \"Hello World!\", message : hello world() }.message")).isEqualTo("Hello World!"); + assertThat(parseCodegenCompileEvaluate("1 + if true then 1 else 2")).isEqualTo(new BigDecimal("2")); + assertThat(parseCodegenCompileEvaluate("\"string with \\\"quotes\\\"\"")).isEqualTo("string with \"quotes\""); + assertThat(parseCodegenCompileEvaluate("date( -0105, 8, 2 )")).isEqualTo(LocalDate.of(-105, 8, 2)); + assertThat(parseCodegenCompileEvaluate("string(null)")).isNull(); + assertThat(parseCodegenCompileEvaluate("[ null ]")).asList().containsExactly(new Object[]{null}); + assertThat(parseCodegenCompileEvaluate("[ null, null ]")).asList().containsExactly(null, null); + assertThat(parseCodegenCompileEvaluate("[ null, 47, null ]")).asList().containsExactly(null, BigDecimal.valueOf(47), null); } @Test void benchmark_feel_expressions() { - assertThat(parseCompileEvaluate("{ full name: { first name: \"John\", last name: \"Doe\" } }.full name.last name")).isEqualTo("Doe"); - assertThat(parseCompileEvaluate("some price in [ 80, 11, 110 ] satisfies price > 100")).isEqualTo(Boolean.TRUE); - assertThat(parseCompileEvaluate("every price in [ 80, 11, 90 ] satisfies price > 10")).isEqualTo(Boolean.TRUE); + assertThat(parseCodegenCompileEvaluate("{ full name: { first name: \"John\", last name: \"Doe\" } }.full name.last name")).isEqualTo("Doe"); + assertThat(parseCodegenCompileEvaluate("some price in [ 80, 11, 110 ] satisfies price > 100")).isEqualTo(Boolean.TRUE); + assertThat(parseCodegenCompileEvaluate("every price in [ 80, 11, 90 ] satisfies price > 10")).isEqualTo(Boolean.TRUE); } @Test void context_expression() { - assertThat(parseCompileEvaluate("{}")).isEqualTo(Collections.emptyMap()); - assertThat(parseCompileEvaluate("{ }")).isEqualTo(Collections.emptyMap()); - assertThat(parseCompileEvaluate("{ a : 1 }")).isEqualTo(mapOf(entry("a", new BigDecimal(1)))); - assertThat(parseCompileEvaluate("{ \"a\" : 1 }")).isEqualTo(mapOf(entry("a", new BigDecimal(1)))); - assertThat(parseCompileEvaluate("{ \" a\" : 1 }")).isEqualTo(mapOf(entry("a", new BigDecimal(1)))); // Demonstrating a bad practice. - assertThat(parseCompileEvaluate("{ a : 1, b : 2, c : 3 }")).isEqualTo(mapOf(entry("a", new BigDecimal(1)), entry("b", new BigDecimal(2)), entry("c", new BigDecimal(3)))); - assertThat(parseCompileEvaluate("{ a : 1, a name : \"John Doe\" }")).isEqualTo(mapOf(entry("a", new BigDecimal(1)), entry("a name", "John Doe"))); + assertThat(parseCodegenCompileEvaluate("{}")).isEqualTo(Collections.emptyMap()); + assertThat(parseCodegenCompileEvaluate("{ }")).isEqualTo(Collections.emptyMap()); + assertThat(parseCodegenCompileEvaluate("{ a : 1 }")).isEqualTo(mapOf(entry("a", new BigDecimal(1)))); + assertThat(parseCodegenCompileEvaluate("{ \"a\" : 1 }")).isEqualTo(mapOf(entry("a", new BigDecimal(1)))); + assertThat(parseCodegenCompileEvaluate("{ \" a\" : 1 }")).isEqualTo(mapOf(entry("a", new BigDecimal(1)))); // Demonstrating a bad practice. + assertThat(parseCodegenCompileEvaluate("{ a : 1, b : 2, c : 3 }")).isEqualTo(mapOf(entry("a", new BigDecimal(1)), entry("b", new BigDecimal(2)), entry("c", new BigDecimal(3)))); + assertThat(parseCodegenCompileEvaluate("{ a : 1, a name : \"John Doe\" }")).isEqualTo(mapOf(entry("a", new BigDecimal(1)), entry("a name", "John Doe"))); - assertThat(parseCompileEvaluate("{ a : 1, b : a }")).isEqualTo(mapOf(entry("a", new BigDecimal(1)), entry("b", new BigDecimal(1)))); + assertThat(parseCodegenCompileEvaluate("{ a : 1, b : a }")).isEqualTo(mapOf(entry("a", new BigDecimal(1)), entry("b", new BigDecimal(1)))); } /** @@ -336,7 +357,7 @@ void contextWithMultipleEntries() { String inputExpression = "{ \"a string key\" : 10," + "\n" + " a non-string key : 11," + "\n" + " a key.with + /' odd chars : 12 }"; - assertThat(parseCompileEvaluate(inputExpression)).isEqualTo(mapOf(entry("a string key", new BigDecimal(10)), entry("a non-string key", new BigDecimal(11)), entry("a key.with + /' odd chars", new BigDecimal(12)))); + assertThat(parseCodegenCompileEvaluate(inputExpression)).isEqualTo(mapOf(entry("a string key", new BigDecimal(10)), entry("a non-string key", new BigDecimal(11)), entry("a key.with + /' odd chars", new BigDecimal(12)))); } /** @@ -356,8 +377,8 @@ void nestedContexts() { + " xxx: last + name" + "\n" + " } " + "\n" + "}"; - assertThat(parseCompileEvaluate(inputExpression)).isEqualTo(mapOf(entry("a value", new BigDecimal(10)), - entry("an applicant", mapOf(entry("first name", "Edson"), + assertThat(parseCodegenCompileEvaluate(inputExpression)).isEqualTo(mapOf(entry("a value", new BigDecimal(10)), + entry("an applicant", mapOf(entry("first name", "Edson"), entry("last + name", "Tirelli"), entry("full name", "EdsonTirelli"), entry("address", mapOf(entry("street", "55 broadway st"), @@ -378,15 +399,15 @@ void nestedContexts2() { " }, \n" + " street : an applicant.home address.street name \n" + "} "; - assertThat(parseCompileEvaluate(complexContext)).isEqualTo(mapOf(entry("an applicant", mapOf(entry("home address", mapOf(entry("street name", "broadway st"), - entry("city", "New York"))))), - entry("street", "broadway st"))); + assertThat(parseCodegenCompileEvaluate(complexContext)).isEqualTo(mapOf(entry("an applicant", mapOf(entry("home address", mapOf(entry("street name", "broadway st"), + entry("city", "New York"))))), + entry("street", "broadway st"))); } @Test void nameReference() { String inputExpression = "someSimpleName"; - CompiledFEELExpression nameRef = parse( inputExpression, mapOf( entry("someSimpleName", BuiltInType.STRING) ) ); + CompiledFEELExpression nameRef = parseCodegen(inputExpression, mapOf(entry("someSimpleName", BuiltInType.STRING) ) ); LOG.debug("{}", nameRef); EvaluationContext context = CodegenTestUtil.newEmptyEvaluationContext(); @@ -401,7 +422,7 @@ void nameReference() { void qualifiedName() { String inputExpression = "My Person.Full Name"; Type personType = new MapBackedType("Person", mapOf( entry("Full Name", BuiltInType.STRING), entry("Age", BuiltInType.NUMBER) ) ); - CompiledFEELExpression qualRef = parse( inputExpression, mapOf( entry("My Person", personType) ) ); + CompiledFEELExpression qualRef = parseCodegen(inputExpression, mapOf(entry("My Person", personType) ) ); LOG.debug("{}", qualRef); EvaluationContext context = CodegenTestUtil.newEmptyEvaluationContext(); @@ -412,7 +433,7 @@ void qualifiedName() { assertThat(result).isEqualTo("John Doe" ); // check number coercion for qualified name - CompiledFEELExpression personAgeExpression = parse("My Person.Age", mapOf(entry("My Person", personType))); + CompiledFEELExpression personAgeExpression = parseCodegen("My Person.Age", mapOf(entry("My Person", personType))); LOG.debug("{}", personAgeExpression); Object resultPersonAge = personAgeExpression.apply(context); // Please notice input variable in context is a Map containing and entry value for int 47. @@ -432,7 +453,7 @@ public String getFullName() { void qualifiedName2() { String inputExpression = "My Person.Full Name"; Type personType = JavaBackedType.of(MyPerson.class); - CompiledFEELExpression qualRef = parse( inputExpression, mapOf( entry("My Person", personType) ) ); + CompiledFEELExpression qualRef = parseCodegen(inputExpression, mapOf(entry("My Person", personType) ) ); LOG.debug("{}", qualRef); EvaluationContext context = CodegenTestUtil.newEmptyEvaluationContext(); @@ -447,7 +468,7 @@ void qualifiedName2() { void qualifiedName3() { String inputExpression = "a date.year"; Type dateType = BuiltInType.DATE; - CompiledFEELExpression qualRef = parse(inputExpression, mapOf(entry("a date", dateType))); + CompiledFEELExpression qualRef = parseCodegen(inputExpression, mapOf(entry("a date", dateType))); LOG.debug("{}", qualRef); EvaluationContext context = CodegenTestUtil.newEmptyEvaluationContext(); diff --git a/kie-dmn/kie-dmn-feel/src/test/java/org/kie/dmn/feel/lang/ast/ForExpressionNodeTest.java b/kie-dmn/kie-dmn-feel/src/test/java/org/kie/dmn/feel/lang/ast/ForExpressionNodeTest.java new file mode 100644 index 00000000000..290abe5132f --- /dev/null +++ b/kie-dmn/kie-dmn-feel/src/test/java/org/kie/dmn/feel/lang/ast/ForExpressionNodeTest.java @@ -0,0 +1,89 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.kie.dmn.feel.lang.ast; + +import java.math.BigDecimal; +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import org.junit.jupiter.api.Test; +import org.kie.dmn.feel.codegen.feel11.CodegenTestUtil; +import org.kie.dmn.feel.lang.Type; +import org.kie.dmn.feel.lang.types.BuiltInType; + +import static org.assertj.core.api.Assertions.assertThat; + +class ForExpressionNodeTest { + + @Test + void evaluateSimpleArray() { + IterationContextNode x = getIterationContextNode("x", getListNode("[ 1, 2, 3, 4 ]", Arrays.asList("1", "2", "3", "4")), "x in [ 1, 2, 3, 4 ]"); + IterationContextNode y = getIterationContextNode("y", getNameRefNode(BuiltInType.UNKNOWN, "x"), "y in x"); + ForExpressionNode forExpressionNode = new ForExpressionNode(Arrays.asList(x, y), getNameRefNode(BuiltInType.UNKNOWN, "y"), "for x in [ 1, 2, 3, 4 ], y in x return y"); + Object retrieved = forExpressionNode.evaluate(CodegenTestUtil.newEmptyEvaluationContext()); + assertThat(retrieved).isInstanceOf(List.class).asList(). + containsExactly(BigDecimal.ONE, BigDecimal.valueOf(2), BigDecimal.valueOf(3), BigDecimal.valueOf(4)); + } + + @Test + void evaluateNestedArray() { + Map> firstIterationContext = new LinkedHashMap<>(); + firstIterationContext.put("1, 2", Arrays.asList("1", "2")); + firstIterationContext.put("3, 4", Arrays.asList("3", "4")); + IterationContextNode x = getIterationContextNode("x", getNestedListNode("[ [1, 2], [3, 4] ]", firstIterationContext), "x in [ [1, 2], [3, 4] ]"); + IterationContextNode y = getIterationContextNode("y", getNameRefNode(BuiltInType.UNKNOWN, "x"), "y in x"); + ForExpressionNode forExpressionNode = new ForExpressionNode(Arrays.asList(x, y), getNameRefNode(BuiltInType.UNKNOWN, "y"), "for x in [ [1, 2], [3, 4] ], y in x return y"); + Object retrieved = forExpressionNode.evaluate(CodegenTestUtil.newEmptyEvaluationContext()); + assertThat(retrieved).isInstanceOf(List.class).asList(). + containsExactly(BigDecimal.ONE, BigDecimal.valueOf(2), BigDecimal.valueOf(3), BigDecimal.valueOf(4)); + + } + + private IterationContextNode getIterationContextNode(String variableName, BaseNode expression, String text) { + return new IterationContextNode(getNameDefNode(variableName), expression, null, text); + } + + private NameDefNode getNameDefNode(String text) { + return new NameDefNode(Collections.singletonList(text), null, text); + } + + private NameRefNode getNameRefNode(Type type, String text) { + return new NameRefNode(type, text); + } + + private ListNode getNestedListNode(String text, Map> values) { + List elements = values.entrySet() + .stream() + .map(entry -> getListNode(entry.getKey(), entry.getValue())) + .map(BaseNode.class::cast) + .toList(); + return new ListNode(elements, text); + } + + private ListNode getListNode(String text, List values) { + List elements = values.stream() + .map(value -> new NumberNode(new BigDecimal(value), value)) + .map(BaseNode.class::cast) + .toList(); + return new ListNode(elements, text); + } +} \ No newline at end of file diff --git a/kie-dmn/kie-dmn-feel/src/test/java/org/kie/dmn/feel/runtime/FEELCompilerTest.java b/kie-dmn/kie-dmn-feel/src/test/java/org/kie/dmn/feel/runtime/FEELCompilerTest.java new file mode 100644 index 00000000000..80502839d79 --- /dev/null +++ b/kie-dmn/kie-dmn-feel/src/test/java/org/kie/dmn/feel/runtime/FEELCompilerTest.java @@ -0,0 +1,484 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.kie.dmn.feel.runtime; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import org.junit.jupiter.api.Test; +import org.kie.dmn.feel.codegen.feel11.CodegenTestUtil; +import org.kie.dmn.feel.codegen.feel11.CompiledFEELExpression; +import org.kie.dmn.feel.lang.EvaluationContext; +import org.kie.dmn.feel.lang.FEELProperty; +import org.kie.dmn.feel.lang.Type; +import org.kie.dmn.feel.lang.impl.JavaBackedType; +import org.kie.dmn.feel.lang.impl.MapBackedType; +import org.kie.dmn.feel.lang.types.BuiltInType; +import org.kie.dmn.feel.parser.feel11.FEELParserTest; +import org.kie.dmn.feel.runtime.functions.CustomFEELFunction; +import org.kie.dmn.feel.util.CompilerUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.kie.dmn.feel.util.CompilerUtils.parseInterpreted; +import static org.kie.dmn.feel.util.CompilerUtils.parseInterpretedCompileEvaluate; +import static org.kie.dmn.feel.util.DynamicTypeUtils.entry; +import static org.kie.dmn.feel.util.DynamicTypeUtils.mapOf; + +public class FEELCompilerTest { + + public static final Logger LOG = LoggerFactory.getLogger(FEELCompilerTest.class); + + + @Test + void feel_number() { + assertThat(parseInterpretedCompileEvaluate("10")).isEqualTo(BigDecimal.valueOf(10)); + } + + @Test + void feel_negative_number() { + assertThat(parseInterpretedCompileEvaluate("-10")).isEqualTo(BigDecimal.valueOf(-10)); + } + + @Test + void feel_drools_2143() { + // DROOLS-2143: Allow ''--1' expression as per FEEL grammar rule 26 + assertThat(parseInterpretedCompileEvaluate("--10")).isEqualTo(BigDecimal.valueOf(10)); + assertThat(parseInterpretedCompileEvaluate("---10")).isEqualTo(BigDecimal.valueOf(-10)); + assertThat(parseInterpretedCompileEvaluate("+10")).isEqualTo(BigDecimal.valueOf(10)); + } + + @Test + void feel_boolean() { + assertThat(parseInterpretedCompileEvaluate("false")).isEqualTo(Boolean.FALSE); + assertThat(parseInterpretedCompileEvaluate("true")).isEqualTo(Boolean.TRUE); + assertThat(parseInterpretedCompileEvaluate("null")).isNull(); + } + + @Test + void feel_null() { + assertThat(parseInterpretedCompileEvaluate("null")).isNull(); + } + + @Test + void feel_string() { + assertThat(parseInterpretedCompileEvaluate("\"some string\"")).isEqualTo("some string" ); + } + + @Test + void primary_parens() { + assertThat(parseInterpretedCompileEvaluate("(\"some string\")")).isEqualTo("some string" ); + assertThat(parseInterpretedCompileEvaluate("(123)")).isEqualTo(BigDecimal.valueOf(123)); + assertThat(parseInterpretedCompileEvaluate("(-123)")).isEqualTo(BigDecimal.valueOf(-123)); + assertThat(parseInterpretedCompileEvaluate("-(123)")).isEqualTo(BigDecimal.valueOf(-123)); + assertThat(parseInterpretedCompileEvaluate("(false)")).isEqualTo(Boolean.FALSE); + assertThat(parseInterpretedCompileEvaluate("(true)")).isEqualTo(Boolean.TRUE); + } + + /** + * See {@link FEELTernaryLogicTest} + */ + @Test + void ternary_logic() { + assertThat(parseInterpretedCompileEvaluate("true and true")).isEqualTo(Boolean.TRUE); + assertThat(parseInterpretedCompileEvaluate("true and false")).isEqualTo(Boolean.FALSE); + assertThat(parseInterpretedCompileEvaluate("true and null")).isNull(); + assertThat(parseInterpretedCompileEvaluate("false and true")).isEqualTo(Boolean.FALSE); + assertThat(parseInterpretedCompileEvaluate("false and false")).isEqualTo(Boolean.FALSE); + assertThat(parseInterpretedCompileEvaluate("false and null")).isEqualTo(Boolean.FALSE); + assertThat(parseInterpretedCompileEvaluate("null and true")).isNull(); + assertThat(parseInterpretedCompileEvaluate("null and false")).isEqualTo(Boolean.FALSE); + assertThat(parseInterpretedCompileEvaluate("null and null")).isNull(); + assertThat(parseInterpretedCompileEvaluate("true or true")).isEqualTo(Boolean.TRUE); + assertThat(parseInterpretedCompileEvaluate("true or false")).isEqualTo(Boolean.TRUE); + assertThat(parseInterpretedCompileEvaluate("true or null")).isEqualTo(Boolean.TRUE); + assertThat(parseInterpretedCompileEvaluate("false or true")).isEqualTo(Boolean.TRUE); + assertThat(parseInterpretedCompileEvaluate("false or false")).isEqualTo(Boolean.FALSE); + assertThat(parseInterpretedCompileEvaluate("false or null")).isNull(); + assertThat(parseInterpretedCompileEvaluate("null or true")).isEqualTo(Boolean.TRUE); + assertThat(parseInterpretedCompileEvaluate("null or false")).isNull(); + assertThat(parseInterpretedCompileEvaluate("null or null")).isNull(); + // logical operator priority + assertThat(parseInterpretedCompileEvaluate("false and false or true")).isEqualTo(Boolean.TRUE); + assertThat(parseInterpretedCompileEvaluate("false and (false or true)")).isEqualTo(Boolean.FALSE); + assertThat(parseInterpretedCompileEvaluate("true or false and false")).isEqualTo(Boolean.TRUE); + assertThat(parseInterpretedCompileEvaluate("(true or false) and false")).isEqualTo(Boolean.FALSE); + } + + /** + * Partially from {@link FEELConditionsAndLoopsTest} + */ + @Test + void test_if() { + assertThat(parseInterpretedCompileEvaluate("if true then 15 else 5")).isEqualTo(BigDecimal.valueOf(15 )); + assertThat(parseInterpretedCompileEvaluate("if false then 15 else 5")).isEqualTo(BigDecimal.valueOf(5 )); + assertThat(parseInterpretedCompileEvaluate("if null then 15 else 5")).isEqualTo(BigDecimal.valueOf(5)); + assertThat(parseInterpretedCompileEvaluate("if \"hello\" then 15 else 5")).isEqualTo(BigDecimal.valueOf(5)); + } + + @Test + void additive_expression() { + assertThat(parseInterpretedCompileEvaluate("1 + 2")).isEqualTo(BigDecimal.valueOf(3 )); + assertThat(parseInterpretedCompileEvaluate("1 + null")).isNull(); + assertThat(parseInterpretedCompileEvaluate("1 - 2")).isEqualTo(BigDecimal.valueOf(-1 )); + assertThat(parseInterpretedCompileEvaluate("1 - null")).isNull(); + assertThat(parseInterpretedCompileEvaluate("\"Hello, \" + \"World\"")).isEqualTo("Hello, World"); + } + + @Test + void multiplicative_expression() { + assertThat(parseInterpretedCompileEvaluate("3 * 5")).isEqualTo(BigDecimal.valueOf(15)); + assertThat(parseInterpretedCompileEvaluate("3 * null")).isNull(); + assertThat(parseInterpretedCompileEvaluate("10 / 2")).isEqualTo(BigDecimal.valueOf(5)); + assertThat(parseInterpretedCompileEvaluate("10 / null")).isNull(); + } + + @Test + void exponentiation_expression() { + assertThat(parseInterpretedCompileEvaluate("3 ** 3")).isEqualTo(BigDecimal.valueOf(27)); + assertThat(parseInterpretedCompileEvaluate("3 ** null")).isNull(); + } + + @Test + void logical_negation_expression() { + // this is all invalid syntax + assertThat(parseInterpretedCompileEvaluate("not true")).isNull(); + assertThat(parseInterpretedCompileEvaluate("not false")).isNull(); + assertThat(parseInterpretedCompileEvaluate("not null")).isNull(); + assertThat(parseInterpretedCompileEvaluate("not 3")).isNull(); + } + + @Test + void list_expression() { + assertThat(parseInterpretedCompileEvaluate("[]")).asList().isEmpty(); + assertThat(parseInterpretedCompileEvaluate("[ ]")).asList().isEmpty(); + assertThat(parseInterpretedCompileEvaluate("[1]")).asList().containsExactly(BigDecimal.valueOf(1)); + assertThat(parseInterpretedCompileEvaluate("[1, 2,3]")).asList().containsExactly(BigDecimal.valueOf(1), BigDecimal.valueOf(2), BigDecimal.valueOf(3)); + } + + @Test + void instance_of_expression() { + assertThat(parseInterpretedCompileEvaluate("123 instance of number")).isEqualTo(Boolean.TRUE); + assertThat(parseInterpretedCompileEvaluate("\"ciao\" instance of number")).isEqualTo(Boolean.FALSE); + assertThat(parseInterpretedCompileEvaluate("123 instance of string")).isEqualTo(Boolean.FALSE); + assertThat(parseInterpretedCompileEvaluate("\"ciao\" instance of string")).isEqualTo(Boolean.TRUE); + } + + @Test + void between() { + assertThat(parseInterpretedCompileEvaluate("10 between 5 and 12")).isEqualTo(Boolean.TRUE); + assertThat(parseInterpretedCompileEvaluate("10 between 20 and 30")).isEqualTo(Boolean.FALSE); + assertThat(parseInterpretedCompileEvaluate("10 between 5 and \"foo\"")).isNull(); + assertThat(parseInterpretedCompileEvaluate("\"foo\" between 5 and 12")).isNull(); + assertThat(parseInterpretedCompileEvaluate("\"foo\" between \"bar\" and \"zap\"")).isEqualTo(Boolean.TRUE); + assertThat(parseInterpretedCompileEvaluate("\"foo\" between null and \"zap\"")).isNull(); + } + + @Test + void filter_path() { + // Filtering by index + assertThat(parseInterpretedCompileEvaluate("[\"a\", \"b\", \"c\"][1]")).isEqualTo("a"); + assertThat(parseInterpretedCompileEvaluate("[\"a\", \"b\", \"c\"][2]")).isEqualTo("b"); + assertThat(parseInterpretedCompileEvaluate("[\"a\", \"b\", \"c\"][3]")).isEqualTo("c"); + assertThat(parseInterpretedCompileEvaluate("[\"a\", \"b\", \"c\"][-1]")).isEqualTo("c"); + assertThat(parseInterpretedCompileEvaluate("[\"a\", \"b\", \"c\"][-2]")).isEqualTo("b"); + assertThat(parseInterpretedCompileEvaluate("[\"a\", \"b\", \"c\"][-3]")).isEqualTo("a"); + assertThat(parseInterpretedCompileEvaluate("[\"a\", \"b\", \"c\"][4]")).isNull(); + assertThat(parseInterpretedCompileEvaluate("[\"a\", \"b\", \"c\"][984]")).isNull(); + assertThat(parseInterpretedCompileEvaluate("[\"a\", \"b\", \"c\"][-4]")).isNull(); + assertThat(parseInterpretedCompileEvaluate("[\"a\", \"b\", \"c\"][-984]")).isNull(); + assertThat(parseInterpretedCompileEvaluate("\"a\"[1]")).isEqualTo("a"); + assertThat(parseInterpretedCompileEvaluate("\"a\"[2]")).isNull(); + assertThat(parseInterpretedCompileEvaluate("\"a\"[-1]")).isEqualTo("a"); + assertThat(parseInterpretedCompileEvaluate("\"a\"[-2]")).isNull(); + + // Filtering by boolean expression + assertThat(parseInterpretedCompileEvaluate("[1, 2, 3, 4][item = 4]")).asList().containsExactly(BigDecimal.valueOf(4)); + assertThat(parseInterpretedCompileEvaluate("[1, 2, 3, 4][item > 2]")).asList().containsExactly(BigDecimal.valueOf(3), BigDecimal.valueOf(4)); + assertThat(parseInterpretedCompileEvaluate("[1, 2, 3, 4][item > 5]")).asList().isEmpty(); + assertThat(parseInterpretedCompileEvaluate("[ {x:1, y:2}, {x:2, y:3} ][x = 1]")).asList().containsExactly(mapOf(entry("x", new BigDecimal(1)), entry("y", new BigDecimal(2)))); + assertThat(parseInterpretedCompileEvaluate("[ {x:1, y:2}, {x:2, y:3} ][x > 1]")).asList().containsExactly(mapOf(entry("x", new BigDecimal(2)), entry("y", new BigDecimal(3)))); + assertThat(parseInterpretedCompileEvaluate("[ {x:1, y:2}, {x:2, y:3} ][x = 0]")).asList().isEmpty(); + } + + @Test + void filter_path_tricky1() { + CompiledFEELExpression nameRef = CompilerUtils.parseInterpreted("[ {x:1, y:2}, {x:2, y:3} ][x]"); + LOG.debug("{}", nameRef); + + EvaluationContext context = CodegenTestUtil.newEmptyEvaluationContext(); + context.setValue("x", 2); + Object result = nameRef.apply(context); + LOG.debug("{}", result); + + assertThat(result).isEqualTo(mapOf(entry("x", new BigDecimal(2)), entry("y", new BigDecimal(3)))); + } + + @Test + void filter_path_tricky2() { + CompiledFEELExpression nameRef = CompilerUtils.parseInterpreted("[ {x:1, y:2}, {x:2, y:3} ][x]"); + LOG.debug("{}", nameRef); + + EvaluationContext context = CodegenTestUtil.newEmptyEvaluationContext(); + context.setValue("x", false); + Object result = nameRef.apply(context); + LOG.debug("{}", result); + + assertThat(result).asList().isEmpty(); + } + + @Test + void filter_path_selection() { + // Selection + assertThat(parseInterpretedCompileEvaluate("[ {x:1, y:2}, {x:2, y:3} ].y")).asList().containsExactly(BigDecimal.valueOf(2), BigDecimal.valueOf(3)); + assertThat(parseInterpretedCompileEvaluate("[ {x:1, y:2}, {x:2} ].y")).asList().containsExactly(BigDecimal.valueOf(2), null); + assertThat(parseInterpretedCompileEvaluate("[ {x:1, y:2}, {x:2, y:3} ].z")).asList().containsExactly(null, null); + } + + @Test + void test_for() { + // for + Object parseCompileEvaluate = parseInterpretedCompileEvaluate("for x in [ 10, 20, 30 ], y in [ 1, 2, 3 ] return x * y"); + assertThat(parseCompileEvaluate).asList(). + containsExactly(BigDecimal.valueOf(10), BigDecimal.valueOf(20), BigDecimal.valueOf(30), BigDecimal.valueOf(20), BigDecimal.valueOf(40), BigDecimal.valueOf(60), BigDecimal.valueOf(30), BigDecimal.valueOf(60), BigDecimal.valueOf(90)); + + // normal: + assertThat(parseInterpretedCompileEvaluate("for x in [1, 2, 3] return x+1")).asList(). + containsExactly(BigDecimal.valueOf(2), BigDecimal.valueOf(3), BigDecimal.valueOf(4)); + + // TODO in order to parse correctly the enhanced for loop it is required to configure the FEEL Profiles + } + + @Test + void test_nested_for() { + List firstExpected = Arrays.asList(BigDecimal.ONE, BigDecimal.valueOf(2)); + List secondExpected = Arrays.asList(BigDecimal.valueOf(3), BigDecimal.valueOf(4)); + Object parseCompileEvaluate = parseInterpretedCompileEvaluate("for x in [ [1, 2], [3, 4] ] return x"); + assertThat(parseCompileEvaluate).asList(). + containsExactly(firstExpected, secondExpected); + parseCompileEvaluate = parseInterpretedCompileEvaluate("for x in [ [1,2], [3,4] ] return for y in x return y"); + assertThat(parseCompileEvaluate).asList(). + containsExactly(firstExpected, secondExpected); + parseCompileEvaluate = CompilerUtils.parseInterpretedCompileEvaluate("for x in [ 1, 2, 3, 4 ], y in x return y"); + assertThat(parseCompileEvaluate).asList(). + containsExactly(BigDecimal.ONE, BigDecimal.valueOf(2), BigDecimal.valueOf(3), BigDecimal.valueOf(4)); + parseCompileEvaluate = parseInterpretedCompileEvaluate("for x in [ [1,2], [3,4] ], y in x return y"); + assertThat(parseCompileEvaluate).asList(). + containsExactly(BigDecimal.ONE, BigDecimal.valueOf(2), BigDecimal.valueOf(3), BigDecimal.valueOf(4)); + } + + @Test + void quantified_expressions() { + // quantified expressions + assertThat(parseInterpretedCompileEvaluate("some price in [ 80, 11, 110 ] satisfies price > 100")).isEqualTo(Boolean.TRUE); + assertThat(parseInterpretedCompileEvaluate("some price in [ 80, 11, 90 ] satisfies price > 100")).isEqualTo(Boolean.FALSE); + assertThat(parseInterpretedCompileEvaluate("some x in [ 5, 6, 7 ], y in [ 10, 11, 6 ] satisfies x > y")).isEqualTo(Boolean.TRUE); + assertThat(parseInterpretedCompileEvaluate("every price in [ 80, 11, 90 ] satisfies price > 10")).isEqualTo(Boolean.TRUE); + assertThat(parseInterpretedCompileEvaluate("every price in [ 80, 11, 90 ] satisfies price > 70")).isEqualTo(Boolean.FALSE); + assertThat(parseInterpretedCompileEvaluate("some x in [ 5, 6, 7 ], y in [ 10, 11, 12 ] satisfies x < y")).isEqualTo(Boolean.TRUE); + assertThat(parseInterpretedCompileEvaluate("some price in [ 80, 11, 110 ] satisfies price > max(100, 50, 10)")).isEqualTo(Boolean.TRUE); + } + + @Test + void basic_function_invocation() { + assertThat(parseInterpretedCompileEvaluate("max(1, 2, 3)")).isEqualTo(new BigDecimal(3)); + } + + @Test + void basic_function_definition() { + assertThat(parseInterpretedCompileEvaluate("function (a, b) a + b")).isInstanceOf(CustomFEELFunction.class); + assertThat(parseInterpretedCompileEvaluate("{ s : function (a, b) a + b, x : 1, y : 2, r : s(x,y) }.r")).isEqualTo(new BigDecimal(3)); + } + + @Test + void named_function_invocation() { + assertThat(parseInterpretedCompileEvaluate("substring(start position: 2, string: \"FOOBAR\")")).isEqualTo("OOBAR"); + assertThat(parseInterpretedCompileEvaluate("ceiling( n : 1.5 )")).isEqualTo(new BigDecimal("2")); + } + + @Test + void misc_from_original_feelinterpreted_test_suite() { + assertThat(parseInterpretedCompileEvaluate("if null then \"foo\" else \"bar\"")).isEqualTo("bar"); + assertThat(parseInterpretedCompileEvaluate("{ hello world : function() \"Hello World!\", message : hello world() }.message")).isEqualTo("Hello World!"); + assertThat(parseInterpretedCompileEvaluate("1 + if true then 1 else 2")).isEqualTo(new BigDecimal("2")); + assertThat(parseInterpretedCompileEvaluate("\"string with \\\"quotes\\\"\"")).isEqualTo("string with \"quotes\""); + assertThat(parseInterpretedCompileEvaluate("date( -0105, 8, 2 )")).isEqualTo(LocalDate.of(-105, 8, 2)); + assertThat(parseInterpretedCompileEvaluate("string(null)")).isNull(); + assertThat(parseInterpretedCompileEvaluate("[ null ]")).asList().containsExactly(new Object[]{null}); + assertThat(parseInterpretedCompileEvaluate("[ null, null ]")).asList().containsExactly(null, null); + assertThat(parseInterpretedCompileEvaluate("[ null, 47, null ]")).asList().containsExactly(null, BigDecimal.valueOf(47), null); + } + + @Test + void benchmark_feel_expressions() { + assertThat(parseInterpretedCompileEvaluate("{ full name: { first name: \"John\", last name: \"Doe\" } }.full name.last name")).isEqualTo("Doe"); + assertThat(parseInterpretedCompileEvaluate("some price in [ 80, 11, 110 ] satisfies price > 100")).isEqualTo(Boolean.TRUE); + assertThat(parseInterpretedCompileEvaluate("every price in [ 80, 11, 90 ] satisfies price > 10")).isEqualTo(Boolean.TRUE); + } + + @Test + void context_expression() { + assertThat(parseInterpretedCompileEvaluate("{}")).isEqualTo(Collections.emptyMap()); + assertThat(parseInterpretedCompileEvaluate("{ }")).isEqualTo(Collections.emptyMap()); + assertThat(parseInterpretedCompileEvaluate("{ a : 1 }")).isEqualTo(mapOf(entry("a", new BigDecimal(1)))); + assertThat(parseInterpretedCompileEvaluate("{ \"a\" : 1 }")).isEqualTo(mapOf(entry("a", new BigDecimal(1)))); + assertThat(parseInterpretedCompileEvaluate("{ \" a\" : 1 }")).isEqualTo(mapOf(entry("a", new BigDecimal(1)))); // Demonstrating a bad practice. + assertThat(parseInterpretedCompileEvaluate("{ a : 1, b : 2, c : 3 }")).isEqualTo(mapOf(entry("a", new BigDecimal(1)), entry("b", new BigDecimal(2)), entry("c", new BigDecimal(3)))); + assertThat(parseInterpretedCompileEvaluate("{ a : 1, a name : \"John Doe\" }")).isEqualTo(mapOf(entry("a", new BigDecimal(1)), entry("a name", "John Doe"))); + + assertThat(parseInterpretedCompileEvaluate("{ a : 1, b : a }")).isEqualTo(mapOf(entry("a", new BigDecimal(1)), entry("b", new BigDecimal(1)))); + } + + /** + * See {@link FEELParserTest} + */ + @Test + void contextWithMultipleEntries() { + String inputExpression = "{ \"a string key\" : 10," + "\n" + + " a non-string key : 11," + "\n" + + " a key.with + /' odd chars : 12 }"; + assertThat(parseInterpretedCompileEvaluate(inputExpression)).isEqualTo(mapOf(entry("a string key", new BigDecimal(10)), entry("a non-string key", new BigDecimal(11)), entry("a key.with + /' odd chars", new BigDecimal(12)))); + } + + /** + * See {@link FEELParserTest} + */ + @Test + void nestedContexts() { + String inputExpression = "{ a value : 10," + "\n" + + " an applicant : { " + "\n" + + " first name : \"Edson\", " + "\n" + + " last + name : \"Tirelli\", " + "\n" + + " full name : first name + last + name, " + "\n" + + " address : {" + "\n" + + " street : \"55 broadway st\"," + "\n" + + " city : \"New York\" " + "\n" + + " }, " + "\n" + + " xxx: last + name" + "\n" + + " } " + "\n" + + "}"; + assertThat(parseInterpretedCompileEvaluate(inputExpression)).isEqualTo(mapOf(entry("a value", new BigDecimal(10)), + entry("an applicant", mapOf(entry("first name", "Edson"), + entry("last + name", "Tirelli"), + entry("full name", "EdsonTirelli"), + entry("address", mapOf(entry("street", "55 broadway st"), + entry("city", "New York"))), + entry("xxx", "Tirelli"))))); + } + + /** + * See {@link FEELParserTest} + */ + @Test + void nestedContexts2() { + String complexContext = "{ an applicant : { \n" + + " home address : { \n" + + " street name: \"broadway st\", \n" + + " city : \"New York\" \n" + + " } \n" + + " }, \n" + + " street : an applicant.home address.street name \n" + + "} "; + assertThat(parseInterpretedCompileEvaluate(complexContext)).isEqualTo(mapOf(entry("an applicant", mapOf(entry("home address", mapOf(entry("street name", "broadway st"), + entry("city", "New York"))))), + entry("street", "broadway st"))); + } + + @Test + void nameReference() { + String inputExpression = "someSimpleName"; + CompiledFEELExpression nameRef = parseInterpreted(inputExpression, mapOf(entry("someSimpleName", BuiltInType.STRING) ) ); + LOG.debug("{}", nameRef); + + EvaluationContext context = CodegenTestUtil.newEmptyEvaluationContext(); + context.setValue("someSimpleName", 123L); + Object result = nameRef.apply(context); + LOG.debug("{}", result); + + assertThat(result).isEqualTo(BigDecimal.valueOf(123)); + } + + @Test + void qualifiedName() { + String inputExpression = "My Person.Full Name"; + Type personType = new MapBackedType("Person", mapOf( entry("Full Name", BuiltInType.STRING), entry("Age", BuiltInType.NUMBER) ) ); + CompiledFEELExpression qualRef = parseInterpreted(inputExpression, mapOf(entry("My Person", personType) ) ); + LOG.debug("{}", qualRef); + + EvaluationContext context = CodegenTestUtil.newEmptyEvaluationContext(); + context.setValue("My Person", mapOf( entry("Full Name", "John Doe"), entry("Age", 47) )); + Object result = qualRef.apply(context); + LOG.debug("{}", result); + + assertThat(result).isEqualTo("John Doe" ); + + // check number coercion for qualified name + CompiledFEELExpression personAgeExpression = parseInterpreted("My Person.Age", mapOf(entry("My Person", personType))); + LOG.debug("{}", personAgeExpression); + + Object resultPersonAge = personAgeExpression.apply(context); // Please notice input variable in context is a Map containing and entry value for int 47. + LOG.debug("{}", resultPersonAge); + + assertThat(resultPersonAge).isEqualTo(BigDecimal.valueOf(47)); + } + + public static class MyPerson { + @FEELProperty("Full Name") + public String getFullName() { + return "John Doe"; + } + } + + @Test + void qualifiedName2() { + String inputExpression = "My Person.Full Name"; + Type personType = JavaBackedType.of(MyPerson.class); + CompiledFEELExpression qualRef = parseInterpreted(inputExpression, mapOf(entry("My Person", personType) ) ); + LOG.debug("{}", qualRef); + + EvaluationContext context = CodegenTestUtil.newEmptyEvaluationContext(); + context.setValue("My Person", new MyPerson()); + Object result = qualRef.apply(context); + LOG.debug("{}", result); + + assertThat(result).isEqualTo("John Doe" ); + } + + @Test + void qualifiedName3() { + String inputExpression = "a date.year"; + Type dateType = BuiltInType.DATE; + CompiledFEELExpression qualRef = parseInterpreted(inputExpression, mapOf(entry("a date", dateType))); + LOG.debug("{}", qualRef); + + EvaluationContext context = CodegenTestUtil.newEmptyEvaluationContext(); + context.setValue("a date", LocalDate.of(2016, 8, 2)); + Object result = qualRef.apply(context); + LOG.debug("{}", result); + + assertThat(result).isEqualTo(BigDecimal.valueOf(2016)); + } + + + +} diff --git a/kie-dmn/kie-dmn-feel/src/test/java/org/kie/dmn/feel/runtime/functions/ListReplaceFunctionTest.java b/kie-dmn/kie-dmn-feel/src/test/java/org/kie/dmn/feel/runtime/functions/ListReplaceFunctionTest.java index 72dfd91742c..83091ae3065 100644 --- a/kie-dmn/kie-dmn-feel/src/test/java/org/kie/dmn/feel/runtime/functions/ListReplaceFunctionTest.java +++ b/kie-dmn/kie-dmn-feel/src/test/java/org/kie/dmn/feel/runtime/functions/ListReplaceFunctionTest.java @@ -30,7 +30,7 @@ import org.kie.dmn.feel.runtime.events.InvalidParametersEvent; import static org.assertj.core.api.Assertions.assertThat; -import static org.kie.dmn.feel.util.CompilerUtils.parseCompileEvaluate; +import static org.kie.dmn.feel.util.CompilerUtils.parseCodegenCompileEvaluate; class ListReplaceFunctionTest { @@ -101,7 +101,7 @@ void invokeMatchNull() { void invokeMatchInvalid() { List list = Arrays.asList(2, 4, 7, 8); String validMatchFunction = "function(item, newItem) item + newItem"; - Object expressionObject = parseCompileEvaluate(validMatchFunction); + Object expressionObject = parseCodegenCompileEvaluate(validMatchFunction); assertThat(expressionObject).isInstanceOf(AbstractCustomFEELFunction.class); FunctionTestUtil.assertResultError(listReplaceFunction.invoke(list, (AbstractCustomFEELFunction)expressionObject, 3), InvalidParametersEvent.class); } @@ -112,7 +112,7 @@ void invokeReplaceByMatchWithNull() { List expected = new ArrayList<>(list); expected.set(1, null); String validMatchFunction = "function(item, newItem) item = \"Element-1\""; - Object expressionObject = parseCompileEvaluate(validMatchFunction); + Object expressionObject = parseCodegenCompileEvaluate(validMatchFunction); assertThat(expressionObject).isInstanceOf(AbstractCustomFEELFunction.class); FunctionTestUtil.assertResult(listReplaceFunction.invoke(list, (AbstractCustomFEELFunction)expressionObject, null), expected); } @@ -120,7 +120,7 @@ void invokeReplaceByMatchWithNull() { @Test void invokeReplaceByMatchWithNotNull() { String validMatchFunction = "function(item, newItem) item < newItem"; - Object expressionObject = parseCompileEvaluate(validMatchFunction); + Object expressionObject = parseCodegenCompileEvaluate(validMatchFunction); assertThat(expressionObject).isInstanceOf(AbstractCustomFEELFunction.class); List list = Arrays.asList(BigDecimal.valueOf(2), BigDecimal.valueOf(4), BigDecimal.valueOf(7), BigDecimal.valueOf(8)); List expected = new ArrayList<>(list); diff --git a/kie-dmn/kie-dmn-feel/src/test/java/org/kie/dmn/feel/runtime/functions/MatchesFunctionTest.java b/kie-dmn/kie-dmn-feel/src/test/java/org/kie/dmn/feel/runtime/functions/MatchesFunctionTest.java index eeb86f511a9..eea47b17de6 100644 --- a/kie-dmn/kie-dmn-feel/src/test/java/org/kie/dmn/feel/runtime/functions/MatchesFunctionTest.java +++ b/kie-dmn/kie-dmn-feel/src/test/java/org/kie/dmn/feel/runtime/functions/MatchesFunctionTest.java @@ -19,54 +19,116 @@ package org.kie.dmn.feel.runtime.functions; import org.junit.jupiter.api.Test; -import org.kie.dmn.feel.runtime.events.InvalidParametersEvent; +import org.mockito.MockedStatic; -class MatchesFunctionTest { +import java.security.InvalidParameterException; +import java.util.regex.PatternSyntaxException; + +import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.*; - private final static MatchesFunction matchesFunction = MatchesFunction.INSTANCE; +class MatchesFunctionTest { @Test void invokeNull() { - FunctionTestUtil.assertResultError(matchesFunction.invoke((String) null, null), InvalidParametersEvent.class); - FunctionTestUtil.assertResultError(matchesFunction.invoke(null, "test"), InvalidParametersEvent.class); - FunctionTestUtil.assertResultError(matchesFunction.invoke("test", null), InvalidParametersEvent.class); + assertThrows(InvalidParameterException.class, () -> MatchesFunction.matchFunctionWithFlags(null, null, null)); + assertThrows(InvalidParameterException.class, () -> MatchesFunction.matchFunctionWithFlags(null, "test",null)); + assertThrows(InvalidParameterException.class, () -> MatchesFunction.matchFunctionWithFlags("test", null,null)); } @Test void invokeUnsupportedFlags() { - FunctionTestUtil.assertResult(matchesFunction.invoke("foobar", "fo.bar", "g"), true); + assertThrows(IllegalArgumentException.class, () -> MatchesFunction.matchFunctionWithFlags("foobar", "fo.bar", "g")); + assertThrows(IllegalArgumentException.class, () -> MatchesFunction.matchFunctionWithFlags("abracadabra", "bra", "p")); + assertThrows(IllegalArgumentException.class, () -> MatchesFunction.matchFunctionWithFlags("abracadabra", "bra", "X")); + assertThrows(IllegalArgumentException.class, () -> MatchesFunction.matchFunctionWithFlags("abracadabra", "bra", " ")); + assertThrows(IllegalArgumentException.class, () -> MatchesFunction.matchFunctionWithFlags("abracadabra", "bra", "iU")); } @Test void invokeWithoutFlagsMatch() { - FunctionTestUtil.assertResult(matchesFunction.invoke("test", "test"), true); - FunctionTestUtil.assertResult(matchesFunction.invoke("foobar", "^fo*b"), true); + FunctionTestUtil.assertResult(MatchesFunction.matchFunctionWithFlags("test", "test",null), true); + FunctionTestUtil.assertResult(MatchesFunction.matchFunctionWithFlags("foobar", "^fo*b",null), true); + FunctionTestUtil.assertResult(MatchesFunction.matchFunctionWithFlags("abracadabra", "bra", ""), true); + FunctionTestUtil.assertResult(MatchesFunction.matchFunctionWithFlags("abracadabra", "bra",null), true); + FunctionTestUtil.assertResult(MatchesFunction.matchFunctionWithFlags("(?xi)[hello world()]", "hello",null), true); } @Test void invokeWithoutFlagsNotMatch() { - FunctionTestUtil.assertResult(matchesFunction.invoke("test", "testt"), false); - FunctionTestUtil.assertResult(matchesFunction.invoke("foobar", "^fo*bb"), false); - FunctionTestUtil.assertResult(matchesFunction.invoke("fo\nbar", "fo.bar"), false); + FunctionTestUtil.assertResult(MatchesFunction.matchFunctionWithFlags("test", "testt",null), false); + FunctionTestUtil.assertResult(MatchesFunction.matchFunctionWithFlags("foobar", "^fo*bb",null), false); + FunctionTestUtil.assertResult(MatchesFunction.matchFunctionWithFlags("fo\nbar", "fo.bar",null), false); + FunctionTestUtil.assertResult(MatchesFunction.matchFunctionWithFlags("h", "(.)\3",null), false); + FunctionTestUtil.assertResult(MatchesFunction.matchFunctionWithFlags("h", "(.)\2",null), false); + FunctionTestUtil.assertResult(MatchesFunction.matchFunctionWithFlags("input", "\3",null), false); + FunctionTestUtil.assertResult(MatchesFunction.matchFunctionWithFlags("fo\nbar", "(?iU)(?iU)(ab)[|cd]",null), false); + FunctionTestUtil.assertResult(MatchesFunction.matchFunctionWithFlags("fo\nbar", "(?x)(?i)hello world","i"), false); + FunctionTestUtil.assertResult(MatchesFunction.matchFunctionWithFlags("fo\nbar", "(?xi)hello world",null), false); } @Test void invokeWithFlagDotAll() { - FunctionTestUtil.assertResult(matchesFunction.invoke("fo\nbar", "fo.bar", "s"), true); + FunctionTestUtil.assertResult(MatchesFunction.matchFunctionWithFlags("fo\nbar", "fo.bar", "s"), true); } @Test void invokeWithFlagMultiline() { - FunctionTestUtil.assertResult(matchesFunction.invoke("fo\nbar", "^bar", "m"), true); + FunctionTestUtil.assertResult(MatchesFunction.matchFunctionWithFlags("fo\nbar", "^bar", "m"), true); } @Test void invokeWithFlagCaseInsensitive() { - FunctionTestUtil.assertResult(matchesFunction.invoke("foobar", "^Fo*bar", "i"), true); + FunctionTestUtil.assertResult(MatchesFunction.matchFunctionWithFlags("foobar", "^Fo*bar", "i"), true); + FunctionTestUtil.assertResult(MatchesFunction.matchFunctionWithFlags("foobar", "^Fo*bar", "i"), true); + FunctionTestUtil.assertResult(MatchesFunction.matchFunctionWithFlags("\u212A", "k","i"), true); + FunctionTestUtil.assertResult(MatchesFunction.matchFunctionWithFlags("\u212A", "K","i"), true); + } + + @Test + void invokeWithFlagComments() { + FunctionTestUtil.assertResult(MatchesFunction.matchFunctionWithFlags("hello world", "hello"+"\"+ sworld", "x"), false); } @Test void invokeWithAllFlags() { - FunctionTestUtil.assertResult(matchesFunction.invoke("fo\nbar", "Fo.^bar", "smi"), true); + FunctionTestUtil.assertResult(MatchesFunction.matchFunctionWithFlags("fo\nbar", "Fo.^bar", "smi"), true); + } + + @Test + void checkForPatternTest() { + assertThrows(PatternSyntaxException.class, () -> MatchesFunction.matchFunctionWithFlags("foobar", "(abc|def(ghi", "i")); + } + + @Test + void checkFlagsTest() { + assertDoesNotThrow(() -> MatchesFunction.checkFlags("s")); + assertDoesNotThrow(() -> MatchesFunction.checkFlags("i")); + assertDoesNotThrow(() -> MatchesFunction.checkFlags("sx")); + assertDoesNotThrow(() -> MatchesFunction.checkFlags("six")); + assertDoesNotThrow(() -> MatchesFunction.checkFlags("sixm")); + assertThrows(IllegalArgumentException.class, () -> MatchesFunction.checkFlags("a")); + assertThrows(IllegalArgumentException.class, () -> MatchesFunction.checkFlags("sa")); + assertThrows(IllegalArgumentException.class, () -> MatchesFunction.checkFlags("siU@")); + assertThrows(IllegalArgumentException.class, () -> MatchesFunction.checkFlags("siUxU")); + assertThrows(IllegalArgumentException.class, () -> MatchesFunction.checkFlags("ss")); + assertThrows(IllegalArgumentException.class, () -> MatchesFunction.checkFlags("siiU")); + assertThrows(IllegalArgumentException.class, () -> MatchesFunction.checkFlags("si U")); + assertThrows(IllegalArgumentException.class, () -> MatchesFunction.checkFlags("U")); } + + @Test + void checkMatchFunctionWithFlagsInvocation() { + MatchesFunction matchesFunctionSpied = spy(MatchesFunction.INSTANCE); + matchesFunctionSpied.invoke("input", "pattern"); + verify(matchesFunctionSpied, times(1)).invoke("input", "pattern", null); + try (MockedStatic matchesFunctionMocked = mockStatic(MatchesFunction.class)) { + matchesFunctionSpied.invoke("input", "pattern"); + matchesFunctionMocked.verify(() -> MatchesFunction.matchFunctionWithFlags("input", "pattern", null)); + matchesFunctionSpied.invoke("input", "pattern", "flags"); + matchesFunctionMocked.verify(() -> MatchesFunction.matchFunctionWithFlags("input", "pattern", "flags")); + } + } + } \ No newline at end of file diff --git a/kie-dmn/kie-dmn-feel/src/test/java/org/kie/dmn/feel/util/CompilerUtils.java b/kie-dmn/kie-dmn-feel/src/test/java/org/kie/dmn/feel/util/CompilerUtils.java index b56bd81798c..c201ec39ca3 100644 --- a/kie-dmn/kie-dmn-feel/src/test/java/org/kie/dmn/feel/util/CompilerUtils.java +++ b/kie-dmn/kie-dmn-feel/src/test/java/org/kie/dmn/feel/util/CompilerUtils.java @@ -31,6 +31,8 @@ import org.kie.dmn.feel.lang.EvaluationContext; import org.kie.dmn.feel.lang.Type; import org.kie.dmn.feel.lang.ast.BaseNode; +import org.kie.dmn.feel.lang.impl.CompiledExpressionImpl; +import org.kie.dmn.feel.lang.impl.InterpretedExecutableExpression; import org.kie.dmn.feel.parser.feel11.ASTBuilderVisitor; import org.kie.dmn.feel.parser.feel11.FEELParser; import org.kie.dmn.feel.parser.feel11.FEEL_1_1Parser; @@ -43,21 +45,17 @@ public class CompilerUtils { private static final String TEMPLATE_RESOURCE = "/TemplateCompiledFEELExpression.java"; private static final String TEMPLATE_CLASS = "TemplateCompiledFEELExpression"; - public static Object parseCompileEvaluate(String feelLiteralExpression) { - CompiledFEELExpression compiledExpression = parse( feelLiteralExpression ); - LOG.debug("{}", compiledExpression); - - EvaluationContext emptyContext = CodegenTestUtil.newEmptyEvaluationContext(); - Object result = compiledExpression.apply(emptyContext); - LOG.debug("{}", result); - return result; + public static Object parseCodegenCompileEvaluate(String feelLiteralExpression) { + LOG.debug("{}", feelLiteralExpression); + CompiledFEELExpression compiledExpression = parseCodegen(feelLiteralExpression ); + return evaluate(compiledExpression); } - public static CompiledFEELExpression parse(String input) { - return parse(input, Collections.emptyMap() ); + public static CompiledFEELExpression parseCodegen(String input) { + return parseCodegen(input, Collections.emptyMap() ); } - public static CompiledFEELExpression parse(String input, Map inputTypes) { + public static CompiledFEELExpression parseCodegen(String input, Map inputTypes) { FEEL_1_1Parser parser = FEELParser.parse(null, input, inputTypes, Collections.emptyMap(), Collections.emptyList(), Collections.emptyList(), null); ParseTree tree = parser.compilation_unit(); @@ -80,4 +78,32 @@ public static CompiledFEELExpression parse(String input, Map input return compilerBytecodeLoader.compileUnit(packageName, TEMPLATE_CLASS, cu); } + public static Object parseInterpretedCompileEvaluate(String feelLiteralExpression) { + LOG.debug("{}", feelLiteralExpression); + CompiledFEELExpression compiledExpression = parseInterpreted(feelLiteralExpression ); + return evaluate(compiledExpression); + } + + public static CompiledFEELExpression parseInterpreted(String input) { + return parseInterpreted(input, Collections.emptyMap() ); + } + + public static CompiledFEELExpression parseInterpreted(String input, Map inputTypes) { + FEEL_1_1Parser parser = FEELParser.parse(null, input, inputTypes, Collections.emptyMap(), Collections.emptyList(), Collections.emptyList(), null); + + ParseTree tree = parser.compilation_unit(); + + ASTBuilderVisitor v = new ASTBuilderVisitor(inputTypes, null); + BaseNode ast = tree.accept(v); + return new InterpretedExecutableExpression(new CompiledExpressionImpl(ast)); + } + + public static Object evaluate(CompiledFEELExpression compiledExpression) { + LOG.debug("{}", compiledExpression); + EvaluationContext emptyContext = CodegenTestUtil.newEmptyEvaluationContext(); + Object result = compiledExpression.apply(emptyContext); + LOG.debug("{}", result); + return result; + } + } \ No newline at end of file diff --git a/kie-dmn/kie-dmn-test-resources/src/test/resources/valid_models/DMNv1_5/LocalHrefs.dmn b/kie-dmn/kie-dmn-test-resources/src/test/resources/valid_models/DMNv1_5/LocalHrefs.dmn new file mode 100644 index 00000000000..33542ce25c3 --- /dev/null +++ b/kie-dmn/kie-dmn-test-resources/src/test/resources/valid_models/DMNv1_5/LocalHrefs.dmn @@ -0,0 +1,46 @@ + + + + Local (non-imported) qualified hrefs + + + + + + + + + "decision_001" + + + + + + + + "bkm_001" + + + + + + + + + + + + + + + + + + decision_001 + " " + input_001 + " " + bkm_001() + + + + + diff --git a/kie-dmn/kie-dmn-validation/src/main/resources/org/kie/dmn/validation/DMNv1x/dmn-validation-rules-dmnelementref.drl b/kie-dmn/kie-dmn-validation/src/main/resources/org/kie/dmn/validation/DMNv1x/dmn-validation-rules-dmnelementref.drl index 5bedd9639bd..07e619bf137 100644 --- a/kie-dmn/kie-dmn-validation/src/main/resources/org/kie/dmn/validation/DMNv1x/dmn-validation-rules-dmnelementref.drl +++ b/kie-dmn/kie-dmn-validation/src/main/resources/org/kie/dmn/validation/DMNv1x/dmn-validation-rules-dmnelementref.drl @@ -20,8 +20,11 @@ package org.kie.dmn.validation.DMNv1x; import org.kie.dmn.model.api.*; +import org.kie.dmn.model.api.DMNElement; +import org.kie.dmn.model.api.DMNElementReference; import org.kie.dmn.api.core.DMNMessage; import org.kie.dmn.core.impl.DMNMessageImpl; +import org.kie.dmn.core.compiler.DMNCompilerImpl; import org.kie.dmn.core.util.MsgUtil; import org.kie.dmn.feel.lang.types.BuiltInType; import org.kie.dmn.feel.parser.feel11.FEELParser; @@ -45,7 +48,7 @@ end rule ELEMREF_NOHASH_p2 when not( Import() ) - $oc : DMNElementReference(href contains ":", !href.substring(href.indexOf(":") + 1).startsWith('#')) + $oc : DMNElementReference(href contains ":", !(href contains "#")) then reporter.report( DMNMessage.Severity.ERROR, $oc , Msg.ELEMREF_NOHASH, $oc.getParentDRDElement().getIdentifierString() ); end @@ -60,7 +63,7 @@ end rule ELEMREF_MISSING_p2 when - $elemRef: DMNElementReference(!href.startsWith("#"), $href: href, href.contains("#"), $targetId : rightOfHash(href), $targetNS : leftOfHash(href)) + $elemRef: DMNElementReference(!href.startsWith("#"), $href: href, href.contains("#"), $targetId : rightOfHash(href), $targetNS : leftOfHash(href), $rootElement: DMNCompilerImpl.getRootElement(this), !$rootElement.namespace.equals($targetNS)) (not (and Import( $importedNS : namespace ) $importDef : Definitions( namespace == $importedNS, namespace == $targetNS ) from entry-point "DMNImports" DMNElement( id == $targetId ) from $importDef.drgElement diff --git a/kie-dmn/kie-dmn-validation/src/test/java/org/kie/dmn/validation/v1_5/DMN15ValidationsTest.java b/kie-dmn/kie-dmn-validation/src/test/java/org/kie/dmn/validation/v1_5/DMN15ValidationsTest.java index 7eb5e6774df..e8a27da03fd 100644 --- a/kie-dmn/kie-dmn-validation/src/test/java/org/kie/dmn/validation/v1_5/DMN15ValidationsTest.java +++ b/kie-dmn/kie-dmn-validation/src/test/java/org/kie/dmn/validation/v1_5/DMN15ValidationsTest.java @@ -132,6 +132,12 @@ void typeConstraintsChecksValidation() { evaluate(modelNamespace, modelName, modelFileName, inputData); } + @Test + void localHrefsValidation() { + String modelFileName = "valid_models/DMNv1_5/LocalHrefs.dmn"; + validate(modelFileName); + } + private void commonUnnamedImportValidation(String importingModelRef, String importedModelRef) { String modelName = "Importing empty-named Model"; String modelNamespace = "http://www.trisotech.com/dmn/definitions/_f79aa7a4-f9a3-410a-ac95-bea496edabgc";