Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,12 @@
import org.acme.auth.AuthUtils;
import org.acme.constants.CheckStatus;
import org.acme.model.domain.EligibilityCheck;
import org.acme.model.dto.SaveDmnRequest;
import org.acme.model.dto.CheckDmnRequest;
import org.acme.persistence.EligibilityCheckRepository;
import org.acme.persistence.StorageService;
import org.acme.service.DmnService;

import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
Expand All @@ -27,6 +29,9 @@ public class EligibilityCheckResource {
@Inject
StorageService storageService;

@Inject
DmnService dmnService;

@GET
@Path("/checks")
public Response getPublicChecks(@Context SecurityIdentity identity) {
Expand Down Expand Up @@ -105,7 +110,7 @@ public Response updatePublicCheck(@Context SecurityIdentity identity,
@POST
@Consumes(MediaType.APPLICATION_JSON)
@Path("/save-check-dmn")
public Response updateCheckDmn(@Context SecurityIdentity identity, SaveDmnRequest saveDmnRequest){
public Response updateCheckDmn(@Context SecurityIdentity identity, CheckDmnRequest saveDmnRequest){
String checkId = saveDmnRequest.id;
String dmnModel = saveDmnRequest.dmnModel;
if (checkId == null || checkId.isBlank()){
Expand All @@ -121,17 +126,10 @@ public Response updateCheckDmn(@Context SecurityIdentity identity, SaveDmnReques
}

EligibilityCheck check = checkOpt.get();

//AUTHORIZATION
// if (!check.getOwnerId().equals(userId)){
// return Response.status(Response.Status.UNAUTHORIZED).build();
// }

if (dmnModel == null){
return Response.status(Response.Status.BAD_REQUEST)
.entity("Error: Missing required data: DMN Model")
.build();
if (!check.getOwnerId().equals(userId)){
return Response.status(Response.Status.UNAUTHORIZED).build();
}

try {
String filePath = storageService.getCheckDmnModelPath(checkId);
storageService.writeStringToStorage(filePath, dmnModel, "application/xml");
Expand All @@ -147,6 +145,51 @@ public Response updateCheckDmn(@Context SecurityIdentity identity, SaveDmnReques
}
}

@POST
@Consumes(MediaType.APPLICATION_JSON)
@Path("/validate-check-dmn")
public Response validateCheckDmn(@Context SecurityIdentity identity, CheckDmnRequest validateDmnRequest){
String checkId = validateDmnRequest.id;
String dmnModel = validateDmnRequest.dmnModel;
if (checkId == null || checkId.isBlank()){
return Response.status(Response.Status.BAD_REQUEST)
.entity("Error: Missing required data: checkId")
.build();
}

String userId = AuthUtils.getUserId(identity);
Optional<EligibilityCheck> checkOpt = eligibilityCheckRepository.getWorkingCustomCheck(userId, checkId);
if (checkOpt.isEmpty()){
return Response.status(Response.Status.NOT_FOUND).build();
}

EligibilityCheck check = checkOpt.get();
if (!check.getOwnerId().equals(userId)){
return Response.status(Response.Status.UNAUTHORIZED).build();
}

if (dmnModel == null || dmnModel.isBlank()){
return Response.ok(Map.of("errors", List.of("DMN Definition cannot be empty"))).build();
}

try {
HashMap<String, String> dmnDependenciesMap = new HashMap<String, String>();
List<String> validationErrors = dmnService.validateDmnXml(dmnModel, dmnDependenciesMap, check.getName(), check.getName());
if (!validationErrors.isEmpty()) {
validationErrors = validationErrors.stream()
.map(error -> error.replaceAll("\\(.*?\\)", ""))
.collect(java.util.stream.Collectors.toList());

return Response.ok(Map.of("errors", validationErrors)).build();
}

return Response.ok(Map.of("errors", List.of())).build();
} catch (Exception e){
Log.info(("Failed to save DMN model for check " + checkId));
return Response.status(Response.Status.INTERNAL_SERVER_ERROR).build();
}
}

// By default, returns the most recent versions of all published checks owned by the calling user
// If the query parameter 'working' is set to true,
// then all the working check objects owned by the user are returned
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package org.acme.model.dto;

public class SaveDmnRequest {
public class CheckDmnRequest {
public String id;
public String dmnModel;
}
7 changes: 7 additions & 0 deletions builder-api/src/main/java/org/acme/service/DmnService.java
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
package org.acme.service;
import org.acme.enums.OptionalBoolean;

import java.util.List;
import java.util.Map;

public interface DmnService {
public List<String> validateDmnXml(
String dmnXml,
Map<String, String> dependenciesMap,
String modelId,
String requiredBooleanDecisionName
) throws Exception;
public OptionalBoolean evaluateDmn(
String dmnFilePath,
String dmnModelName,
Expand Down
77 changes: 67 additions & 10 deletions builder-api/src/main/java/org/acme/service/KieDmnService.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,23 @@
import org.kie.api.runtime.KieContainer;
import org.kie.api.runtime.KieSession;
import org.kie.dmn.api.core.*;
import org.kie.dmn.api.core.ast.DecisionNode;

import java.io.*;
import java.util.*;
import org.drools.compiler.kie.builder.impl.InternalKieModule;


class DmnCompilationResult {
public byte[] dmnBytes;
public List<String> errors;

public DmnCompilationResult(byte[] dmnBytes, List<String> errors) {
this.dmnBytes = dmnBytes;
this.errors = errors;
}
}

@ApplicationScoped
public class KieDmnService implements DmnService {
@Inject
Expand All @@ -31,7 +43,46 @@ private KieSession initializeKieSession(byte[] moduleBytes) throws IOException {
return kieContainer.newKieSession();
}

private byte[] compileDmnModel(String dmnXml, Map<String, String> dependenciesMap, String modelId) throws IOException {
// Validates that the DMN XML can compile and contains the required decision.
// Returns a list of error messages if any issues are found.
public List<String> validateDmnXml (
String dmnXml, Map<String, String> dependenciesMap, String modelId, String requiredBooleanDecisionName
) throws Exception {
DmnCompilationResult compilationResult = compileDmnModel(dmnXml, dependenciesMap, modelId);
if (!compilationResult.errors.isEmpty()) {
return compilationResult.errors;
}

KieSession kieSession = initializeKieSession(compilationResult.dmnBytes);
DMNRuntime dmnRuntime = kieSession.getKieRuntime(DMNRuntime.class);

List<DMNModel> dmnModels = dmnRuntime.getModels();
if (dmnModels.size() != 1) {
return List.of("Expected exactly one DMN model, found: " + dmnModels.size());
}

DMNModel dmnModel = dmnModels.get(0);
DecisionNode requiredBooleanDecision = dmnModel.getDecisions().stream()
.filter(d -> d.getName().equals(requiredBooleanDecisionName))
.findFirst()
.orElse(null);
if (requiredBooleanDecision == null) {
List<String> decisionNames = dmnModel.getDecisions().stream()
.map(DecisionNode::getName)
.toList();
return List.of(
"Required Decision '" + requiredBooleanDecisionName + "' not found in DMN definition. " +
"Decisions found: " + decisionNames
);
}

if (requiredBooleanDecision.getResultType().getName() != "boolean") {
return List.of("The Result DataType of Decision '" + requiredBooleanDecisionName + "' must be of type 'boolean'.");
}
return new ArrayList<String>();
}

private DmnCompilationResult compileDmnModel(String dmnXml, Map<String, String> dependenciesMap, String modelId) {
Log.info("Compiling and saving DMN model: " + modelId);

KieServices kieServices = KieServices.Factory.get();
Expand Down Expand Up @@ -69,18 +120,17 @@ private byte[] compileDmnModel(String dmnXml, Map<String, String> dependenciesMa
Results results = kieBuilder.getResults();

if (results.hasMessages(Message.Level.ERROR)) {
Log.error("DMN Compilation errors for model " + modelId + ":");
for (Message message : results.getMessages(Message.Level.ERROR)) {
Log.error(message.getText());
}
throw new IllegalStateException("DMN Model compilation failed for model: " + modelId);
return new DmnCompilationResult(
null,
results.getMessages(Message.Level.ERROR).stream().map(Message::getText).toList()
);
}

InternalKieModule kieModule = (InternalKieModule) kieBuilder.getKieModule();
byte[] kieModuleBytes = kieModule.getBytes();

Log.info("Serialized kieModule for model " + modelId);
return kieModuleBytes;
return new DmnCompilationResult(kieModuleBytes, new ArrayList<String>());
}

public OptionalBoolean evaluateDmn(
Expand All @@ -95,12 +145,19 @@ public OptionalBoolean evaluateDmn(
if (dmnXmlOpt.isEmpty()) {
throw new RuntimeException("DMN file not found: " + dmnFilePath);
}

String dmnXml = dmnXmlOpt.get();

HashMap<String, String> dmnDependenciesMap = new HashMap<String, String>();
byte[] serializedModel = compileDmnModel(dmnXml, dmnDependenciesMap, dmnModelName);
DmnCompilationResult compilationResult = compileDmnModel(dmnXml, dmnDependenciesMap, dmnModelName);
if (!compilationResult.errors.isEmpty()) {
Log.error("DMN Compilation errors for model " + dmnModelName + ":");
for (String error : compilationResult.errors) {
Log.error(error);
}
throw new IllegalStateException("DMN Model compilation failed for model: " + dmnModelName);
}

KieSession kieSession = initializeKieSession(serializedModel);
KieSession kieSession = initializeKieSession(compilationResult.dmnBytes);
DMNRuntime dmnRuntime = kieSession.getKieRuntime(DMNRuntime.class);

List<DMNModel> dmnModels = dmnRuntime.getModels();
Expand Down
24 changes: 24 additions & 0 deletions builder-frontend/src/api/check.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,30 @@ export const saveCheckDmn = async (checkId: string, dmnModel: string) => {
}
};

export const validateCheckDmn = async (checkId: string, dmnModel: string): Promise<string[]> => {
const url = apiUrl + "/validate-check-dmn";
try {
const response = await authFetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
body: JSON.stringify({ id: checkId, dmnModel: dmnModel }),
});

if (!response.ok) {
throw new Error(`Validation failed with status: ${response.status}`);
}

const data = await response.json();
return data.errors;
} catch (error) {
console.error("Error validation DMN for check:", error);
throw error; // rethrow so you can handle it in your component if needed
}
};

export const fetchUserDefinedChecks = async (
working: boolean
): Promise<EligibilityCheck[]> => {
Expand Down
Loading