Skip to content

Commit

Permalink
[DROOLS-3730] Add support to DMN constraints on inputs (report input …
Browse files Browse the repository at this point in the history
…validation errors) (apache#1103)

* [DROOLS-3730] Introduce DMN input validation verification

* [DROOLS-3730] Enable type check, add tests

* [DROOLS-3730] Fix enforcer

* [DROOLS-3730] Update Javadoc

* [DROOLS-3730] Update Javadoc
  • Loading branch information
danielezonca authored and manstis committed Mar 26, 2019
1 parent d45fe4d commit 87945e6
Show file tree
Hide file tree
Showing 9 changed files with 162 additions and 15 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,16 @@
<artifactId>jackson-databind</artifactId>
</dependency>

<dependency>
<groupId>org.kie</groupId>
<artifactId>kie-dmn-core</artifactId>
</dependency>

<dependency>
<groupId>org.kie</groupId>
<artifactId>kie-dmn-model</artifactId>
</dependency>

<!-- Test dependencies -->
<dependency>
<groupId>org.uberfire</groupId>
Expand All @@ -152,11 +162,6 @@
<artifactId>assertj-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.kie</groupId>
<artifactId>kie-dmn-core</artifactId>
<scope>test</scope>
</dependency>

</dependencies>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -164,9 +164,9 @@ protected void checkTypeSupport(DMNType type, ErrorHolder errorHolder, String pa
}

private void visitType(DMNType type,
boolean alreadyInCollection,
ErrorHolder errorHolder,
String path) {
boolean alreadyInCollection,
ErrorHolder errorHolder,
String path) {
if (type.isComposite()) {
for (Map.Entry<String, DMNType> entry : type.getFields().entrySet()) {
String name = entry.getKey();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -151,8 +151,8 @@ public void init() {

@Override
public Map<Integer, Scenario> runScenario(final Path path,
final SimulationDescriptor simulationDescriptor,
final Map<Integer, Scenario> scenarioMap) {
final SimulationDescriptor simulationDescriptor,
final Map<Integer, Scenario> scenarioMap) {
return scenarioRunnerService.runTest(user.getIdentifier(),
path,
simulationDescriptor,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@
import org.kie.api.runtime.RequestContext;
import org.kie.dmn.api.core.DMNModel;
import org.kie.dmn.api.core.DMNRuntime;
import org.kie.dmn.core.compiler.RuntimeTypeCheckOption;
import org.kie.dmn.core.impl.DMNRuntimeImpl;
import org.kie.internal.builder.fluent.DMNRuntimeFluent;
import org.kie.internal.builder.fluent.ExecutableBuilder;
import org.kie.internal.command.RegistryContext;
Expand All @@ -42,7 +44,16 @@ private DMNScenarioExecutableBuilder(KieContainer kieContainer, String applicati

dmnRuntimeFluent = executableBuilder.newApplicationContext(applicationName)
.setKieContainer(kieContainer)
.newDMNRuntime();
.newDMNRuntime()
.addCommand(context -> {
RegistryContext registryContext = (RegistryContext) context;

DMNRuntime dmnRuntime = registryContext.lookup(DMNRuntime.class);

// force typeCheck to enable constraints
((DMNRuntimeImpl) dmnRuntime).setOption(new RuntimeTypeCheckOption(true));
return dmnRuntime;
});
}

private DMNScenarioExecutableBuilder(KieContainer kieContainer) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ public List<ScenarioGiven> extractGivenValues(SimulationDescriptor simulationDes
Object bean = getDirectMapping(paramsForBean)
.orElseGet(() -> createObject(factIdentifier.getClassName(), paramsForBean, classLoader));

scenarioGiven.add(new ScenarioGiven(factIdentifier, bean));
scenarioGiven.add(new ScenarioGiven(factIdentifier, bean, entry.getValue()));
}

return scenarioGiven;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Stream;

import org.drools.workbench.screens.scenariosimulation.backend.server.expression.ExpressionEvaluator;
import org.drools.workbench.screens.scenariosimulation.backend.server.fluent.DMNScenarioExecutableBuilder;
Expand All @@ -34,10 +35,13 @@
import org.drools.workbench.screens.scenariosimulation.model.FactMappingValue;
import org.drools.workbench.screens.scenariosimulation.model.ScenarioSimulationModel;
import org.drools.workbench.screens.scenariosimulation.model.SimulationDescriptor;
import org.kie.api.builder.Message;
import org.kie.api.runtime.KieContainer;
import org.kie.api.runtime.RequestContext;
import org.kie.dmn.api.core.DMNDecisionResult;
import org.kie.dmn.api.core.DMNMessage;
import org.kie.dmn.api.core.DMNResult;
import org.kie.dmn.model.api.NamedElement;

import static org.drools.workbench.screens.scenariosimulation.backend.server.runner.model.ResultWrapper.createErrorResult;
import static org.drools.workbench.screens.scenariosimulation.backend.server.runner.model.ResultWrapper.createResult;
Expand Down Expand Up @@ -69,6 +73,9 @@ public void verifyConditions(SimulationDescriptor simulationDescriptor,
RequestContext requestContext) {
DMNResult dmnResult = requestContext.getOutput(DMNScenarioExecutableBuilder.DMN_RESULT);

// report input validation errors
verifyInputConstraints(dmnResult.getMessages(), scenarioRunnerData.getGivens());

for (ScenarioExpect output : scenarioRunnerData.getExpects()) {
FactIdentifier factIdentifier = output.getFactIdentifier();
String decisionName = factIdentifier.getName();
Expand All @@ -90,6 +97,49 @@ public void verifyConditions(SimulationDescriptor simulationDescriptor,
}
}

/**
* Iterate over <code>DMNMessage</code>s to report errors on input data (validation errors)
* @param messages
* @param givens
*/
protected void verifyInputConstraints(List<DMNMessage> messages, List<ScenarioGiven> givens) {

messages.stream()
// filter out invalid messages (only errors with sourceReference)
.filter(message -> Message.Level.ERROR.equals(message.getLevel()) &&
message.getSourceReference() != null)
.flatMap(this::retrieveInputNodeName)
.forEach(name -> reportErrorIfUsed(name, givens));
}

/**
* Return the name of the input node that failed if exists (a <code>DMNMessage</code> can have no source)
* @param message
* @return
*/
protected Stream<String> retrieveInputNodeName(DMNMessage message) {
Object sourceReference = message.getSourceReference();
if (sourceReference instanceof NamedElement &&
((NamedElement) sourceReference).getName() != null) {
return Stream.of(((NamedElement) sourceReference).getName());
}
return Stream.empty();
}

/**
* Look for a <code>ScenarioGiven</code> mapped to the given <b>name</b> and set its errore status to <code>true</code>
* @param name
* @param givens
*/
protected void reportErrorIfUsed(String name, List<ScenarioGiven> givens) {
for (ScenarioGiven given : givens) {
if (name.equals(given.getFactIdentifier().getName())) {
given.getFactMappingValues().forEach(fmv -> fmv.setError(true));
return;
}
}
}

@SuppressWarnings("unchecked")
protected ResultWrapper getSingleFactValueResult(FactMapping factMapping,
FactMappingValue expectedResult,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,27 @@

package org.drools.workbench.screens.scenariosimulation.backend.server.runner.model;

import java.util.List;

import org.drools.workbench.screens.scenariosimulation.model.FactIdentifier;
import org.drools.workbench.screens.scenariosimulation.model.FactMappingValue;

/**
* This class wrap an entire given fact. It contains factIdentifier, instance of the
* bean and list of values (columns) used to create it
*/
public class ScenarioGiven {

private final FactIdentifier factIdentifier;
private final Object value;
private final List<FactMappingValue> factMappingValues;

public ScenarioGiven(FactIdentifier factIdentifier, Object value) {
public ScenarioGiven(FactIdentifier factIdentifier,
Object value,
List<FactMappingValue> factMappingValues) {
this.factIdentifier = factIdentifier;
this.value = value;
this.factMappingValues = factMappingValues;
}

public FactIdentifier getFactIdentifier() {
Expand All @@ -35,4 +46,8 @@ public FactIdentifier getFactIdentifier() {
public Object getValue() {
return value;
}

public List<FactMappingValue> getFactMappingValues() {
return factMappingValues;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
import org.drools.workbench.screens.scenariosimulation.backend.server.model.Dispute;
import org.drools.workbench.screens.scenariosimulation.backend.server.model.Person;
import org.drools.workbench.screens.scenariosimulation.backend.server.runner.model.ScenarioExpect;
import org.drools.workbench.screens.scenariosimulation.backend.server.runner.model.ScenarioGiven;
import org.drools.workbench.screens.scenariosimulation.backend.server.runner.model.ScenarioRunnerData;
import org.drools.workbench.screens.scenariosimulation.model.ExpressionIdentifier;
import org.drools.workbench.screens.scenariosimulation.model.FactIdentifier;
Expand All @@ -39,19 +40,28 @@
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.kie.api.builder.Message;
import org.kie.api.runtime.RequestContext;
import org.kie.dmn.api.core.DMNDecisionResult;
import org.kie.dmn.api.core.DMNMessage;
import org.kie.dmn.api.core.DMNResult;
import org.kie.dmn.model.api.NamedElement;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;

import static java.util.Arrays.asList;
import static java.util.Collections.singletonList;
import static java.util.stream.Collectors.toList;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.Matchers.any;
import static org.mockito.Mockito.anyString;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

@RunWith(MockitoJUnitRunner.class)
Expand All @@ -72,8 +82,8 @@ public class DMNScenarioRunnerHelperTest {
private static final String TEST_DESCRIPTION = "Test description";
private static final ClassLoader classLoader = RuleScenarioRunnerHelperTest.class.getClassLoader();
private static final ExpressionEvaluator expressionEvaluator = new DMNFeelExpressionEvaluator(classLoader);
private static final DMNScenarioRunnerHelper runnerHelper = new DMNScenarioRunnerHelper();

private DMNScenarioRunnerHelper runnerHelper;
private Simulation simulation;
private FactIdentifier personFactIdentifier;
private ExpressionIdentifier firstNameGivenExpressionIdentifier;
Expand All @@ -92,6 +102,8 @@ public class DMNScenarioRunnerHelperTest {

@Before
public void init() {
runnerHelper = spy(new DMNScenarioRunnerHelper());

simulation = new Simulation();
personFactIdentifier = FactIdentifier.create("Fact 1", Person.class.getCanonicalName());
firstNameGivenExpressionIdentifier = ExpressionIdentifier.create("First Name Given", FactMappingType.GIVEN);
Expand Down Expand Up @@ -192,4 +204,58 @@ public void createObject() {
assertEquals("TestName", creator.get("name"));
assertEquals("TestSurname", creator.get("surname"));
}

@Test
public void verifyInputConstraints() {
DMNMessage dmnMessageMock = mock(DMNMessage.class);
NamedElement namedElementMock = mock(NamedElement.class);
when(namedElementMock.getName()).thenReturn("");

List<DMNMessage> dmnMessages = singletonList(dmnMessageMock);
runnerHelper.verifyInputConstraints(dmnMessages, Collections.emptyList());

verify(runnerHelper, never()).retrieveInputNodeName(any());
verify(runnerHelper, never()).reportErrorIfUsed(any(), any());

when(dmnMessageMock.getSourceReference()).thenReturn(namedElementMock);
when(dmnMessageMock.getLevel()).thenReturn(Message.Level.ERROR);
runnerHelper.verifyInputConstraints(dmnMessages, Collections.emptyList());

verify(runnerHelper, times(1)).retrieveInputNodeName(any());
verify(runnerHelper, times(1)).reportErrorIfUsed(any(), any());
}

@Test
public void retrieveInputNodeName() {
final String INPUT_NAME = "INPUT_NAME";
DMNMessage dmnMessageMock = mock(DMNMessage.class);
NamedElement nodeMock = mock(NamedElement.class);
when(dmnMessageMock.getSourceReference()).thenReturn(nodeMock);
when(nodeMock.getName()).thenReturn(INPUT_NAME);
List<String> result = runnerHelper.retrieveInputNodeName(dmnMessageMock).collect(toList());
assertEquals(1, result.size());
assertEquals(INPUT_NAME, result.get(0));

result = runnerHelper.retrieveInputNodeName(mock(DMNMessage.class)).collect(toList());
assertEquals(0, result.size());
}

@Test
public void reportErrorIfUsed() {
String TEST = "TEST";
FactIdentifier factIdentifier = FactIdentifier.create(TEST, String.class.getCanonicalName());
FactMappingValue fmv = new FactMappingValue(factIdentifier, mock(ExpressionIdentifier.class), "");

List<FactMappingValue> factMappingValues = singletonList(fmv);

List<ScenarioGiven> scenarioGivens = singletonList(new ScenarioGiven(factIdentifier, "", factMappingValues));

runnerHelper.reportErrorIfUsed("noMatch", scenarioGivens);

assertFalse(fmv.isError());

runnerHelper.reportErrorIfUsed(TEST, scenarioGivens);

assertTrue(fmv.isError());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,7 @@ public void getScenarioResultsTest() {

Person person = new Person();
person.setFirstName("ANOTHER STRING");
ScenarioGiven newInput = new ScenarioGiven(personFactIdentifier, person);
ScenarioGiven newInput = new ScenarioGiven(personFactIdentifier, person, Collections.emptyList());

List<ScenarioResult> scenario3Results = runnerHelper.getScenarioResultsFromGivenFacts(simulation.getSimulationDescriptor(), scenario1Outputs, newInput, expressionEvaluator);
assertTrue(scenario1Outputs.get(0).getExpectedResult().get(0).isError());
Expand Down

0 comments on commit 87945e6

Please sign in to comment.