Skip to content

Commit

Permalink
[Incubator-kie-issues#1605] DMN JIT Executor successfully executing i…
Browse files Browse the repository at this point in the history
…nvalid DMN (apache#2142)

* [incubator-kie-issues#1605] Added validation checks before model evaluation to handle invalid models

* [incubator-kie-issues#1605] WIP

* [incubator-kie-issues#1605] Updated test cases

* [incubator-kie-issues#1605] Mockito  dependency added

* [incubator-kie-issues#1605] Fixing imports

* [incubator-kie-issues#1605] Fixing review comments

* [incubator-kie-issues#1605] Code update for SchemaResource

* [incubator-kie-issues#1605] Code update for DMNEvaluator

* [incubator-kie-issues#1605] Code refactoring

---------

Co-authored-by: athira <athira77@ibm.com>
  • Loading branch information
2 people authored and rgdoliveira committed Nov 18, 2024
1 parent 2d2ab5a commit 09037bb
Show file tree
Hide file tree
Showing 10 changed files with 342 additions and 56 deletions.
5 changes: 5 additions & 0 deletions jitexecutor/jitexecutor-dmn/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,11 @@
<artifactId>assertj-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,15 @@
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;

import org.kie.api.builder.Message;
import org.kie.api.io.Resource;
import org.kie.dmn.api.core.DMNContext;
import org.kie.dmn.api.core.DMNMessage;
import org.kie.dmn.api.core.DMNModel;
import org.kie.dmn.api.core.DMNResult;
import org.kie.dmn.api.core.DMNRuntime;
Expand All @@ -51,7 +55,43 @@ public static DMNEvaluator fromXML(String modelXML) {
.fromResources(Collections.singletonList(modelResource)).getOrElseThrow(RuntimeException::new);
dmnRuntime.addListener(new JITDMNListener());
DMNModel dmnModel = dmnRuntime.getModels().get(0);
return new DMNEvaluator(dmnModel, dmnRuntime);
return validateForErrors(dmnModel, dmnRuntime);
}

public static DMNEvaluator fromMultiple(MultipleResourcesPayload payload) {
Map<String, Resource> resources = new HashMap<>();
for (ResourceWithURI r : payload.getResources()) {
Resource readerResource = ResourceFactory.newReaderResource(new StringReader(r.getContent()), "UTF-8");
readerResource.setSourcePath(r.getURI());
resources.put(r.getURI(), readerResource);
}
ResolveByKey rbk = new ResolveByKey(resources);
DMNRuntime dmnRuntime = DMNRuntimeBuilder.fromDefaults()
.setRelativeImportResolver((x, y, locationURI) -> rbk.readerByKey(locationURI))
.buildConfiguration()
.fromResources(resources.values())
.getOrElseThrow(RuntimeException::new);
DMNModel mainModel = null;
for (DMNModel m : dmnRuntime.getModels()) {
if (m.getResource().getSourcePath().equals(payload.getMainURI())) {
mainModel = m;
break;
}
}
if (mainModel == null) {
throw new IllegalStateException("Was not able to identify main model from MultipleResourcesPayload contents.");
}
return validateForErrors(mainModel, dmnRuntime);
}

static DMNEvaluator validateForErrors(DMNModel dmnModel, DMNRuntime dmnRuntime) {
if (dmnModel.hasErrors()) {
List<DMNMessage> messages = dmnModel.getMessages(DMNMessage.Severity.ERROR);
String errorMessage = messages.stream().map(Message::getText).collect(Collectors.joining(", "));
throw new IllegalStateException(errorMessage);
} else {
return new DMNEvaluator(dmnModel, dmnRuntime);
}
}

private DMNEvaluator(DMNModel dmnModel, DMNRuntime dmnRuntime) {
Expand Down Expand Up @@ -88,29 +128,4 @@ public JITDMNResult evaluate(Map<String, Object> context) {
return new JITDMNResult(getNamespace(), getName(), dmnResult, evaluationHitIds.orElse(Collections.emptyMap()));
}

public static DMNEvaluator fromMultiple(MultipleResourcesPayload payload) {
Map<String, Resource> resources = new HashMap<>();
for (ResourceWithURI r : payload.getResources()) {
Resource readerResource = ResourceFactory.newReaderResource(new StringReader(r.getContent()), "UTF-8");
readerResource.setSourcePath(r.getURI());
resources.put(r.getURI(), readerResource);
}
ResolveByKey rbk = new ResolveByKey(resources);
DMNRuntime dmnRuntime = DMNRuntimeBuilder.fromDefaults()
.setRelativeImportResolver((x, y, locationURI) -> rbk.readerByKey(locationURI))
.buildConfiguration()
.fromResources(resources.values())
.getOrElseThrow(RuntimeException::new);
DMNModel mainModel = null;
for (DMNModel m : dmnRuntime.getModels()) {
if (m.getResource().getSourcePath().equals(payload.getMainURI())) {
mainModel = m;
break;
}
}
if (mainModel == null) {
throw new IllegalStateException("Was not able to identify main model from MultipleResourcesPayload contents.");
}
return new DMNEvaluator(mainModel, dmnRuntime);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.kogito.jitexecutor.dmn.api;

import java.util.function.Supplier;

import jakarta.ws.rs.core.Response;

public class DMNResourceHelper {

private DMNResourceHelper() {
}

public static Response manageResponse(Supplier<Response> responseSupplier) {
try {
return responseSupplier.get();
} catch (Exception e) {
String errorMessage = e.getMessage() != null ? e.getMessage() : "Failed to get result due to " + e.getClass().getName();
return Response.status(Response.Status.BAD_REQUEST).entity(errorMessage).build();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.function.Supplier;

import org.kie.dmn.core.internal.utils.MarshallingStubUtils;
import org.kie.kogito.jitexecutor.dmn.JITDMNService;
Expand Down Expand Up @@ -56,12 +57,15 @@ public Response jitdmn(JITDMNPayload payload) {
LOGGER.debug("jitdmn/");
LOGGER.debug(payload.toString());
LOGGER.debug(LINEBREAK);
JITDMNResult evaluateAll = payload.getModel() != null ? jitdmnService.evaluateModel(payload.getModel(), payload.getContext()) : jitdmnService.evaluateModel(payload, payload.getContext());
Map<String, Object> restResulk = new HashMap<>();
for (Entry<String, Object> kv : evaluateAll.getContext().getAll().entrySet()) {
restResulk.put(kv.getKey(), MarshallingStubUtils.stubDMNResult(kv.getValue(), String::valueOf));
}
return Response.ok(restResulk).build();
Supplier<Response> supplier = () -> {
JITDMNResult evaluateAll = payload.getModel() != null ? jitdmnService.evaluateModel(payload.getModel(), payload.getContext()) : jitdmnService.evaluateModel(payload, payload.getContext());
Map<String, Object> restResulk = new HashMap<>();
for (Entry<String, Object> kv : evaluateAll.getContext().getAll().entrySet()) {
restResulk.put(kv.getKey(), MarshallingStubUtils.stubDMNResult(kv.getValue(), String::valueOf));
}
return Response.ok(restResulk).build();
};
return DMNResourceHelper.manageResponse(supplier);
}

@POST
Expand All @@ -73,8 +77,11 @@ public Response jitdmnResult(JITDMNPayload payload) {
LOGGER.debug("jitdmn/dmnresult");
LOGGER.debug(payload.toString());
LOGGER.debug(LINEBREAK);
JITDMNResult dmnResult = payload.getModel() != null ? jitdmnService.evaluateModel(payload.getModel(), payload.getContext()) : jitdmnService.evaluateModel(payload, payload.getContext());
return Response.ok(dmnResult).build();
Supplier<Response> supplier = () -> {
JITDMNResult dmnResult = payload.getModel() != null ? jitdmnService.evaluateModel(payload.getModel(), payload.getContext()) : jitdmnService.evaluateModel(payload, payload.getContext());
return Response.ok(dmnResult).build();
};
return DMNResourceHelper.manageResponse(supplier);
}

@POST
Expand All @@ -86,9 +93,12 @@ public Response jitEvaluateAndExplain(JITDMNPayload payload) {
LOGGER.debug("jitdmn/evaluateAndExplain");
LOGGER.debug(payload.toString());
LOGGER.debug(LINEBREAK);
DMNResultWithExplanation response =
payload.getModel() != null ? jitdmnService.evaluateModelAndExplain(payload.getModel(), payload.getContext()) : jitdmnService.evaluateModelAndExplain(payload, payload.getContext());
return Response.ok(response).build();
Supplier<Response> supplier = () -> {
DMNResultWithExplanation response =
payload.getModel() != null ? jitdmnService.evaluateModelAndExplain(payload.getModel(), payload.getContext()) : jitdmnService.evaluateModelAndExplain(payload, payload.getContext());
return Response.ok(response).build();
};
return DMNResourceHelper.manageResponse(supplier);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.function.Supplier;

import org.eclipse.microprofile.openapi.OASFactory;
import org.eclipse.microprofile.openapi.models.OpenAPI;
Expand Down Expand Up @@ -75,9 +76,12 @@ public Response schema(String payload) {
LOGGER.debug("jitdmn/validate");
LOGGER.debug(payload);
LOGGER.debug(LINEBREAK);
DMNModel dmnModel = DMNEvaluator.fromXML(payload).getDmnModel();
DMNOASResult oasResult = DMNOASGeneratorFactory.generator(Collections.singletonList(dmnModel)).build();
return fullSchema(dmnModel, oasResult, true);
Supplier<Response> supplier = () -> {
DMNModel dmnModel = DMNEvaluator.fromXML(payload).getDmnModel();
DMNOASResult oasResult = DMNOASGeneratorFactory.generator(Collections.singletonList(dmnModel)).build();
return fullSchema(dmnModel, oasResult, true);
};
return DMNResourceHelper.manageResponse(supplier);
}

private Response fullSchema(DMNModel dmnModel, DMNOASResult oasResult, final boolean singleModel) {
Expand Down Expand Up @@ -111,31 +115,40 @@ private Response fullSchema(DMNModel dmnModel, DMNOASResult oasResult, final boo
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public Response schema(MultipleResourcesPayload payload) {
DMNEvaluator dmnEvaluator = DMNEvaluator.fromMultiple(payload);
DMNModel dmnModel = dmnEvaluator.getDmnModel();
DMNOASResult oasResult = DMNOASGeneratorFactory.generator(dmnEvaluator.getAllDMNModels()).build();
return fullSchema(dmnModel, oasResult, false);
Supplier<Response> supplier = () -> {
DMNEvaluator dmnEvaluator = DMNEvaluator.fromMultiple(payload);
DMNModel dmnModel = dmnEvaluator.getDmnModel();
DMNOASResult oasResult = DMNOASGeneratorFactory.generator(dmnEvaluator.getAllDMNModels()).build();
return fullSchema(dmnModel, oasResult, false);
};
return DMNResourceHelper.manageResponse(supplier);
}

@POST
@Consumes(MediaType.APPLICATION_XML)
@Produces(MediaType.APPLICATION_JSON)
@Path("form")
public Response form(String payload) {
DMNModel dmnModel = DMNEvaluator.fromXML(payload).getDmnModel();
DMNOASResult oasResult = DMNOASGeneratorFactory.generator(Collections.singletonList(dmnModel)).build();
return formSchema(dmnModel, oasResult);
Supplier<Response> supplier = () -> {
DMNModel dmnModel = DMNEvaluator.fromXML(payload).getDmnModel();
DMNOASResult oasResult = DMNOASGeneratorFactory.generator(Collections.singletonList(dmnModel)).build();
return formSchema(dmnModel, oasResult);
};
return DMNResourceHelper.manageResponse(supplier);
}

@POST
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
@Path("form")
public Response form(MultipleResourcesPayload payload) {
DMNEvaluator dmnEvaluator = DMNEvaluator.fromMultiple(payload);
DMNModel dmnModel = dmnEvaluator.getDmnModel();
DMNOASResult oasResult = DMNOASGeneratorFactory.generator(dmnEvaluator.getAllDMNModels()).build();
return formSchema(dmnModel, oasResult);
Supplier<Response> supplier = () -> {
DMNEvaluator dmnEvaluator = DMNEvaluator.fromMultiple(payload);
DMNModel dmnModel = dmnEvaluator.getDmnModel();
DMNOASResult oasResult = DMNOASGeneratorFactory.generator(dmnEvaluator.getAllDMNModels()).build();
return formSchema(dmnModel, oasResult);
};
return DMNResourceHelper.manageResponse(supplier);
}

private Response formSchema(DMNModel dmnModel, DMNOASResult oasResult) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.kogito.jitexecutor.dmn;

import java.io.IOException;
import java.util.Collections;

import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.kie.dmn.api.core.DMNMessage;
import org.kie.dmn.api.core.DMNModel;
import org.kie.dmn.api.core.DMNRuntime;
import org.kie.dmn.core.impl.DMNRuntimeImpl;

import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.kie.kogito.jitexecutor.dmn.TestingUtils.getModelFromIoUtils;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

public class DMNEvaluatorTest {

private static String model;
private static String invalidModel;

@BeforeAll
public static void setup() throws IOException {
model = getModelFromIoUtils("invalid_models/DMNv1_x/test.dmn");
invalidModel = getModelFromIoUtils("invalid_models/DMNv1_5/DMN-Invalid.dmn");
}

@Test
void testFromXMLSuccessModel() {
String modelXML = model;

DMNEvaluator evaluator = DMNEvaluator.fromXML(modelXML);
assertNotNull(evaluator);
}

@Test
public void testFromXMLModelWithError() {
String modelXML = invalidModel;

assertThrows(IllegalStateException.class, () -> {
DMNEvaluator.fromXML(modelXML);
});
}

@Test
void testValidateForErrorsThrowsException() {
DMNModel dmnModel = mock(DMNModel.class);
DMNRuntime dmnRuntime = mock(DMNRuntime.class);
DMNMessage message = mock(DMNMessage.class);

String errorMessage = "Error compiling FEEL expression 'Person Age >= 18' for name 'Can Drive?' on node 'Can Drive?': syntax error near 'Age'";
when(message.getText()).thenReturn(errorMessage);
when(dmnModel.hasErrors()).thenReturn(true);
when(dmnModel.getMessages(DMNMessage.Severity.ERROR)).thenReturn(Collections.singletonList(message));

assertThrows(IllegalStateException.class,
() -> DMNEvaluator.validateForErrors(dmnModel, dmnRuntime), errorMessage);
}

@Test
void testValidateForErrors() {
DMNModel dmnModel = mock(DMNModel.class);
DMNRuntime dmnRuntime = mock(DMNRuntimeImpl.class);

when(dmnModel.hasErrors()).thenReturn(false);
DMNEvaluator evaluator = DMNEvaluator.validateForErrors(dmnModel, dmnRuntime);

assertNotNull(evaluator);
}

}
Loading

0 comments on commit 09037bb

Please sign in to comment.