From 8129e204198c6e028d4ccb506138ba54b33a8ce0 Mon Sep 17 00:00:00 2001 From: Tommy Neubert Date: Wed, 27 Oct 2021 09:42:34 +0200 Subject: [PATCH] DROOLS-6647 only remove inner dependencies of an MID, external ones should be kept --- .../signavio/MultiInstanceDecisionLogic.java | 475 ++++++++++-------- .../MultiInstanceDecisionLogicTest.java | 97 ++++ 2 files changed, 376 insertions(+), 196 deletions(-) create mode 100644 kie-dmn/kie-dmn-signavio/src/test/java/org/kie/dmn/signavio/MultiInstanceDecisionLogicTest.java diff --git a/kie-dmn/kie-dmn-signavio/src/main/java/org/kie/dmn/signavio/MultiInstanceDecisionLogic.java b/kie-dmn/kie-dmn-signavio/src/main/java/org/kie/dmn/signavio/MultiInstanceDecisionLogic.java index 273145b6f5..d68d98f86c 100644 --- a/kie-dmn/kie-dmn-signavio/src/main/java/org/kie/dmn/signavio/MultiInstanceDecisionLogic.java +++ b/kie-dmn/kie-dmn-signavio/src/main/java/org/kie/dmn/signavio/MultiInstanceDecisionLogic.java @@ -19,12 +19,14 @@ package org.kie.dmn.signavio; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; +import java.util.HashSet; import java.util.List; import java.util.Optional; -import java.util.stream.Collectors; +import java.util.Set; import com.thoughtworks.xstream.annotations.XStreamAlias; import org.kie.dmn.api.core.DMNContext; +import org.kie.dmn.api.core.DMNModel; import org.kie.dmn.api.core.DMNResult; import org.kie.dmn.api.core.ast.DMNNode; import org.kie.dmn.api.core.event.DMNRuntimeEventManager; @@ -52,201 +54,282 @@ import org.kie.dmn.feel.runtime.functions.SumFunction; import org.kie.dmn.feel.util.EvalHelper; import org.kie.dmn.model.api.DMNElement.ExtensionElements; +import static java.util.Collections.emptySet; +import static java.util.Collections.singleton; +import static java.util.stream.Collectors.toSet; + @XStreamAlias("MultiInstanceDecisionLogic") public class MultiInstanceDecisionLogic { - - @XStreamAlias("iterationExpression") - private String iterationExpression; - - @XStreamAlias("iteratorShapeId") - private String iteratorShapeId; - - @XStreamAlias("aggregationFunction") - private String aggregationFunction; - - @XStreamAlias("topLevelDecisionId") - private String topLevelDecisionId; - - public String getIterationExpression() { - return iterationExpression; - } - - public String getIteratorShapeId() { - return iteratorShapeId; - } - - public String getAggregationFunction() { - return aggregationFunction; - } - - public String getTopLevelDecisionId() { - return topLevelDecisionId; - } - - @Override - public String toString() { - StringBuilder builder = new StringBuilder(); - builder.append("MultiInstanceDecisionLogic [iterationExpression=").append(iterationExpression).append(", iteratorShapeId=").append(iteratorShapeId).append(", aggregationFunction=").append(aggregationFunction).append(", topLevelDecisionId=").append(topLevelDecisionId).append("]"); - return builder.toString(); - } - - public static class MultiInstanceDecisionNodeCompiler extends DecisionCompiler { - - private Optional getMIDL(DMNNode node) { - if ( node instanceof DecisionNodeImpl ) { - DecisionNodeImpl nodeImpl = (DecisionNodeImpl) node; - ExtensionElements extElementsList = nodeImpl.getSource().getExtensionElements(); - if ( extElementsList != null && extElementsList.getAny() != null ) { - return extElementsList.getAny().stream() - .filter(MultiInstanceDecisionLogic.class::isInstance) - .map(MultiInstanceDecisionLogic.class::cast) - .findFirst(); - } - } - return Optional.empty(); - } - - @Override - public boolean accept(DMNNode node) { - return getMIDL(node).isPresent(); - } - - @Override - public void compileEvaluator(DMNNode node, DMNCompilerImpl compiler, DMNCompilerContext ctx, DMNModelImpl model) { - DecisionNodeImpl di = (DecisionNodeImpl) node; - compiler.linkRequirements(model, di); - - MultiInstanceDecisionLogic midl = - getMIDL(node).orElseThrow(() -> new IllegalStateException("Node doesn't contain multi instance decision logic!" + node.toString())); - - // set the evaluator accordingly to Signavio logic. - MultiInstanceDecisionNodeEvaluator miEvaluator = new MultiInstanceDecisionNodeEvaluator(midl, model, di, ctx.getFeelHelper().newFEELInstance()); - di.setEvaluator(miEvaluator); - - compiler.addCallback((cCompiler, cCtx, cModel) -> { - // Remove the top level decision and its dependencies, from the DMN Model (Decision|BKM|InputData) indexes - // Remember that as the dependencies will be removed from indexes, are no longer available at evalutation from the DMNExpressionEvaluator - // hence the MultiInstanceDecisionNodeEvaluator will need to cache anything which is accessed by the DMN Model (Decision|BKM|InputData) indexes - DecisionNodeImpl topLevelDecision = (DecisionNodeImpl) cModel.getDecisionById(midl.topLevelDecisionId); - recurseNodeToRemoveItAndDepsFromModelIndex(topLevelDecision, cModel); - - List allWrappedDeps = DRGAnalysisUtils.dependencies(cModel, topLevelDecision).stream().map(DRGDependency::getDependency).collect(Collectors.toList()); - - // DROOLS-6647 multi-instance decisions with multiple levels of decisions inside - for (DMNNode candidate : allWrappedDeps) { - if (candidate instanceof DecisionNodeImpl) { - DecisionNodeImpl reqDecision = (DecisionNodeImpl) candidate; - recurseNodeToRemoveItAndDepsFromModelIndex(reqDecision, cModel); - miEvaluator.addReqDecision(reqDecision); - } - } - }); - } - - public static void recurseNodeToRemoveItAndDepsFromModelIndex(DMNNode topLevelDecision, DMNModelImpl model) { - model.removeDMNNodeFromIndexes(topLevelDecision); - - for ( DMNNode dep : ((DMNBaseNode)topLevelDecision).getDependencies().values() ) { - recurseNodeToRemoveItAndDepsFromModelIndex( dep, model); - } - } - } - - /** - * Implements the Multi instance Decision node of Signavio as a DMNExpressionEvaluator - */ - public static class MultiInstanceDecisionNodeEvaluator implements DMNExpressionEvaluator { - - private MultiInstanceDecisionLogic mi; - private DMNModelImpl model; - private DecisionNodeImpl di; - private String contextIteratorName; - private DecisionNodeImpl topLevelDecision; - private List reqDecisions = new ArrayList<>(); - private final FEEL feel; - - public MultiInstanceDecisionNodeEvaluator(MultiInstanceDecisionLogic mi, DMNModelImpl model, DecisionNodeImpl di, FEEL feel) { - this.mi = mi; - this.model = model; - this.di = di; - this.feel = feel; - contextIteratorName = model.getInputById( mi.iteratorShapeId ).getName(); - topLevelDecision = (DecisionNodeImpl) model.getDecisionById(mi.topLevelDecisionId); - } - - public void addReqDecision(DecisionNodeImpl reqDecision) { - this.reqDecisions.add(reqDecision); - } - - @Override - public EvaluatorResult evaluate(DMNRuntimeEventManager eventManager, DMNResult dmnr) { - DMNResultImpl result = (DMNResultImpl) dmnr; - DMNContext previousContext = result.getContext(); - DMNContextImpl dmnContext = (DMNContextImpl) previousContext.clone(); - result.setContext( dmnContext ); - - List invokationResults = new ArrayList<>(); - - try { - Object cycleOnRaw = feel.evaluate(mi.iterationExpression, dmnContext.getAll()); - Collection cycleOn = null; - if ( cycleOnRaw instanceof Collection ) { - cycleOn = (Collection) cycleOnRaw; - } else { - cycleOn = Arrays.asList(cycleOnRaw); - } - for ( Object cycledValue : cycleOn ) { - DMNContext nonCycledContext = result.getContext(); - DMNContextImpl cyclingContext = (DMNContextImpl) nonCycledContext.clone(); - result.setContext( cyclingContext ); - - cyclingContext.set(contextIteratorName, cycledValue); - for (DecisionNodeImpl reqDecision : this.reqDecisions) { - Object subResult = reqDecision.getEvaluator().evaluate(eventManager, result).getResult(); - cyclingContext.set(reqDecision.getName(), subResult); - } - Object evaluationResult = topLevelDecision.getEvaluator().evaluate(eventManager, result).getResult(); - invokationResults.add(evaluationResult); - - result.setContext( nonCycledContext ); - } - } finally { - result.setContext( previousContext ); - } - - FEELFnResult r; - switch (mi.aggregationFunction) { - case "SUM": - r = new SumFunction().invoke(invokationResults); - break; - case "MIN": - r = new MinFunction().invoke(invokationResults); - break; - case "MAX": - r = new MaxFunction().invoke(invokationResults); - break; - case "COUNT": - r = FEELFnResult.ofResult(EvalHelper.getBigDecimalOrNull(invokationResults.size())); - break; - case "ALLTRUE": - r = new AllFunction().invoke(invokationResults); - break; - case "ANYTRUE": - r = new AnyFunction().invoke(invokationResults); - break; - case "ALLFALSE": - FEELFnResult anyResult = new AnyFunction().invoke(invokationResults); - r = anyResult.map(b -> !b); - break; - case "COLLECT": - default: - r = FEELFnResult.ofResult(invokationResults); - break; - } - - return new EvaluatorResultImpl(r.getOrElseThrow(e -> new RuntimeException(e.toString())), ResultType.SUCCESS); - } - - } - + + @XStreamAlias("iterationExpression") + private String iterationExpression; + + @XStreamAlias("iteratorShapeId") + private String iteratorShapeId; + + @XStreamAlias("aggregationFunction") + private String aggregationFunction; + + @XStreamAlias("topLevelDecisionId") + private String topLevelDecisionId; + + + public String getIterationExpression() { + return iterationExpression; + } + + + public String getIteratorShapeId() { + return iteratorShapeId; + } + + + public String getAggregationFunction() { + return aggregationFunction; + } + + + public String getTopLevelDecisionId() { + return topLevelDecisionId; + } + + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append("MultiInstanceDecisionLogic [iterationExpression=").append(iterationExpression) + .append(", iteratorShapeId=").append(iteratorShapeId).append(", aggregationFunction=") + .append(aggregationFunction).append(", topLevelDecisionId=").append(topLevelDecisionId).append("]"); + return builder.toString(); + } + + + public static class MultiInstanceDecisionNodeCompiler extends DecisionCompiler { + + private Optional getMIDL(DMNNode node) { + if (node instanceof DecisionNodeImpl) { + DecisionNodeImpl nodeImpl = (DecisionNodeImpl) node; + ExtensionElements extElementsList = nodeImpl.getSource().getExtensionElements(); + if (extElementsList != null && extElementsList.getAny() != null) { + return extElementsList.getAny().stream() + .filter(MultiInstanceDecisionLogic.class::isInstance) + .map(MultiInstanceDecisionLogic.class::cast) + .findFirst(); + } + } + return Optional.empty(); + } + + + @Override + public boolean accept(DMNNode node) { + return getMIDL(node).isPresent(); + } + + + @Override + public void compileEvaluator(DMNNode node, DMNCompilerImpl compiler, DMNCompilerContext ctx, + DMNModelImpl model) { + DecisionNodeImpl di = (DecisionNodeImpl) node; + compiler.linkRequirements(model, di); + + MultiInstanceDecisionLogic midl = + getMIDL(node).orElseThrow(() -> new IllegalStateException( + "Node doesn't contain multi instance decision logic!" + node.toString())); + + // set the evaluator accordingly to Signavio logic. + MultiInstanceDecisionNodeEvaluator miEvaluator = + new MultiInstanceDecisionNodeEvaluator(midl, model, di, ctx.getFeelHelper().newFEELInstance()); + di.setEvaluator(miEvaluator); + + compiler.addCallback((cCompiler, cCtx, cModel) -> { + MultiInstanceDecisionProcessor processor = new MultiInstanceDecisionProcessor(midl, cModel); + addRequiredDecisions(miEvaluator, processor); + removeChildElementsFromIndex(model, processor); + }); + } + + + private void addRequiredDecisions(MultiInstanceDecisionNodeEvaluator miEvaluator, + MultiInstanceDecisionProcessor processor) { + processor.findAllDependencies().stream() + .filter(DecisionNodeImpl.class::isInstance) + .map(DecisionNodeImpl.class::cast) + .forEach(miEvaluator::addReqDecision); + } + + + private void removeChildElementsFromIndex(DMNModelImpl model, MultiInstanceDecisionProcessor processor) { + processor.findAllChildElements().forEach(model::removeDMNNodeFromIndexes); + } + + } + + public static class MultiInstanceDecisionProcessor { + + private final MultiInstanceDecisionLogic mid; + private final DMNModel model; + + + public MultiInstanceDecisionProcessor(MultiInstanceDecisionLogic mid, DMNModel model) { + this.mid = mid; + this.model = model; + } + + + public Collection findAllChildElements() { + return new HashSet<>(processNode(topLevelDecision())); + } + + + public Collection findAllDependencies() { + return DRGAnalysisUtils.dependencies(model, topLevelDecision()).stream() + .map(DRGDependency::getDependency) + .collect(toSet()); + } + + + private Set processNode(DMNBaseNode currentNode) { + if (currentNodeIsTheIterator(currentNode)) { + return singleton(currentNode); + } + + Set pathToIterator = findPathToIterator(currentNode); + if (pathToIterator.isEmpty()) { + return emptySet(); + } + return extendPathBy(currentNode, pathToIterator); + } + + + private Set extendPathBy(DMNBaseNode node, Set pathToIterator) { + Set extendedPath = new HashSet<>(pathToIterator); + extendedPath.add(node); + return extendedPath; + } + + + private Set findPathToIterator(DMNBaseNode currentNode) { + return currentNode.getDependencies().values().stream() + .map(DMNBaseNode.class::cast) + .map(this::processNode) + .flatMap(Collection::stream) + .collect(toSet()); + } + + + private boolean currentNodeIsTheIterator(DMNBaseNode node) { + return mid.getIteratorShapeId().equals(node.getId()); + } + + + private DMNBaseNode topLevelDecision() { + return (DMNBaseNode) model.getDecisionById(mid.getTopLevelDecisionId()); + } + + } + + /** + * Implements the Multi instance Decision node of Signavio as a DMNExpressionEvaluator + */ + public static class MultiInstanceDecisionNodeEvaluator implements DMNExpressionEvaluator { + + private MultiInstanceDecisionLogic mi; + private DMNModelImpl model; + private DecisionNodeImpl di; + private String contextIteratorName; + private DecisionNodeImpl topLevelDecision; + private List reqDecisions = new ArrayList<>(); + private final FEEL feel; + + + public MultiInstanceDecisionNodeEvaluator(MultiInstanceDecisionLogic mi, DMNModelImpl model, + DecisionNodeImpl di, FEEL feel) { + this.mi = mi; + this.model = model; + this.di = di; + this.feel = feel; + contextIteratorName = model.getInputById(mi.iteratorShapeId).getName(); + topLevelDecision = (DecisionNodeImpl) model.getDecisionById(mi.topLevelDecisionId); + } + + + public void addReqDecision(DecisionNodeImpl reqDecision) { + this.reqDecisions.add(reqDecision); + } + + + @Override + public EvaluatorResult evaluate(DMNRuntimeEventManager eventManager, DMNResult dmnr) { + DMNResultImpl result = (DMNResultImpl) dmnr; + DMNContext previousContext = result.getContext(); + DMNContextImpl dmnContext = (DMNContextImpl) previousContext.clone(); + result.setContext(dmnContext); + + List invokationResults = new ArrayList<>(); + + try { + Object cycleOnRaw = feel.evaluate(mi.iterationExpression, dmnContext.getAll()); + Collection cycleOn = null; + if (cycleOnRaw instanceof Collection) { + cycleOn = (Collection) cycleOnRaw; + } else { + cycleOn = Arrays.asList(cycleOnRaw); + } + for (Object cycledValue : cycleOn) { + DMNContext nonCycledContext = result.getContext(); + DMNContextImpl cyclingContext = (DMNContextImpl) nonCycledContext.clone(); + result.setContext(cyclingContext); + + cyclingContext.set(contextIteratorName, cycledValue); + for (DecisionNodeImpl reqDecision : this.reqDecisions) { + Object subResult = reqDecision.getEvaluator().evaluate(eventManager, result).getResult(); + cyclingContext.set(reqDecision.getName(), subResult); + } + Object evaluationResult = + topLevelDecision.getEvaluator().evaluate(eventManager, result).getResult(); + invokationResults.add(evaluationResult); + + result.setContext(nonCycledContext); + } + } finally { + result.setContext(previousContext); + } + + FEELFnResult r; + switch (mi.aggregationFunction) { + case "SUM": + r = new SumFunction().invoke(invokationResults); + break; + case "MIN": + r = new MinFunction().invoke(invokationResults); + break; + case "MAX": + r = new MaxFunction().invoke(invokationResults); + break; + case "COUNT": + r = FEELFnResult.ofResult(EvalHelper.getBigDecimalOrNull(invokationResults.size())); + break; + case "ALLTRUE": + r = new AllFunction().invoke(invokationResults); + break; + case "ANYTRUE": + r = new AnyFunction().invoke(invokationResults); + break; + case "ALLFALSE": + FEELFnResult anyResult = new AnyFunction().invoke(invokationResults); + r = anyResult.map(b -> !b); + break; + case "COLLECT": + default: + r = FEELFnResult.ofResult(invokationResults); + break; + } + + return new EvaluatorResultImpl(r.getOrElseThrow(e -> new RuntimeException(e.toString())), + ResultType.SUCCESS); + } + + } + } diff --git a/kie-dmn/kie-dmn-signavio/src/test/java/org/kie/dmn/signavio/MultiInstanceDecisionLogicTest.java b/kie-dmn/kie-dmn-signavio/src/test/java/org/kie/dmn/signavio/MultiInstanceDecisionLogicTest.java new file mode 100644 index 0000000000..75c0cdbd20 --- /dev/null +++ b/kie-dmn/kie-dmn-signavio/src/test/java/org/kie/dmn/signavio/MultiInstanceDecisionLogicTest.java @@ -0,0 +1,97 @@ +package org.kie.dmn.signavio; + +import java.util.Collection; +import java.util.List; +import java.util.Map; + +import org.junit.Before; +import org.junit.Test; +import org.kie.dmn.api.core.DMNModel; +import org.kie.dmn.api.core.ast.DMNNode; +import org.kie.dmn.core.ast.DMNBaseNode; +import org.kie.dmn.core.ast.DecisionNodeImpl; +import org.kie.dmn.core.ast.InputDataNodeImpl; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import static java.util.Arrays.asList; +import static java.util.function.Function.identity; +import static java.util.stream.Collectors.toList; +import static java.util.stream.Collectors.toMap; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.hasSize; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; + +public class MultiInstanceDecisionLogicTest { + + private static final String ITERATOR = "I1"; + private static final String TOP_LEVEL = "A"; + + @Mock + private MultiInstanceDecisionLogic mid; + + @Mock + private DMNModel model; + + + @Before + public void setup() { + MockitoAnnotations.openMocks(this); + doReturn(TOP_LEVEL).when(mid).getTopLevelDecisionId(); + doReturn(ITERATOR).when(mid).getIteratorShapeId(); + + DMNBaseNode a = decision("A"); + DMNBaseNode b = decision("B"); + DMNBaseNode c = decision("C"); + DMNBaseNode d = decision("D"); + + DMNBaseNode i1 = input("I1"); + DMNBaseNode i2 = input("I2"); + + connect(a, asList(b, c)); + connect(b, asList(i1)); + connect(c, asList(i1, d)); + connect(d, asList(i2)); + } + + + private void connect(DMNBaseNode node, List dependencies) { + Map deps = dependencies.stream().collect(toMap(DMNNode::getId, identity())); + doReturn(deps).when(node).getDependencies(); + } + + + private DMNBaseNode decision(String id) { + DecisionNodeImpl decision = mock(DecisionNodeImpl.class); + doReturn(id).when(decision).getId(); + doReturn(decision).when(model).getDecisionById(id); + return decision; + } + + + private DMNBaseNode input(String id) { + InputDataNodeImpl input = mock(InputDataNodeImpl.class); + doReturn(id).when(input).getId(); + doReturn(input).when(model).getInputById(id); + return input; + } + + + @Test + public void thatFindAllChildElements_withMid_collectsCorrectChildsAndSkipsExternals() { + // Arrange + MultiInstanceDecisionLogic.MultiInstanceDecisionProcessor processor = + new MultiInstanceDecisionLogic.MultiInstanceDecisionProcessor(mid, model); + + // Act + Collection innerNodes = processor.findAllChildElements(); + + // Assert + Collection ids = innerNodes.stream().map(DMNNode::getId).collect(toList()); + assertThat(ids, containsInAnyOrder("A", "B", "C", "I1")); + assertThat(ids, hasSize(4)); + } + +} \ No newline at end of file -- 2.33.1.windows.1