diff --git a/kie-dmn-core/src/main/java/org/kie/dmn/core/ast/DMNDTExpressionEvaluator.java b/kie-dmn-core/src/main/java/org/kie/dmn/core/ast/DMNDTExpressionEvaluator.java index f1967c256df..65996e5a10d 100644 --- a/kie-dmn-core/src/main/java/org/kie/dmn/core/ast/DMNDTExpressionEvaluator.java +++ b/kie-dmn-core/src/main/java/org/kie/dmn/core/ast/DMNDTExpressionEvaluator.java @@ -80,6 +80,8 @@ private EventResults processEvents(List events, InternalDMNRuntimeEve } else if ( e.getSeverity() == FEELEvent.Severity.ERROR ) { result.addMessage( DMNMessage.Severity.ERROR, e.getMessage(), node.getId(), e ); r.hasErrors = true; + } else if ( e.getSeverity() == FEELEvent.Severity.WARN ) { + result.addMessage( DMNMessage.Severity.WARN, e.getMessage(), node.getId(), e ); } } events.clear(); diff --git a/kie-dmn-core/src/test/java/org/kie/dmn/core/DMNDecisionTableRuntimeTest.java b/kie-dmn-core/src/test/java/org/kie/dmn/core/DMNDecisionTableRuntimeTest.java index e232c2d803d..4f820d3a353 100644 --- a/kie-dmn-core/src/test/java/org/kie/dmn/core/DMNDecisionTableRuntimeTest.java +++ b/kie-dmn-core/src/test/java/org/kie/dmn/core/DMNDecisionTableRuntimeTest.java @@ -33,12 +33,17 @@ import org.junit.Test; import org.kie.dmn.core.api.DMNContext; import org.kie.dmn.core.api.DMNFactory; +import org.kie.dmn.core.api.DMNMessage; import org.kie.dmn.core.api.DMNModel; import org.kie.dmn.core.api.DMNResult; import org.kie.dmn.core.api.DMNRuntime; import org.kie.dmn.core.api.event.AfterEvaluateDecisionTableEvent; +import org.kie.dmn.core.api.event.DMNEvent; import org.kie.dmn.core.api.event.DMNRuntimeEventListener; import org.kie.dmn.core.util.DMNRuntimeUtil; +import org.kie.dmn.feel.runtime.events.FEELEvent; +import org.kie.dmn.feel.runtime.events.HitPolicyViolationEvent; +import org.kie.dmn.feel.runtime.events.FEELEvent.Severity; import org.mockito.ArgumentCaptor; import org.mockito.Mockito; @@ -80,6 +85,28 @@ public void testSimpleDecisionTableUniqueHitPolicySatisfies() { assertThat( result.get( "Approval Status" ), nullValue() ); assertTrue( dmnResult.getMessages().size() > 0 ); } + + @Test + public void testSimpleDecisionTableUniqueHitPolicyNullWarn() { + DMNRuntime runtime = DMNRuntimeUtil.createRuntime( "0004-simpletable-U-noinputvalues.dmn", this.getClass() ); + DMNModel dmnModel = runtime.getModel( "https://github.com/droolsjbpm/kie-dmn", "0004-simpletable-U-noinputvalues" ); + assertThat( dmnModel, notNullValue() ); + + DMNContext context = DMNFactory.newContext(); + context.set( "Age", new BigDecimal( 18 ) ); + context.set( "RiskCategory", "ASD" ); + context.set( "isAffordable", false ); + + DMNResult dmnResult = runtime.evaluateAll( dmnModel, context ); + + DMNContext result = dmnResult.getContext(); + + assertThat( result.get( "Approval Status" ), nullValue() ); + assertTrue( dmnResult.getMessages().size() > 0 ); + assertTrue( dmnResult.getMessages().stream().anyMatch( dm -> dm.getSeverity().equals(DMNMessage.Severity.WARN) + && dm.getFeelEvent() instanceof HitPolicyViolationEvent + && dm.getFeelEvent().getSeverity().equals(FEELEvent.Severity.WARN) ) ); + } @Test public void testDecisionTableWithCalculatedResult() { diff --git a/kie-dmn-core/src/test/resources/org/kie/dmn/core/0004-simpletable-U-noinputvalues.dmn b/kie-dmn-core/src/test/resources/org/kie/dmn/core/0004-simpletable-U-noinputvalues.dmn new file mode 100644 index 00000000000..9921a93d4eb --- /dev/null +++ b/kie-dmn-core/src/test/resources/org/kie/dmn/core/0004-simpletable-U-noinputvalues.dmn @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + + + Age + + + + + RiskCategory + + + + + isAffordable + + + + + "Approved", "Declined" + + + + + >=18 + + + "Medium","Low" + + + true + + + "Approved" + + + + + <18 + + + "Medium","Low" + + + true + + + "Declined" + + + + + - + + + "High" + + + true + + + "Declined" + + + + + - + + + "High" + + + false + + + "Declined" + + + + + + + + + + + + + + \ No newline at end of file diff --git a/kie-dmn-feel/src/main/java/org/kie/dmn/feel/runtime/decisiontables/DecisionTableImpl.java b/kie-dmn-feel/src/main/java/org/kie/dmn/feel/runtime/decisiontables/DecisionTableImpl.java index d0f93f83781..32af599cf85 100644 --- a/kie-dmn-feel/src/main/java/org/kie/dmn/feel/runtime/decisiontables/DecisionTableImpl.java +++ b/kie-dmn-feel/src/main/java/org/kie/dmn/feel/runtime/decisiontables/DecisionTableImpl.java @@ -22,8 +22,12 @@ import org.kie.dmn.feel.runtime.UnaryTest; import org.kie.dmn.feel.runtime.events.DecisionTableRulesMatchedEvent; import org.kie.dmn.feel.runtime.events.FEELEvent; +import org.kie.dmn.feel.runtime.events.FEELEventBase; import org.kie.dmn.feel.runtime.events.HitPolicyViolationEvent; import org.kie.dmn.feel.runtime.events.InvalidInputEvent; +import org.kie.dmn.feel.runtime.events.FEELEvent.Severity; +import org.kie.dmn.feel.runtime.functions.FEELFnResult; +import org.kie.dmn.feel.util.Either; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -31,6 +35,7 @@ import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.function.Function; import java.util.stream.Collectors; import java.util.stream.IntStream; @@ -70,42 +75,37 @@ public DecisionTableImpl(String name, * decision table that are expressions derived from these parameters * @return */ - public Object evaluate(EvaluationContext ctx, Object[] params) { + public FEELFnResult evaluate(EvaluationContext ctx, Object[] params) { if ( decisionRules.isEmpty() ) { - return null; + return FEELFnResult.ofError(new FEELEventBase(Severity.WARN, "Decision table is empty", null)); } + + FEEL feel = FEEL.newInstance(); + Object[] actualInputs = resolveActualInputs( ctx, feel ); - try { - FEEL feel = FEEL.newInstance(); - Object[] actualInputs = resolveActualInputs( ctx, feel ); - - if ( ! actualInputsMatchInputValues( ctx, actualInputs ) ) { - return null; - } + Either actualInputMatch = actualInputsMatchInputValues( ctx, actualInputs ); + if ( actualInputMatch.isLeft() ) { + return actualInputMatch.cata( e -> FEELFnResult.ofError(e), e -> FEELFnResult.ofError(null) ); + } - List matches = findMatches( ctx, actualInputs ); - if( !matches.isEmpty() ) { - List results = evaluateResults( ctx, feel, actualInputs, matches ); - Object result = hitPolicy.getDti().dti( ctx, this, actualInputs, matches, results ); + List matches = findMatches( ctx, actualInputs ); + if( !matches.isEmpty() ) { + List results = evaluateResults( ctx, feel, actualInputs, matches ); + Object result = hitPolicy.getDti().dti( ctx, this, actualInputs, matches, results ); - return result; + return FEELFnResult.ofResult( result ); + } else { + // check if there is a default value set for the outputs + if( hasDefaultValues ) { + Object result = defaultToOutput( ctx, feel ); + return FEELFnResult.ofResult( result ); } else { - // check if there is a default value set for the outputs - if( hasDefaultValues ) { - Object result = defaultToOutput( ctx, feel ); - return result; - } else { - FEELEventListenersManager.notifyListeners(ctx.getEventsManager(), () -> new HitPolicyViolationEvent( - FEELEvent.Severity.WARN, - "No rule matched for decision table '" + name + "' and no default values were defined. Setting result to null.", - name, - Collections.EMPTY_LIST ) ); - } - return null; + return FEELFnResult.ofError( new HitPolicyViolationEvent( + Severity.WARN, + "No rule matched for decision table '" + name + "' and no default values were defined. Setting result to null.", + name, + Collections.EMPTY_LIST ) ); } - } catch ( Throwable e ) { - // no longer needed as DTInvokerFunction support Either for wrapping the exception : logger.error( "Error invoking decision table '" + getName() + "'.", e ); - throw e; } } @@ -124,7 +124,7 @@ private Object[] resolveActualInputs(EvaluationContext ctx, FEEL feel) { * @param params * @return */ - private boolean actualInputsMatchInputValues(EvaluationContext ctx, Object[] params) { + private Either actualInputsMatchInputValues(EvaluationContext ctx, Object[] params) { // check that all the parameters match the input list values if they are defined for( int i = 0; i < params.length; i++ ) { final DTInputClause input = inputs.get( i ); @@ -134,20 +134,17 @@ private boolean actualInputsMatchInputValues(EvaluationContext ctx, Object[] par boolean satisfies = input.getInputValues().stream().map( ut -> ut.apply( ctx, parameter ) ).filter( Boolean::booleanValue ).findAny().orElse( false ); if ( !satisfies ) { - FEELEventListenersManager.notifyListeners( ctx.getEventsManager(), () -> { - String values = input.getInputValuesText(); - return new InvalidInputEvent( FEELEvent.Severity.ERROR, - input.getInputExpression()+"='" + parameter + "' does not match any of the valid values " + values + " for decision table '" + getName() + "'.", - getName(), - null, - values ); - } - ); - return false; + String values = input.getInputValuesText(); + return Either.ofLeft(new InvalidInputEvent( FEELEvent.Severity.ERROR, + input.getInputExpression()+"='" + parameter + "' does not match any of the valid values " + values + " for decision table '" + getName() + "'.", + getName(), + null, + values ) + ); } } } - return true; + return Either.ofRight(true); } /** diff --git a/kie-dmn-feel/src/main/java/org/kie/dmn/feel/runtime/functions/DTInvokerFunction.java b/kie-dmn-feel/src/main/java/org/kie/dmn/feel/runtime/functions/DTInvokerFunction.java index 3703507277f..15f70b8a4b5 100644 --- a/kie-dmn-feel/src/main/java/org/kie/dmn/feel/runtime/functions/DTInvokerFunction.java +++ b/kie-dmn-feel/src/main/java/org/kie/dmn/feel/runtime/functions/DTInvokerFunction.java @@ -46,8 +46,7 @@ public FEELFnResult invoke(EvaluationContext ctx, Object[] params) { for( int i = 0; i < params.length; i++ ) { ctx.setValue( dt.getParameterNames().get( i ), params[i] ); } - Object result = dt.evaluate( ctx, params ); - return FEELFnResult.ofResult( result ); + return dt.evaluate( ctx, params ); } catch ( Exception e ) { capturedException = new FEELEventBase(Severity.ERROR, "Error invoking decision table '"+getName()+"'.", e); } finally { diff --git a/kie-dmn-feel/src/test/java/org/kie/dmn/feel/lang/examples/SimpleDecisionTablesTest.java b/kie-dmn-feel/src/test/java/org/kie/dmn/feel/lang/examples/SimpleDecisionTablesTest.java index 41e5dd384c1..d49084610ea 100644 --- a/kie-dmn-feel/src/test/java/org/kie/dmn/feel/lang/examples/SimpleDecisionTablesTest.java +++ b/kie-dmn-feel/src/test/java/org/kie/dmn/feel/lang/examples/SimpleDecisionTablesTest.java @@ -10,10 +10,14 @@ import org.junit.Test; import org.kie.dmn.feel.FEEL; import org.kie.dmn.feel.runtime.events.FEELEvent; +import org.kie.dmn.feel.runtime.events.FEELEvent.Severity; +import org.kie.dmn.feel.runtime.events.FEELEventListener; +import org.kie.dmn.feel.runtime.events.HitPolicyViolationEvent; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.math.BigDecimal; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.List; @@ -44,12 +48,18 @@ public static void setupTest() { } logger.error(" [stacktraces omitted.]"); } - } + } else if (evt.getSeverity() == FEELEvent.Severity.WARN) { + logger.warn("{}", evt); + } } ); } @Test public void testMain() { + List events = new ArrayList<>(); + FEELEventListener listener = evt -> events.add(evt); + feel.addListener(listener); + String expression = loadExpression( "simple_decision_tables.feel" ); Map context = (Map) feel.evaluate( expression ); @@ -63,6 +73,11 @@ public void testMain() { assertThat( (Map) context.get( "result4" ), hasSize(2)); assertThat( (Map) context.get( "result4" ), hasEntry("Out1", "io1a" )); assertThat( (Map) context.get( "result4" ), hasEntry("Out2", "io2a" )); + assertThat( context.get( "result5" ), nullValue() ); + assertTrue( events.stream().anyMatch(e -> e instanceof HitPolicyViolationEvent + && e.getSeverity().equals(Severity.WARN) ) ); + + feel.removeListener(listener); } @Test diff --git a/kie-dmn-feel/src/test/resources/org/kie/dmn/feel/lang/examples/simple_decision_tables.feel b/kie-dmn-feel/src/test/resources/org/kie/dmn/feel/lang/examples/simple_decision_tables.feel index b426a6a9e69..ddb0f0fa786 100644 --- a/kie-dmn-feel/src/test/resources/org/kie/dmn/feel/lang/examples/simple_decision_tables.feel +++ b/kie-dmn-feel/src/test/resources/org/kie/dmn/feel/lang/examples/simple_decision_tables.feel @@ -41,9 +41,20 @@ ], hit policy: "U" ), + + checkNull : decision table ( + outputs: "Adult", + input expression list: ["Age"], + rule list: [ + [ <18, "\"Young\""], + [ [>18, <99], "\"Adult\""] + ], + hit policy: "U" + ), result1: myDecisionTable( 34 ), result2: biggerDecisionTable( 35, "good" ), result3: multipleOutputs(2), - result4: multipleInOut(In2: 47, In1:2) + result4: multipleInOut(In2: 47, In1:2), + result5: checkNull(9999) } \ No newline at end of file