/* * 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.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.DecisionTableRulesSelectedEvent; import org.kie.dmn.feel.runtime.events.HitPolicyViolationEvent; import org.kie.dmn.feel.util.Pair; import java.math.BigDecimal; import java.util.*; import java.util.function.Function; import java.util.stream.Collector; import java.util.stream.Collectors; import java.util.stream.Stream; import static java.util.stream.Collectors.*; import static java.util.stream.IntStream.range; public enum HitPolicy { UNIQUE( "U", "UNIQUE", HitPolicy::unique ), FIRST( "F", "FIRST", HitPolicy::first ), PRIORITY( "P", "PRIORITY", HitPolicy::priority ), ANY( "A", "ANY", HitPolicy::any ), COLLECT( "C", "COLLECT", HitPolicy::ruleOrder ), // Collect – return a list of the outputs in arbitrary order COLLECT_SUM( "C+", "COLLECT SUM", HitPolicy::sumCollect ), COLLECT_COUNT( "C#", "COLLECT COUNT", HitPolicy::countCollect ), COLLECT_MIN( "C<", "COLLECT MIN", HitPolicy::minCollect ), COLLECT_MAX( "C>", "COLLECT MAX", HitPolicy::maxCollect ), RULE_ORDER( "R", "RULE ORDER", HitPolicy::ruleOrder ), OUTPUT_ORDER( "O", "OUTPUT ORDER", HitPolicy::outputOrder ); private final String shortName; private final String longName; private final HitPolicyDTI dti; HitPolicy(final String shortName, final String longName) { this( shortName, longName, HitPolicy::notImplemented ); } HitPolicy(final String shortName, final String longName, final HitPolicyDTI dti) { this.shortName = shortName; this.longName = longName; this.dti = dti; } public String getShortName() { return shortName; } public String getLongName() { return longName; } public HitPolicyDTI getDti() { return dti; } public static HitPolicy fromString(String policy) { policy = policy.toUpperCase(); for ( HitPolicy c : HitPolicy.values() ) { if ( c.shortName.equals( policy ) || c.longName.equals( policy ) ) { return c; } } throw new IllegalArgumentException( "Unknown hit policy: " + policy ); } /* --------------------------------------- HIT POLICY IMPLEMENTATION --------------------------------------- */ @FunctionalInterface public interface HitPolicyDTI { Object dti( EvaluationContext ctx, DecisionTableImpl dt, Object[] params, List<DTDecisionRule> matches, List<Object> results); } public static Object notImplemented( EvaluationContext ctx, DecisionTableImpl dt, Object[] params, List<DTDecisionRule> matches, List<Object> results) { throw new RuntimeException( "Not implemented" ); } /** * Unique – only a single rule can be matched */ public static Object unique( EvaluationContext ctx, DecisionTableImpl dt, Object[] params, List<DTDecisionRule> matches, List<Object> results) { if ( matches.size() > 1 ) { FEELEventListenersManager.notifyListeners( ctx.getEventsManager(), () -> { List<Integer> ruleMatches = matches.stream().map( m -> m.getIndex() + 1 ).collect( toList() ); return new HitPolicyViolationEvent( FEELEvent.Severity.ERROR, "UNIQUE hit policy decision tables can only have one matching rule. " + "Multiple matches found for decision table '" + dt.getName() + "'. Matched rules: " + ruleMatches, dt.getName(), ruleMatches ); } ); return null; } if ( matches.size() == 1 ) { FEELEventListenersManager.notifyListeners( ctx.getEventsManager(), () -> { int index = matches.get( 0 ).getIndex() + 1; return new DecisionTableRulesSelectedEvent( FEELEvent.Severity.INFO, "Rule fired for decision table '" + dt.getName() + "': " + index, dt.getName(), dt.getName(), Collections.singletonList( index ) ); } ); return results.get( 0 ); } return null; } /** * First – return the first match in rule order */ public static Object first( EvaluationContext ctx, DecisionTableImpl dt, Object[] params, List<DTDecisionRule> matches, List<Object> results) { if ( matches.size() >= 1 ) { FEELEventListenersManager.notifyListeners( ctx.getEventsManager(), () -> { int index = matches.get( 0 ).getIndex() + 1; return new DecisionTableRulesSelectedEvent( FEELEvent.Severity.INFO, "Rule fired for decision table '" + dt.getName() + "': " + index, dt.getName(), dt.getName(), Collections.singletonList( index ) ); } ); return results.get( 0 ); } return null; } /** * Any – multiple rules can match, but they all have the same output */ public static Object any( EvaluationContext ctx, DecisionTableImpl dt, Object[] params, List<DTDecisionRule> matches, List<Object> results) { if ( matches.size() >= 1 ) { long distinctOutputEntry = results.stream() .distinct() .count(); if ( distinctOutputEntry > 1 ) { throw new RuntimeException( "multiple rules can match, but they [must] all have the same output" ); } FEELEventListenersManager.notifyListeners( ctx.getEventsManager(), () -> { int index = matches.get( 0 ).getIndex() + 1; return new DecisionTableRulesSelectedEvent( FEELEvent.Severity.INFO, "Rule fired for decision table '" + dt.getName() + "': " + index, dt.getName(), dt.getName(), Collections.singletonList( index ) ); } ); return results.get( 0 ); } return null; } /** * Priority – multiple rules can match, with different outputs. The output that comes first in the supplied output values list is returned */ public static Object priority( EvaluationContext ctx, DecisionTableImpl dt, Object[] params, List<DTDecisionRule> matches, List<Object> results) { if ( matches.isEmpty() ) { return null; } List<Pair<DTDecisionRule, Object>> pairs = sortPairs( ctx, dt, matches, results ); FEELEventListenersManager.notifyListeners( ctx.getEventsManager(), () -> { List<Integer> indexes = Collections.singletonList( pairs.get( 0 ).getLeft().getIndex() + 1 ); return new DecisionTableRulesSelectedEvent( FEELEvent.Severity.INFO, "Rules fired for decision table '" + dt.getName() + "': " + indexes, dt.getName(), dt.getName(), indexes ); } ); return pairs.get( 0 ).getRight(); } /** * Output order – return a list of outputs in the order of the output values list */ public static Object outputOrder( EvaluationContext ctx, DecisionTableImpl dt, Object[] params, List<DTDecisionRule> matches, List<Object> results ) { if ( matches.isEmpty() ) { return null; } List<Pair<DTDecisionRule, Object>> pairs = sortPairs( ctx, dt, matches, results ); FEELEventListenersManager.notifyListeners( ctx.getEventsManager(), () -> { List<Integer> indexes = pairs.stream().map( p -> p.getLeft().getIndex() + 1 ).collect( toList() ); return new DecisionTableRulesSelectedEvent( FEELEvent.Severity.INFO, "Rules fired for decision table '" + dt.getName() + "': " + indexes, dt.getName(), dt.getName(), indexes ); } ); return pairs.stream().map( p -> p.getRight() ).collect( Collectors.toList() ); } private static List<Pair<DTDecisionRule, Object>> sortPairs(EvaluationContext ctx, DecisionTableImpl dt, List<DTDecisionRule> matches, List<Object> results) { List<Pair<DTDecisionRule,Object>> pairs = new ArrayList<>( ); for( int i = 0; i < matches.size(); i++ ) { pairs.add( new Pair<>( matches.get( i ), results.get( i ) ) ); } if ( dt.getOutputs().size() == 1 && !dt.getOutputs().get( 0 ).getOutputValues().isEmpty() ) { // single output, just sort the results List<UnaryTest> outs = dt.getOutputs().get( 0 ).getOutputValues(); pairs.sort( (r1, r2) -> { return sortByOutputsOrder( ctx, outs, r1.getRight(), r2.getRight() ); } ); } else if ( dt.getOutputs().size() > 1 ) { // multiple outputs, collect the ones that have values listed List<DTOutputClause> priorities = dt.getOutputs().stream().filter( o -> !o.getOutputValues().isEmpty() ).collect( toList() ); pairs.sort( (r1, r2) -> { Map<String, Object> m1 = (Map<String, Object>) r1.getRight(); Map<String, Object> m2 = (Map<String, Object>) r2.getRight(); for ( DTOutputClause oc : priorities ) { int o = sortByOutputsOrder( ctx, oc.getOutputValues(), m1.get( oc.getName() ), m2.get( oc.getName() ) ); if ( o != 0 ) { return o; } } // unable to sort, so keep order return 0; } ); } return pairs; } private static int sortByOutputsOrder(EvaluationContext ctx, List<UnaryTest> outs, Object r1, Object r2) { boolean r1found = false; boolean r2found = false; for( int index = 0; index < outs.size() && !r1found && !r2found; index++ ) { UnaryTest ut = outs.get( index ); if( ut.apply( ctx, r1 ) ) { r1found = true; } if( ut.apply( ctx, r2 ) ) { r2found = true; } } if ( r1found && r2found ) { return 0; } else if ( r1found ) { return -1; } else if ( r2found ) { return 1; } else { return 0; } } /** * Rule order – return a list of outputs in rule order * Collect – return a list of the outputs in arbitrary order */ public static Object ruleOrder( EvaluationContext ctx, DecisionTableImpl dt, Object[] params, List<DTDecisionRule> matches, List<Object> results) { if ( matches.isEmpty() ) { return null; } FEELEventListenersManager.notifyListeners( ctx.getEventsManager(), () -> { List<Integer> indexes = matches.stream().map( m -> m.getIndex() + 1 ).collect( toList() ); return new DecisionTableRulesSelectedEvent( FEELEvent.Severity.INFO, "Rules fired for decision table '" + dt.getName() + "': " + indexes, dt.getName(), dt.getName(), indexes ); } ); return results; } public static <T> Collector<T, ?, Object> singleValueOrContext(List<DTOutputClause> outputs) { return new SingleValueOrContextCollector<T>( outputs.stream().map( DTOutputClause::getName ).collect( toList() ) ); } public static Object generalizedCollect( EvaluationContext ctx, DecisionTableImpl dt, List<?> results, Function<Stream<Object>, Object> resultCollector) { final List<Map<String, Object>> raw; final List<String> names = dt.getOutputs().stream().map( o -> o.getName() != null ? o.getName() : dt.getName() ).collect( toList() ); if ( names.size() > 1 ) { raw = (List<Map<String, Object>>) results; } else { raw = results.stream().map( (Object r) -> Collections.singletonMap( names.get( 0 ), r ) ).collect( toList() ); } return range( 0, names.size() ) .mapToObj( index -> names.get( index ) ) .map( name -> resultCollector.apply( raw.stream().map( r -> r.get( name ) ) ) ) .collect( singleValueOrContext( dt.getOutputs() ) ); } /** * C# – return the count of the outputs */ public static Object countCollect( EvaluationContext ctx, DecisionTableImpl dt, Object[] params, List<DTDecisionRule> matches, List<Object> results) { FEELEventListenersManager.notifyListeners( ctx.getEventsManager(), () -> { List<Integer> indexes = matches.stream().map( m -> m.getIndex() + 1 ).collect( toList() ); return new DecisionTableRulesSelectedEvent( FEELEvent.Severity.INFO, "Rules fired for decision table '" + dt.getName() + "': " + indexes, dt.getName(), dt.getName(), indexes ); } ); return generalizedCollect( ctx, dt, results, x -> new BigDecimal( x.collect( toSet() ).size() ) ); } /** * C< – return the minimum-valued output */ public static Object minCollect( EvaluationContext ctx, DecisionTableImpl dt, Object[] params, List<DTDecisionRule> matches, List<Object> results) { Object result = generalizedCollect( ctx, dt, results, x -> x.map( y -> (Comparable) y ).collect( minBy( Comparator.naturalOrder() ) ).orElse( null ) ); FEELEventListenersManager.notifyListeners( ctx.getEventsManager(), () -> { List<Integer> indexes = Collections.singletonList( matches.get( results.indexOf( result ) ).getIndex() + 1 ); return new DecisionTableRulesSelectedEvent( FEELEvent.Severity.INFO, "Rules fired for decision table '" + dt.getName() + "': " + indexes, dt.getName(), dt.getName(), indexes ); } ); return result; } /** * C> – return the maximum-valued output */ public static Object maxCollect( EvaluationContext ctx, DecisionTableImpl dt, Object[] params, List<DTDecisionRule> matches, List<Object> results) { Object result = generalizedCollect( ctx, dt, results, x -> x.map( y -> (Comparable) y ).collect( maxBy( Comparator.naturalOrder() ) ).orElse( null ) ); FEELEventListenersManager.notifyListeners( ctx.getEventsManager(), () -> { List<Integer> indexes = Collections.singletonList( matches.get( results.indexOf( result ) ).getIndex() + 1 ); return new DecisionTableRulesSelectedEvent( FEELEvent.Severity.INFO, "Rules fired for decision table '" + dt.getName() + "': " + indexes, dt.getName(), dt.getName(), indexes ); } ); return result; } /** * C+ – return the sum of the outputs */ public static Object sumCollect( EvaluationContext ctx, DecisionTableImpl dt, Object[] params, List<DTDecisionRule> matches, List<Object> results) { FEELEventListenersManager.notifyListeners( ctx.getEventsManager(), () -> { List<Integer> indexes = matches.stream().map( m -> m.getIndex() + 1 ).collect( toList() ); return new DecisionTableRulesSelectedEvent( FEELEvent.Severity.INFO, "Rules fired for decision table '" + dt.getName() + "': " + indexes, dt.getName(), dt.getName(), indexes ); } ); return generalizedCollect( ctx, dt, results, x -> x.reduce( BigDecimal.ZERO, (a, b) -> { if ( !(a instanceof Number && b instanceof Number) ) { return null; } else { BigDecimal aB = new BigDecimal( ((Number) a).toString() ); BigDecimal bB = new BigDecimal( ((Number) b).toString() ); return aB.add( bB ); } } ) ); } }