Skip to content

Commit 15d65f7

Browse files
Merge pull request #209 from CodeForPhilly/helpful-check-dmn
2 parents dc93fc5 + 585f0c6 commit 15d65f7

File tree

18 files changed

+488
-262
lines changed

18 files changed

+488
-262
lines changed

builder-api/src/main/java/org/acme/controller/EligibilityCheckResource.java

Lines changed: 55 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,12 @@
1010
import org.acme.auth.AuthUtils;
1111
import org.acme.constants.CheckStatus;
1212
import org.acme.model.domain.EligibilityCheck;
13-
import org.acme.model.dto.SaveDmnRequest;
13+
import org.acme.model.dto.CheckDmnRequest;
1414
import org.acme.persistence.EligibilityCheckRepository;
1515
import org.acme.persistence.StorageService;
16+
import org.acme.service.DmnService;
1617

18+
import java.util.HashMap;
1719
import java.util.List;
1820
import java.util.Map;
1921
import java.util.Optional;
@@ -27,6 +29,9 @@ public class EligibilityCheckResource {
2729
@Inject
2830
StorageService storageService;
2931

32+
@Inject
33+
DmnService dmnService;
34+
3035
@GET
3136
@Path("/checks")
3237
public Response getPublicChecks(@Context SecurityIdentity identity) {
@@ -105,7 +110,7 @@ public Response updatePublicCheck(@Context SecurityIdentity identity,
105110
@POST
106111
@Consumes(MediaType.APPLICATION_JSON)
107112
@Path("/save-check-dmn")
108-
public Response updateCheckDmn(@Context SecurityIdentity identity, SaveDmnRequest saveDmnRequest){
113+
public Response updateCheckDmn(@Context SecurityIdentity identity, CheckDmnRequest saveDmnRequest){
109114
String checkId = saveDmnRequest.id;
110115
String dmnModel = saveDmnRequest.dmnModel;
111116
if (checkId == null || checkId.isBlank()){
@@ -121,17 +126,10 @@ public Response updateCheckDmn(@Context SecurityIdentity identity, SaveDmnReques
121126
}
122127

123128
EligibilityCheck check = checkOpt.get();
124-
125-
//AUTHORIZATION
126-
// if (!check.getOwnerId().equals(userId)){
127-
// return Response.status(Response.Status.UNAUTHORIZED).build();
128-
// }
129-
130-
if (dmnModel == null){
131-
return Response.status(Response.Status.BAD_REQUEST)
132-
.entity("Error: Missing required data: DMN Model")
133-
.build();
129+
if (!check.getOwnerId().equals(userId)){
130+
return Response.status(Response.Status.UNAUTHORIZED).build();
134131
}
132+
135133
try {
136134
String filePath = storageService.getCheckDmnModelPath(checkId);
137135
storageService.writeStringToStorage(filePath, dmnModel, "application/xml");
@@ -147,6 +145,51 @@ public Response updateCheckDmn(@Context SecurityIdentity identity, SaveDmnReques
147145
}
148146
}
149147

148+
@POST
149+
@Consumes(MediaType.APPLICATION_JSON)
150+
@Path("/validate-check-dmn")
151+
public Response validateCheckDmn(@Context SecurityIdentity identity, CheckDmnRequest validateDmnRequest){
152+
String checkId = validateDmnRequest.id;
153+
String dmnModel = validateDmnRequest.dmnModel;
154+
if (checkId == null || checkId.isBlank()){
155+
return Response.status(Response.Status.BAD_REQUEST)
156+
.entity("Error: Missing required data: checkId")
157+
.build();
158+
}
159+
160+
String userId = AuthUtils.getUserId(identity);
161+
Optional<EligibilityCheck> checkOpt = eligibilityCheckRepository.getWorkingCustomCheck(userId, checkId);
162+
if (checkOpt.isEmpty()){
163+
return Response.status(Response.Status.NOT_FOUND).build();
164+
}
165+
166+
EligibilityCheck check = checkOpt.get();
167+
if (!check.getOwnerId().equals(userId)){
168+
return Response.status(Response.Status.UNAUTHORIZED).build();
169+
}
170+
171+
if (dmnModel == null || dmnModel.isBlank()){
172+
return Response.ok(Map.of("errors", List.of("DMN Definition cannot be empty"))).build();
173+
}
174+
175+
try {
176+
HashMap<String, String> dmnDependenciesMap = new HashMap<String, String>();
177+
List<String> validationErrors = dmnService.validateDmnXml(dmnModel, dmnDependenciesMap, check.getName(), check.getName());
178+
if (!validationErrors.isEmpty()) {
179+
validationErrors = validationErrors.stream()
180+
.map(error -> error.replaceAll("\\(.*?\\)", ""))
181+
.collect(java.util.stream.Collectors.toList());
182+
183+
return Response.ok(Map.of("errors", validationErrors)).build();
184+
}
185+
186+
return Response.ok(Map.of("errors", List.of())).build();
187+
} catch (Exception e){
188+
Log.info(("Failed to save DMN model for check " + checkId));
189+
return Response.status(Response.Status.INTERNAL_SERVER_ERROR).build();
190+
}
191+
}
192+
150193
// By default, returns the most recent versions of all published checks owned by the calling user
151194
// If the query parameter 'working' is set to true,
152195
// then all the working check objects owned by the user are returned
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
package org.acme.model.dto;
22

3-
public class SaveDmnRequest {
3+
public class CheckDmnRequest {
44
public String id;
55
public String dmnModel;
66
}

builder-api/src/main/java/org/acme/service/DmnService.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,16 @@
11
package org.acme.service;
22
import org.acme.enums.OptionalBoolean;
33

4+
import java.util.List;
45
import java.util.Map;
56

67
public interface DmnService {
8+
public List<String> validateDmnXml(
9+
String dmnXml,
10+
Map<String, String> dependenciesMap,
11+
String modelId,
12+
String requiredBooleanDecisionName
13+
) throws Exception;
714
public OptionalBoolean evaluateDmn(
815
String dmnFilePath,
916
String dmnModelName,

builder-api/src/main/java/org/acme/service/KieDmnService.java

Lines changed: 67 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,23 @@
1111
import org.kie.api.runtime.KieContainer;
1212
import org.kie.api.runtime.KieSession;
1313
import org.kie.dmn.api.core.*;
14+
import org.kie.dmn.api.core.ast.DecisionNode;
15+
1416
import java.io.*;
1517
import java.util.*;
1618
import org.drools.compiler.kie.builder.impl.InternalKieModule;
1719

1820

21+
class DmnCompilationResult {
22+
public byte[] dmnBytes;
23+
public List<String> errors;
24+
25+
public DmnCompilationResult(byte[] dmnBytes, List<String> errors) {
26+
this.dmnBytes = dmnBytes;
27+
this.errors = errors;
28+
}
29+
}
30+
1931
@ApplicationScoped
2032
public class KieDmnService implements DmnService {
2133
@Inject
@@ -31,7 +43,46 @@ private KieSession initializeKieSession(byte[] moduleBytes) throws IOException {
3143
return kieContainer.newKieSession();
3244
}
3345

34-
private byte[] compileDmnModel(String dmnXml, Map<String, String> dependenciesMap, String modelId) throws IOException {
46+
// Validates that the DMN XML can compile and contains the required decision.
47+
// Returns a list of error messages if any issues are found.
48+
public List<String> validateDmnXml (
49+
String dmnXml, Map<String, String> dependenciesMap, String modelId, String requiredBooleanDecisionName
50+
) throws Exception {
51+
DmnCompilationResult compilationResult = compileDmnModel(dmnXml, dependenciesMap, modelId);
52+
if (!compilationResult.errors.isEmpty()) {
53+
return compilationResult.errors;
54+
}
55+
56+
KieSession kieSession = initializeKieSession(compilationResult.dmnBytes);
57+
DMNRuntime dmnRuntime = kieSession.getKieRuntime(DMNRuntime.class);
58+
59+
List<DMNModel> dmnModels = dmnRuntime.getModels();
60+
if (dmnModels.size() != 1) {
61+
return List.of("Expected exactly one DMN model, found: " + dmnModels.size());
62+
}
63+
64+
DMNModel dmnModel = dmnModels.get(0);
65+
DecisionNode requiredBooleanDecision = dmnModel.getDecisions().stream()
66+
.filter(d -> d.getName().equals(requiredBooleanDecisionName))
67+
.findFirst()
68+
.orElse(null);
69+
if (requiredBooleanDecision == null) {
70+
List<String> decisionNames = dmnModel.getDecisions().stream()
71+
.map(DecisionNode::getName)
72+
.toList();
73+
return List.of(
74+
"Required Decision '" + requiredBooleanDecisionName + "' not found in DMN definition. " +
75+
"Decisions found: " + decisionNames
76+
);
77+
}
78+
79+
if (requiredBooleanDecision.getResultType().getName() != "boolean") {
80+
return List.of("The Result DataType of Decision '" + requiredBooleanDecisionName + "' must be of type 'boolean'.");
81+
}
82+
return new ArrayList<String>();
83+
}
84+
85+
private DmnCompilationResult compileDmnModel(String dmnXml, Map<String, String> dependenciesMap, String modelId) {
3586
Log.info("Compiling and saving DMN model: " + modelId);
3687

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

71122
if (results.hasMessages(Message.Level.ERROR)) {
72-
Log.error("DMN Compilation errors for model " + modelId + ":");
73-
for (Message message : results.getMessages(Message.Level.ERROR)) {
74-
Log.error(message.getText());
75-
}
76-
throw new IllegalStateException("DMN Model compilation failed for model: " + modelId);
123+
return new DmnCompilationResult(
124+
null,
125+
results.getMessages(Message.Level.ERROR).stream().map(Message::getText).toList()
126+
);
77127
}
78128

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

82132
Log.info("Serialized kieModule for model " + modelId);
83-
return kieModuleBytes;
133+
return new DmnCompilationResult(kieModuleBytes, new ArrayList<String>());
84134
}
85135

86136
public OptionalBoolean evaluateDmn(
@@ -95,12 +145,19 @@ public OptionalBoolean evaluateDmn(
95145
if (dmnXmlOpt.isEmpty()) {
96146
throw new RuntimeException("DMN file not found: " + dmnFilePath);
97147
}
98-
99148
String dmnXml = dmnXmlOpt.get();
149+
100150
HashMap<String, String> dmnDependenciesMap = new HashMap<String, String>();
101-
byte[] serializedModel = compileDmnModel(dmnXml, dmnDependenciesMap, dmnModelName);
151+
DmnCompilationResult compilationResult = compileDmnModel(dmnXml, dmnDependenciesMap, dmnModelName);
152+
if (!compilationResult.errors.isEmpty()) {
153+
Log.error("DMN Compilation errors for model " + dmnModelName + ":");
154+
for (String error : compilationResult.errors) {
155+
Log.error(error);
156+
}
157+
throw new IllegalStateException("DMN Model compilation failed for model: " + dmnModelName);
158+
}
102159

103-
KieSession kieSession = initializeKieSession(serializedModel);
160+
KieSession kieSession = initializeKieSession(compilationResult.dmnBytes);
104161
DMNRuntime dmnRuntime = kieSession.getKieRuntime(DMNRuntime.class);
105162

106163
List<DMNModel> dmnModels = dmnRuntime.getModels();

builder-frontend/src/api/check.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,30 @@ export const saveCheckDmn = async (checkId: string, dmnModel: string) => {
140140
}
141141
};
142142

143+
export const validateCheckDmn = async (checkId: string, dmnModel: string): Promise<string[]> => {
144+
const url = apiUrl + "/validate-check-dmn";
145+
try {
146+
const response = await authFetch(url, {
147+
method: "POST",
148+
headers: {
149+
"Content-Type": "application/json",
150+
Accept: "application/json",
151+
},
152+
body: JSON.stringify({ id: checkId, dmnModel: dmnModel }),
153+
});
154+
155+
if (!response.ok) {
156+
throw new Error(`Validation failed with status: ${response.status}`);
157+
}
158+
159+
const data = await response.json();
160+
return data.errors;
161+
} catch (error) {
162+
console.error("Error validation DMN for check:", error);
163+
throw error; // rethrow so you can handle it in your component if needed
164+
}
165+
};
166+
143167
export const fetchUserDefinedChecks = async (
144168
working: boolean
145169
): Promise<EligibilityCheck[]> => {

0 commit comments

Comments
 (0)