Skip to content

Commit 24e131b

Browse files
Merge pull request #77 from datasharingframework/issue/71-maintain-dsf-linter-improve-log-messages-and-review-bpmn-types
Issue/71 maintain dsf linter improve log messages and review bpmn types
2 parents 2993bec + 2b67723 commit 24e131b

22 files changed

Lines changed: 752 additions & 133 deletions

linter-core/pom.xml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,15 @@
101101
<version>2.22.0</version>
102102
</dependency>
103103

104+
<!-- Jakarta JAXB API 3.x+ (jakarta.xml.bind.*); required when Thymeleaf/Jackson
105+
auto-registers jackson-module-jakarta-xmlbind (Java 11+ has no JAXB in the JDK).
106+
Camunda's transitive jakarta.xml.bind-api 2.3.3 only exposes javax.xml.bind.*. -->
107+
<dependency>
108+
<groupId>jakarta.xml.bind</groupId>
109+
<artifactId>jakarta.xml.bind-api</artifactId>
110+
<version>4.0.2</version>
111+
</dependency>
112+
104113
<dependency>
105114
<groupId>org.jetbrains</groupId>
106115
<artifactId>annotations</artifactId>

linter-core/src/main/java/dev/dsf/linter/bpmn/BpmnEventLinter.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package dev.dsf.linter.bpmn;
22

33
import dev.dsf.linter.output.item.BpmnElementLintItem;
4+
import dev.dsf.linter.util.bpmn.BpmnModelUtils;
45
import dev.dsf.linter.util.bpmn.linters.*;
56
import org.camunda.bpm.model.bpmn.instance.*;
67

@@ -193,8 +194,7 @@ public void lintStartEvent(
193194
File bpmnFile,
194195
String processId) {
195196

196-
if (!startEvent.getEventDefinitions().isEmpty()
197-
&& startEvent.getEventDefinitions().iterator().next() instanceof MessageEventDefinition) {
197+
if (BpmnModelUtils.isMessageStartEvent(startEvent)) {
198198
BpmnStartEventLinter.lintMessageStartEvent(startEvent, issues, bpmnFile, processId, projectRoot);
199199
} else {
200200
BpmnStartEventLinter.lintGenericStartEvent(startEvent, issues, bpmnFile, processId, projectRoot);

linter-core/src/main/java/dev/dsf/linter/bpmn/BpmnFieldInjectionLinter.java

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
import dev.dsf.linter.output.LinterSeverity;
44
import dev.dsf.linter.output.LintingType;
55
import dev.dsf.linter.output.item.*;
6+
import dev.dsf.linter.util.api.ApiVersion;
7+
import dev.dsf.linter.util.api.ApiVersionHolder;
68
import dev.dsf.linter.util.resource.FhirResourceExtractor;
79
import dev.dsf.linter.util.resource.FhirResourceLocator;
810
import dev.dsf.linter.util.resource.FhirResourceParser;
@@ -47,11 +49,11 @@
4749
*
4850
* <h3>Field Value Type Validation</h3>
4951
* <ul>
50-
* <li><strong>String Literal Requirement</strong>: Validates that field values are provided as
52+
* <li><strong>String Literal Requirement (API v1)</strong>: Validates that field values are provided as
5153
* string literals rather than expressions, ensuring static configuration that can be validated
5254
* at linting time</li>
53-
* <li><strong>Expression Detection</strong>: Issues errors when field values are provided as
54-
* expressions, which cannot be statically validated</li>
55+
* <li><strong>Expression Detection</strong>: For API v1, issues errors when field values are provided as
56+
* expressions. For API v2, expressions are allowed and further static validation of that field is skipped</li>
5557
* </ul>
5658
*
5759
* <h3>Profile Field Validation</h3>
@@ -312,12 +314,20 @@ private static void lintCamundaFields(ExtensionElements extensionElements,
312314
"Field injection '" + fieldName + "' provided as string literal"));
313315
}
314316

315-
// expression? -> immediate error + skip further processing of this field
317+
// expression: error for API v1; allowed for API v2 (skip static field validation)
316318
if (fv != null && fv.type == FieldValueType.EXPRESSION) {
317-
issues.add(new BpmnElementLintItem(
318-
LinterSeverity.ERROR, LintingType.BPMN_FIELD_INJECTION_NOT_STRING_LITERAL,
319-
elementId, bpmnFile, processId,
320-
"Field injection '" + fieldName + "' is provided as expression, expected string literal")); //todo in api v2 is allowed
319+
if (ApiVersionHolder.getVersion() == ApiVersion.V2) {
320+
issues.add(BpmnElementLintItem.success(
321+
elementId,
322+
bpmnFile,
323+
processId,
324+
"Field injection '" + fieldName + "' provided as expression (allowed for API v2)"));
325+
} else {
326+
issues.add(new BpmnElementLintItem(
327+
LinterSeverity.ERROR, LintingType.BPMN_FIELD_INJECTION_NOT_STRING_LITERAL,
328+
elementId, bpmnFile, processId,
329+
"Field injection '" + fieldName + "' is provided as expression, expected string literal"));
330+
}
321331
continue;
322332
}
323333

@@ -350,7 +360,7 @@ private static void lintCamundaFields(ExtensionElements extensionElements,
350360
}
351361
}
352362
case "instantiatesCanonical" -> {
353-
checkInstantiatesCanonicalField(elementId, literal, bpmnFile, processId, issues, projectRoot);
363+
checkInstantiatesCanonicalField(elementId, literal, bpmnFile, processId, issues);
354364
instantiatesVal = literal;
355365
}
356366
default -> issues.add(new BpmnElementLintItem(
@@ -515,15 +525,13 @@ public static void checkProfileField(
515525
* @param bpmnFile the BPMN file under lint
516526
* @param processId the identifier of the BPMN process containing the element
517527
* @param issues the list of {@link BpmnElementLintItem} to which lint issues or success items will be added
518-
* @param projectRoot the project root directory containing FHIR resources
519528
*/
520529
public static void checkInstantiatesCanonicalField(
521530
String elementId,
522531
String literalValue,
523532
File bpmnFile,
524533
String processId,
525-
List<BpmnElementLintItem> issues,
526-
File projectRoot) {
534+
List<BpmnElementLintItem> issues) {
527535

528536
if (isEmpty(literalValue)) {
529537
issues.add(new BpmnElementLintItem(LinterSeverity.ERROR,

linter-core/src/main/java/dev/dsf/linter/bpmn/BpmnGatewayAndFlowLinter.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,7 @@ public void lintInclusiveGateway(
177177
if (gateway.getOutgoing() != null && gateway.getOutgoing().size() > 1) {
178178
if (isEmpty(gateway.getName())) {
179179
issues.add(BpmnElementLintItem.of(
180-
LinterSeverity.ERROR,
180+
LinterSeverity.WARN,
181181
LintingType.BPMN_INCLUSIVE_GATEWAY_HAS_MULTIPLE_OUTGOING_FLOWS_BUT_NAME_IS_EMPTY,
182182
elementId, bpmnFile, processId));
183183
} else {

linter-core/src/main/java/dev/dsf/linter/bpmn/BpmnModelLinter.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@
4646
* <li><strong>Process Count</strong>: Validates that each BPMN file contains exactly one process definition</li>
4747
* <li><strong>History Time To Live</strong>: Warns if {@code camunda:historyTimeToLive} is not set</li>
4848
* <li><strong>Process Executable</strong>: Validates that the process has {@code isExecutable="true"}</li>
49+
* <li><strong>Message Start Event</strong>: Validates that the process has at least one message start event
50+
* as a direct child (start events in subprocesses are not counted)</li>
4951
* </ul>
5052
*
5153
* <h3>Task Validation</h3>

linter-core/src/main/java/dev/dsf/linter/bpmn/BpmnProcessLinter.java

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@
33
import dev.dsf.linter.output.LinterSeverity;
44
import dev.dsf.linter.output.LintingType;
55
import dev.dsf.linter.output.item.BpmnElementLintItem;
6+
import dev.dsf.linter.util.bpmn.BpmnModelUtils;
67
import org.camunda.bpm.model.bpmn.BpmnModelInstance;
78
import org.camunda.bpm.model.bpmn.instance.Process;
9+
import org.camunda.bpm.model.bpmn.instance.StartEvent;
810

911
import java.io.File;
1012
import java.util.Collection;
@@ -58,6 +60,12 @@
5860
* which is required for the process to be deployable and executable by the process engine.</li>
5961
* </ul>
6062
*
63+
* <h3>Message Start Event Validation</h3>
64+
* <ul>
65+
* <li><strong>Message Start Requirement</strong>: Validates that the process has at least one message
66+
* start event as a direct child of the process element. Start events inside subprocesses are not counted.</li>
67+
* </ul>
68+
*
6169
* <h2>Usage Example</h2>
6270
* <pre>{@code
6371
* File projectRoot = new File("/path/to/project");
@@ -134,6 +142,7 @@ public record BpmnProcessLinter(File projectRoot) {
134142
* <li>Process ID pattern</li>
135143
* <li>History time to live attribute</li>
136144
* <li>Executable flag</li>
145+
* <li>Message start event on process level (not in subprocesses)</li>
137146
* <li>Version tag placeholder ({@code camunda:versionTag="#{version}"})</li>
138147
* </ul>
139148
* </p>
@@ -153,6 +162,7 @@ public String lintProcesses(BpmnModelInstance model, File bpmnFile, List<BpmnEle
153162
for (Process process : processes) {
154163
validateHistoryTimeToLive(process, bpmnFile, issues);
155164
validateProcessExecutable(process, bpmnFile, issues);
165+
validateMessageStartEvent(process, bpmnFile, issues);
156166
validateVersionTag(process, bpmnFile, issues);
157167
}
158168

@@ -362,6 +372,42 @@ void validateProcessExecutable(Process process, File bpmnFile, List<BpmnElementL
362372
}
363373
}
364374

375+
/**
376+
* Validates that the process has at least one message start event as a direct child.
377+
*
378+
* <p>
379+
* DSF processes are started via incoming messages. Start events inside subprocesses are not counted.
380+
* </p>
381+
*
382+
* @param process the BPMN process to validate
383+
* @param bpmnFile the BPMN file for error reporting
384+
* @param issues the list to add validation issues to
385+
*/
386+
void validateMessageStartEvent(Process process, File bpmnFile, List<BpmnElementLintItem> issues) {
387+
String processId = process.getId() != null ? process.getId() : "";
388+
389+
boolean hasMessageStart = process.getChildElementsByType(StartEvent.class).stream()
390+
.anyMatch(BpmnModelUtils::isMessageStartEvent);
391+
392+
if (!hasMessageStart) {
393+
issues.add(BpmnElementLintItem.of(
394+
LinterSeverity.ERROR,
395+
LintingType.BPMN_MESSAGE_START_EVENT_NOT_FOUND,
396+
processId,
397+
bpmnFile,
398+
processId));
399+
} else {
400+
issues.add(new BpmnElementLintItem(
401+
LinterSeverity.SUCCESS,
402+
LintingType.SUCCESS,
403+
processId,
404+
bpmnFile,
405+
processId,
406+
String.format("Process '%s': has at least one message start event on process level.", processId)
407+
));
408+
}
409+
}
410+
365411
/**
366412
* Validates that camunda:versionTag exists and uses the DSF placeholder "#{version}".
367413
*

linter-core/src/main/java/dev/dsf/linter/bpmn/BpmnTaskLinter.java

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,7 @@ public void lintServiceTask(
197197

198198
// 1. Validate task name
199199
if (isEmpty(task.getName())) {
200-
issues.add(BpmnElementLintItem.of(LinterSeverity.ERROR,
200+
issues.add(BpmnElementLintItem.of(LinterSeverity.WARN,
201201
LintingType.BPMN_SERVICE_TASK_NAME_EMPTY, elementId, bpmnFile, processId));
202202
} else {
203203
issues.add(BpmnElementLintItem.success(elementId, bpmnFile, processId,
@@ -282,9 +282,8 @@ public void lintSendTask(
282282

283283
// 1. Validate task name
284284
if (isEmpty(sendTask.getName())) {
285-
issues.add(new BpmnElementLintItem(LinterSeverity.WARN,
286-
LintingType.BPMN_EVENT_NAME_EMPTY, elementId, bpmnFile, processId,
287-
"'" + elementId + "' has no name"));
285+
issues.add(BpmnElementLintItem.of(LinterSeverity.WARN,
286+
LintingType.BPMN_SEND_TASK_NAME_EMPTY, elementId, bpmnFile, processId));
288287
} else {
289288
issues.add(BpmnElementLintItem.success(elementId, bpmnFile, processId,
290289
"SendTask has a non-empty name: '" + sendTask.getName() + "'"));
@@ -357,7 +356,7 @@ public void lintUserTask(
357356

358357
// 1. Validate task name
359358
if (isEmpty(userTask.getName())) {
360-
issues.add(BpmnElementLintItem.of(LinterSeverity.ERROR,
359+
issues.add(BpmnElementLintItem.of(LinterSeverity.WARN,
361360
LintingType.BPMN_USER_TASK_NAME_EMPTY, elementId, bpmnFile, processId));
362361
} else {
363362
issues.add(BpmnElementLintItem.success(elementId, bpmnFile, processId,
@@ -409,17 +408,16 @@ public void lintReceiveTask(
409408
String elementId = receiveTask.getId();
410409

411410
if (isEmpty(receiveTask.getName())) {
412-
issues.add(new BpmnElementLintItem(LinterSeverity.WARN,
413-
LintingType.BPMN_EVENT_NAME_EMPTY, elementId, bpmnFile, processId,
414-
"'" + elementId + "' has no name."));
411+
issues.add(BpmnElementLintItem.of(LinterSeverity.WARN,
412+
LintingType.BPMN_RECEIVE_TASK_NAME_EMPTY, elementId, bpmnFile, processId));
415413
} else {
416414
issues.add(BpmnElementLintItem.success(elementId, bpmnFile, processId,
417415
"ReceiveTask has a non-empty name: '" + receiveTask.getName() + "'"));
418416
}
419417

420418
if (receiveTask.getMessage() == null || isEmpty(receiveTask.getMessage().getName())) {
421419
issues.add(BpmnElementLintItem.of(LinterSeverity.ERROR,
422-
LintingType.BPMN_MESSAGE_START_EVENT_MESSAGE_NAME_EMPTY, elementId, bpmnFile, processId));
420+
LintingType.BPMN_RECEIVE_TASK_MESSAGE_NAME_EMPTY, elementId, bpmnFile, processId));
423421
} else {
424422
String msgName = receiveTask.getMessage().getName();
425423
issues.add(BpmnElementLintItem.success(elementId, bpmnFile, processId,

linter-core/src/main/java/dev/dsf/linter/output/LintingType.java

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -34,24 +34,31 @@ public enum LintingType {
3434
BPMN_MESSAGE_SEND_TASK_IMPLEMENTATION_CLASS_NOT_FOUND("Message send task implementation class not found."),
3535

3636
// ==================== BPMN SEND TASK ====================
37+
BPMN_SEND_TASK_NAME_EMPTY("Send task name is empty."),
3738
BPMN_SEND_TASK_NO_INTERFACE_CLASS_IMPLEMENTING("Send task implementation class does not implement required interface."),
3839
BPMN_SEND_TASK_IMPLEMENTATION_CLASS_NOT_EXTENDING_ABSTRACT_TASK_MESSAGE_SEND("Send task implementation class does not extend AbstractTaskMessageSend."),
3940

41+
// ==================== BPMN RECEIVE TASK ====================
42+
BPMN_RECEIVE_TASK_NAME_EMPTY("Receive task name is empty."),
43+
BPMN_RECEIVE_TASK_MESSAGE_NAME_EMPTY("Receive task message name is empty."),
44+
4045
// ==================== BPMN FLOW ====================
4146
BPMN_MESSAGE_START_EVENT_NOT_FOUND("Message start event not found."),
47+
BPMN_MESSAGE_START_EVENT_NAME_EMPTY("Message start event name is empty."),
4248
BPMN_MESSAGE_START_EVENT_MESSAGE_NAME_EMPTY("Message start event message name is empty."),
4349
BPMN_FLOATING_ELEMENT("BPMN element is outside of message start event triggered flow."),
4450
BPMN_FLOW_ELEMENT("BPMN flow element issue."),
4551

4652
// ==================== BPMN EVENTS ====================
47-
BPMN_EVENT_NAME_EMPTY("Event name is empty."),
53+
BPMN_MESSAGE_END_EVENT_NAME_EMPTY("Message end event name is empty."),
54+
BPMN_MESSAGE_INTERMEDIATE_THROW_EVENT_NAME_EMPTY("Message intermediate throw event name is empty."),
4855
BPMN_ERROR_BOUNDARY_EVENT_ERROR_CODE_EMPTY("Error boundary event error code is empty."),
4956
BPMN_ERROR_BOUNDARY_EVENT_ERROR_CODE_VARIABLE_EMPTY("Error boundary event error code variable is empty."),
5057
BPMN_ERROR_BOUNDARY_EVENT_ERROR_NAME_EMPTY("Error boundary event error name is empty."),
5158
BPMN_ERROR_BOUNDARY_EVENT_NAME_EMPTY("Error boundary event name is empty."),
52-
BPMN_MESSAGE_INTERMEDIATE_THROW_EVENT_HAS_MESSAGE("Message intermediate throw event has message."),
53-
BPMN_START_EVENT_NOT_PART_OF_SUB_PROCESS("Start event is not part of subprocess."),
54-
BPMN_END_EVENT_NOT_PART_OF_SUB_PROCESS_AND_NAME_IS_EMPTY("End event is not part of subprocess and name is empty."),
59+
BPMN_MESSAGE_INTERMEDIATE_THROW_EVENT_HAS_MESSAGE_REFERENCE("Message intermediate throw event has message reference."),
60+
BPMN_START_EVENT_NOT_PART_OF_SUB_PROCESS_AND_HAS_NO_NAME("Start event is not part of subprocess and has no name."),
61+
BPMN_END_EVENT_NOT_PART_OF_SUB_PROCESS_AND_HAS_NO_NAME("End event has no name and is not part of subprocess."),
5562
BPMN_MESSAGE_INTERMEDIATE_CATCH_EVENT_NAME_EMPTY("Message intermediate catch event name is empty."),
5663
BPMN_MESSAGE_INTERMEDIATE_CATCH_EVENT_MESSAGE_NAME_EMPTY("Message intermediate catch event message name is empty."),
5764
BPMN_MESSAGE_BOUNDARY_EVENT_NAME_EMPTY("Message boundary event name is empty."),
@@ -62,6 +69,9 @@ public enum LintingType {
6269
BPMN_SIGNAL_END_EVENT_SIGNAL_EMPTY("Signal end event signal is empty."),
6370
BPMN_END_EVENT_INSIDE_SUB_PROCESS_SHOULD_HAVE_ASYNC_AFTER_TRUE("End event inside subprocess should have asyncAfter=true."),
6471
BPMN_END_EVENT_NO_INTERFACE_CLASS_IMPLEMENTING("End event implementation class does not implement required interface."),
72+
BPMN_INTERMEDIATE_THROW_EVENT_NO_INTERFACE_CLASS_IMPLEMENTING("Intermediate throw event implementation class does not implement required interface."),
73+
BPMN_MESSAGE_END_EVENT_IMPLEMENTATION_CLASS_NOT_EXTENDING_ABSTRACT_TASK_MESSAGE_SEND("Message end event implementation class does not extend AbstractTaskMessageSend."),
74+
BPMN_MESSAGE_INTERMEDIATE_THROW_EVENT_IMPLEMENTATION_CLASS_NOT_EXTENDING_ABSTRACT_TASK_MESSAGE_SEND("Message intermediate throw event implementation class does not extend AbstractTaskMessageSend."),
6575

6676
// ==================== BPMN GATEWAYS ====================
6777
BPMN_EXCLUSIVE_GATEWAY_HAS_MULTIPLE_OUTGOING_FLOWS_BUT_NAME_IS_EMPTY("Exclusive gateway has multiple outgoing flows but name is empty."),
@@ -78,6 +88,10 @@ public enum LintingType {
7888
BPMN_USER_TASK_LISTENER_TASK_OUTPUT_CODE_INVALID_FHIR_RESOURCE("User task listener task output code references invalid FHIR resource."),
7989
BPMN_USER_TASK_LISTENER_TASK_OUTPUT_SYSTEM_INVALID_FHIR_RESOURCE("User task listener task output system references invalid FHIR resource."),
8090
BPMN_USER_TASK_LISTENER_TASK_OUTPUT_VERSION_NO_PLACEHOLDER("User task listener task output version does not use placeholder."),
91+
BPMN_USER_TASK_LISTENER_PRACTITIONER_ROLE_INPUT_EMPTY(
92+
"User task listener input parameter 'practitionerRole' is defined but has no value."),
93+
BPMN_USER_TASK_LISTENER_PRACTITIONERS_INPUT_EMPTY(
94+
"User task listener input parameter 'practitioners' is defined but has no value."),
8195

8296
// ==================== BPMN FIELD INJECTION ====================
8397
BPMN_FIELD_INJECTION_NOT_STRING_LITERAL("Field injection value is not a string literal."),
@@ -93,7 +107,7 @@ public enum LintingType {
93107

94108
// ==================== BPMN EXECUTION LISTENER ====================
95109
BPMN_EXECUTION_LISTENER_CLASS_NOT_FOUND("Execution listener class not found."),
96-
BPMN_EXECUTION_LISTENER_NOT_IMPLEMENTING_REQUIRED_INTERFACE("Execution listener does not implement required interface."),
110+
BPMN_EXECUTION_LISTENER_CLASS_NOT_IMPLEMENTING_REQUIRED_INTERFACE("Execution listener class does not implement required interface."),
97111

98112
// ==================== BPMN PROCESS ====================
99113
BPMN_PROCESS_ID_PATTERN_MISMATCH("Process ID does not match required pattern: domain_processname (e.g. testorg_myprocess)."),
@@ -106,8 +120,6 @@ public enum LintingType {
106120
BPMN_PROCESS_VERSION_TAG_NO_PLACEHOLDER("Process camunda:versionTag does not use '#{version}' placeholder."),
107121

108122
// ==================== BPMN GENERAL ====================
109-
BPMN_PRACTITIONERS_HAS_NO_VALUE_OR_NULL("Practitioners has no value or is null."),
110-
BPMN_PRACTITIONER_ROLE_HAS_NO_VALUE_OR_NULL("PractitionerRole has no value or is null."),
111123
BPMN_NO_ACTIVITY_DEFINITION_FOUND_FOR_MESSAGE("No ActivityDefinition found for message."),
112124
BPMN_NO_STRUCTURE_DEFINITION_FOUND_FOR_MESSAGE("No StructureDefinition found for message."),
113125

linter-core/src/main/java/dev/dsf/linter/util/bpmn/BpmnModelUtils.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,18 @@ public static Optional<String> extractImplementationClass(BaseElement element) {
5151
return getCamundaClassFromMessageEvents(definitions);
5252
}
5353

54+
55+
/**
56+
* Returns whether the start event is triggered by a message (has a {@link MessageEventDefinition}).
57+
*
58+
* @param startEvent the BPMN start event to inspect
59+
* @return {@code true} if the start event has a message event definition
60+
*/
61+
public static boolean isMessageStartEvent(StartEvent startEvent) {
62+
return !startEvent.getEventDefinitions().isEmpty()
63+
&& startEvent.getEventDefinitions().iterator().next() instanceof MessageEventDefinition;
64+
}
65+
5466
/**
5567
* Scans a collection of event definitions to find the Camunda class from a MessageEventDefinition.
5668
*

0 commit comments

Comments
 (0)