/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates.
*
* Licensed 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.decisiontables;
import org.kie.dmn.api.feel.runtime.events.FEELEvent;
import org.kie.dmn.api.feel.runtime.events.FEELEvent.Severity;
import org.kie.dmn.feel.FEEL;
import org.kie.dmn.feel.lang.CompiledExpression;
import org.kie.dmn.feel.lang.EvaluationContext;
import org.kie.dmn.feel.lang.impl.FEELEventListenersManager;
import org.kie.dmn.feel.runtime.UnaryTest;
import org.kie.dmn.feel.runtime.events.DecisionTableRulesMatchedEvent;
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.functions.FEELFnResult;
import org.kie.dmn.feel.util.Either;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
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;
import static java.util.stream.Collectors.toList;
import static java.util.stream.Collectors.toMap;
public class DecisionTableImpl {
private static final Logger logger = LoggerFactory.getLogger( DecisionTableImpl.class );
private String name;
private List<String> parameterNames;
private List<DTInputClause> inputs;
private List<DTOutputClause> outputs;
private List<DTDecisionRule> decisionRules;
private HitPolicy hitPolicy;
private boolean hasDefaultValues;
public DecisionTableImpl(String name,
List<String> parameterNames,
List<DTInputClause> inputs,
List<DTOutputClause> outputs,
List<DTDecisionRule> decisionRules,
HitPolicy hitPolicy) {
this.name = name;
this.parameterNames = parameterNames;
this.inputs = inputs;
this.outputs = outputs;
this.decisionRules = decisionRules;
this.hitPolicy = hitPolicy;
this.hasDefaultValues = outputs.stream().allMatch( o -> o.getDefaultValue() != null );
}
/**
* Evaluates this decision table returning the result
* @param ctx
* @param params these are the required information items, not to confuse with the columns of the
* decision table that are expressions derived from these parameters
* @return
*/
public FEELFnResult<Object> evaluate(EvaluationContext ctx, Object[] params) {
if ( decisionRules.isEmpty() ) {
return FEELFnResult.ofError(new FEELEventBase(Severity.WARN, "Decision table is empty", null));
}
FEEL feel = FEEL.newInstance();
Object[] actualInputs = resolveActualInputs( ctx, feel );
Either<FEELEvent, Object> actualInputMatch = actualInputsMatchInputValues( ctx, actualInputs );
if ( actualInputMatch.isLeft() ) {
return actualInputMatch.cata( e -> FEELFnResult.ofError(e), e -> FEELFnResult.ofError(null) );
}
List<DTDecisionRule> matches = findMatches( ctx, actualInputs );
if( !matches.isEmpty() ) {
List<Object> results = evaluateResults( ctx, feel, actualInputs, matches );
Object result = hitPolicy.getDti().dti( ctx, this, actualInputs, matches, results );
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 {
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 ) );
}
}
}
private Object[] resolveActualInputs(EvaluationContext ctx, FEEL feel) {
Map<String, Object> variables = ctx.getAllValues();
Object[] actualInputs = new Object[ inputs.size() ];
for( int i = 0; i < inputs.size(); i++ ) {
CompiledExpression compiledInput = inputs.get( i ).getCompiledInput();
if( compiledInput != null ) {
actualInputs[i] = feel.evaluate( compiledInput, variables );
} else {
actualInputs[i] = feel.evaluate( inputs.get( i ).getInputExpression(), variables );
}
}
return actualInputs;
}
/**
* If valid input values are defined, check that all parameters match the respective valid inputs
* @param ctx
* @param params
* @return
*/
private Either<FEELEvent, Object> 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 );
// if a list of values is defined, check the the parameter matches the value
if ( input.getInputValues() != null && ! input.getInputValues().isEmpty() ) {
final Object parameter = params[i];
boolean satisfies = input.getInputValues().stream().map( ut -> ut.apply( ctx, parameter ) ).filter( Boolean::booleanValue ).findAny().orElse( false );
if ( !satisfies ) {
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 Either.ofRight(true);
}
/**
* Finds all rules that match a given set of parameters
*
* @param ctx
* @param params
* @return
*/
private List<DTDecisionRule> findMatches(EvaluationContext ctx, Object[] params) {
List<DTDecisionRule> matchingDecisionRules = new ArrayList<>();
for ( DTDecisionRule decisionRule : decisionRules ) {
if ( matches( ctx, params, decisionRule ) ) {
matchingDecisionRules.add( decisionRule );
}
}
FEELEventListenersManager.notifyListeners( ctx.getEventsManager() , () -> {
List<Integer> matches = matchingDecisionRules.stream().map( dr -> dr.getIndex() + 1 ).collect( Collectors.toList() );
return new DecisionTableRulesMatchedEvent(FEELEvent.Severity.INFO,
"Rules matched for decision table '" + getName() + "': " + matches.toString(),
getName(),
getName(),
matches );
}
);
return matchingDecisionRules;
}
/**
* Checks if the parameters match a single rule
* @param ctx
* @param params
* @param rule
* @return
*/
private boolean matches(EvaluationContext ctx, Object[] params, DTDecisionRule rule) {
for( int i = 0; i < params.length; i++ ) {
if( ! satisfies( ctx, params[i], rule.getInputEntry().get( i ) ) ) {
return false;
}
}
return true;
}
/**
* Checks that a given parameter matches a single cell test
* @param ctx
* @param param
* @param test
* @return
*/
private boolean satisfies(EvaluationContext ctx, Object param, UnaryTest test ) {
return test.apply( ctx, param );
}
private List<Object> evaluateResults(EvaluationContext ctx, FEEL feel, Object[] params, List<DTDecisionRule> matchingDecisionRules) {
List<Object> results = matchingDecisionRules.stream().map( dr -> hitToOutput( ctx, feel, dr ) ).collect( Collectors.toList());
return results;
}
/**
* Each hit results in one output value (multiple outputs are collected into a single context value)
*/
private Object hitToOutput(EvaluationContext ctx, FEEL feel, DTDecisionRule rule) {
List<CompiledExpression> outputEntries = rule.getOutputEntry();
Map<String, Object> values = ctx.getAllValues();
if ( outputEntries.size() == 1 ) {
Object value = feel.evaluate( outputEntries.get( 0 ), values );
return value;
} else {
// zip outputEntries with its name:
return IntStream.range( 0, outputs.size() ).boxed()
.collect( toMap( i -> outputs.get( i ).getName(), i -> feel.evaluate( outputEntries.get( i ), values ) ) );
}
}
/**
* No hits matched for the DT, so calculate result based on default outputs
*/
private Object defaultToOutput(EvaluationContext ctx, FEEL feel) {
Map<String, Object> values = ctx.getAllValues();
if ( outputs.size() == 1 ) {
Object value = feel.evaluate( outputs.get( 0 ).getDefaultValue(), values );
return value;
} else {
// zip outputEntries with its name:
return IntStream.range( 0, outputs.size() ).boxed()
.collect( toMap( i -> outputs.get( i ).getName(), i -> feel.evaluate( outputs.get( i ).getDefaultValue(), values ) ) );
}
}
public HitPolicy getHitPolicy() {
return hitPolicy;
}
public void setName(String name) {
this.name = name;
}
public String getName() {
return name;
}
public List<DTOutputClause> getOutputs() {
return outputs;
}
public List<String> getParameterNames() {
return parameterNames;
}
public String getSignature() {
return getName() + "( " + parameterNames.stream().collect( Collectors.joining( ", " ) ) + " )";
}
}