This is an automated email from the ASF dual-hosted git repository.
pefernan pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/incubator-kie.git
The following commit(s) were added to refs/heads/main by this push:
new c2d92553980 [kie-issues#7155] Invalid Java identifier generation for
REST task params with hyphens (HEADER_content-type, QUERY_max-size) (#7158)
c2d92553980 is described below
commit c2d925539807cd87cc9ee49092754d1096f57c53
Author: Pere Fernández <[email protected]>
AuthorDate: Mon Oct 5 09:20:55 2026 +0200
[kie-issues#7155] Invalid Java identifier generation for REST task params
with hyphens (HEADER_content-type, QUERY_max-size) (#7158)
* [kie-issues#7155] Invalid Java identifier generation for REST task params
with hyphens (HEADER_content-type, QUERY_max-size)
* improve testing
* recover single variable expression cleanup to avoid unnecessary MVEL
evaluation
---
.../kogito/codegen/process/ProcessCodegenTest.java | 49 +++++++
.../workitem/RestServiceHyphenParams.bpmn2 | 94 +++++++++++++
.../org/jbpm/bpmn2/xml/AbstractNodeHandler.java | 8 +-
.../compiler/canonical/WorkItemModelMetaData.java | 30 +++--
.../canonical/ProcessToExecModelGeneratorTest.java | 62 +++++++++
.../jbpm/workflow/core/impl/IOSpecification.java | 9 +-
.../core/impl/InputExpressionAssignment.java | 24 ++--
.../workflow/core/impl/IOSpecificationTest.java | 149 +++++++++++++++++++++
.../core/impl/InputExpressionAssignmentTest.java | 123 +++++++++++++++++
.../data/BPMN2-DataInputAssociationsVariable.bpmn2 | 68 ++++++++++
.../src/test/java/org/jbpm/bpmn2/DataTest.java | 22 +++
11 files changed, 608 insertions(+), 30 deletions(-)
diff --git
a/kogito-codegen-modules/kogito-codegen-processes/src/test/java/org/kie/kogito/codegen/process/ProcessCodegenTest.java
b/kogito-codegen-modules/kogito-codegen-processes/src/test/java/org/kie/kogito/codegen/process/ProcessCodegenTest.java
index d1d930a1166..d24d83d5690 100644
---
a/kogito-codegen-modules/kogito-codegen-processes/src/test/java/org/kie/kogito/codegen/process/ProcessCodegenTest.java
+++
b/kogito-codegen-modules/kogito-codegen-processes/src/test/java/org/kie/kogito/codegen/process/ProcessCodegenTest.java
@@ -24,6 +24,7 @@ import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
+import java.util.Optional;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@@ -48,6 +49,8 @@ class ProcessCodegenTest {
private static final Path BASE_PATH =
Paths.get("src/test/resources/").toAbsolutePath();
private static final String MESSAGE_USERTASK_SOURCE =
"usertask/UserTasksProcess.bpmn2";
private static final Path MESSAGE_USERTASK_SOURCE_FULL_SOURCE =
BASE_PATH.resolve(MESSAGE_USERTASK_SOURCE);
+ private static final String REST_HYPHEN_PARAMS_SOURCE =
"workitem/RestServiceHyphenParams.bpmn2";
+ private static final Path REST_HYPHEN_PARAMS_FULL_PATH =
BASE_PATH.resolve(REST_HYPHEN_PARAMS_SOURCE);
@ParameterizedTest
@MethodSource("org.kie.kogito.codegen.api.utils.KogitoContextTestUtils#contextBuilders")
@@ -146,4 +149,50 @@ class ProcessCodegenTest {
return Stream.of(
Arguments.of(JavaKogitoBuildContext.builder()));
}
+
+ @ParameterizedTest
+
@MethodSource("org.kie.kogito.codegen.api.utils.KogitoContextTestUtils#contextBuilders")
+ public void
restTaskInputsWithHyphensAreSanitisedToValidJavaIdentifiers(KogitoBuildContext.Builder
contextBuilder) {
+ KogitoBuildContext context = contextBuilder.build();
+ ProcessCodegen codeGenerator = ProcessCodegen.ofCollectedResources(
+ context,
+ CollectedResourceProducer.fromFiles(BASE_PATH,
REST_HYPHEN_PARAMS_FULL_PATH.toFile()));
+
+ Collection<GeneratedFile> generatedFiles = codeGenerator.generate();
+ assertThat(generatedFiles).isNotEmpty();
+
+ // Find the generated TaskInput source file
+ Optional<GeneratedFile> taskInputFile = generatedFiles.stream()
+ .filter(f -> f.relativePath().contains("TaskInput"))
+ .findFirst();
+
+ assertThat(taskInputFile).as("TaskInput generated file should be
present").isPresent();
+
+ String taskInputSource = new String(taskInputFile.get().contents());
+
+ // Field / accessor names must use underscores — hyphens are not valid
Java identifiers
+ assertThat(taskInputSource)
+ .as("Sanitised field HEADER_content_type should be present")
+ .contains("HEADER_content_type");
+ assertThat(taskInputSource)
+ .as("Sanitised field QUERY_max_size should be present")
+ .contains("QUERY_max_size");
+
+ // Original hyphenated names must still appear as string literals for
the runtime map lookup
+ assertThat(taskInputSource)
+ .as("Original key \"HEADER_content-type\" must be used in
params.get()")
+ .contains("\"HEADER_content-type\"");
+ assertThat(taskInputSource)
+ .as("Original key \"QUERY_max-size\" must be used in
params.get()")
+ .contains("\"QUERY_max-size\"");
+
+ // The hyphenated names must NOT appear as identifiers (only inside
string literals)
+ String noLiterals = taskInputSource.replaceAll("\"[^\"]*\"", "\"\"");
+ assertThat(noLiterals)
+ .as("Hyphenated identifier HEADER_content-type must not appear
in generated code")
+ .doesNotContain("HEADER_content-type");
+ assertThat(noLiterals)
+ .as("Hyphenated identifier QUERY_max-size must not appear in
generated code")
+ .doesNotContain("QUERY_max-size");
+ }
}
diff --git
a/kogito-codegen-modules/kogito-codegen-processes/src/test/resources/workitem/RestServiceHyphenParams.bpmn2
b/kogito-codegen-modules/kogito-codegen-processes/src/test/resources/workitem/RestServiceHyphenParams.bpmn2
new file mode 100644
index 00000000000..130359f2a99
--- /dev/null
+++
b/kogito-codegen-modules/kogito-codegen-processes/src/test/resources/workitem/RestServiceHyphenParams.bpmn2
@@ -0,0 +1,94 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ 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.
+ -->
+<bpmn2:definitions
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xmlns:bpmn2="http://www.omg.org/spec/BPMN/20100524/MODEL"
+ xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI"
+ xmlns:dc="http://www.omg.org/spec/DD/20100524/DC"
+ xmlns:di="http://www.omg.org/spec/DD/20100524/DI"
+ xmlns:drools="http://www.jboss.org/drools"
+ id="_RestServiceHyphenParams"
+ targetNamespace="http://www.omg.org/bpmn20">
+
+ <bpmn2:itemDefinition id="_StringItem" structureRef="String"/>
+
+ <bpmn2:process id="resthyphenparms" drools:packageName="com.example"
drools:version="1.0"
+ drools:adHoc="false" name="resthyphenparms"
isExecutable="true" processType="Public">
+
+ <bpmn2:startEvent id="_Start">
+ <bpmn2:outgoing>_flow1</bpmn2:outgoing>
+ </bpmn2:startEvent>
+
+ <!-- REST work-item task with hyphenated input names (the bug reproducer)
-->
+ <bpmn2:task id="_RestTask" name="Rest Service Task" drools:taskName="Rest">
+ <bpmn2:extensionElements>
+ <drools:metaData name="elementname">
+ <drools:metaValue><![CDATA[Rest Service Task]]></drools:metaValue>
+ </drools:metaData>
+ </bpmn2:extensionElements>
+ <bpmn2:incoming>_flow1</bpmn2:incoming>
+ <bpmn2:outgoing>_flow2</bpmn2:outgoing>
+ <bpmn2:ioSpecification>
+ <bpmn2:dataInput id="_di1" itemSubjectRef="_StringItem"
name="HEADER_content-type"/>
+ <bpmn2:dataInput id="_di2" itemSubjectRef="_StringItem"
name="QUERY_max-size"/>
+ <bpmn2:inputSet>
+ <bpmn2:dataInputRefs>_di1</bpmn2:dataInputRefs>
+ <bpmn2:dataInputRefs>_di2</bpmn2:dataInputRefs>
+ </bpmn2:inputSet>
+ <bpmn2:outputSet/>
+ </bpmn2:ioSpecification>
+ <bpmn2:dataInputAssociation>
+ <bpmn2:targetRef>_di1</bpmn2:targetRef>
+ <bpmn2:assignment>
+ <bpmn2:from
xsi:type="bpmn2:tFormalExpression"><![CDATA[application/json]]></bpmn2:from>
+ <bpmn2:to
xsi:type="bpmn2:tFormalExpression"><![CDATA[_di1]]></bpmn2:to>
+ </bpmn2:assignment>
+ </bpmn2:dataInputAssociation>
+ <bpmn2:dataInputAssociation>
+ <bpmn2:targetRef>_di2</bpmn2:targetRef>
+ <bpmn2:assignment>
+ <bpmn2:from
xsi:type="bpmn2:tFormalExpression"><![CDATA[100]]></bpmn2:from>
+ <bpmn2:to
xsi:type="bpmn2:tFormalExpression"><![CDATA[_di2]]></bpmn2:to>
+ </bpmn2:assignment>
+ </bpmn2:dataInputAssociation>
+ </bpmn2:task>
+
+ <bpmn2:endEvent id="_End">
+ <bpmn2:incoming>_flow2</bpmn2:incoming>
+ </bpmn2:endEvent>
+
+ <bpmn2:sequenceFlow id="_flow1" sourceRef="_Start" targetRef="_RestTask"/>
+ <bpmn2:sequenceFlow id="_flow2" sourceRef="_RestTask" targetRef="_End"/>
+ </bpmn2:process>
+
+ <bpmndi:BPMNDiagram>
+ <bpmndi:BPMNPlane bpmnElement="resthyphenparms">
+ <bpmndi:BPMNShape id="shape__Start" bpmnElement="_Start">
+ <dc:Bounds height="56" width="56" x="100" y="100"/>
+ </bpmndi:BPMNShape>
+ <bpmndi:BPMNShape id="shape__RestTask" bpmnElement="_RestTask">
+ <dc:Bounds height="102" width="154" x="236" y="77"/>
+ </bpmndi:BPMNShape>
+ <bpmndi:BPMNShape id="shape__End" bpmnElement="_End">
+ <dc:Bounds height="56" width="56" x="470" y="100"/>
+ </bpmndi:BPMNShape>
+ </bpmndi:BPMNPlane>
+ </bpmndi:BPMNDiagram>
+</bpmn2:definitions>
diff --git
a/kogito-jbpm/jbpm-bpmn2/src/main/java/org/jbpm/bpmn2/xml/AbstractNodeHandler.java
b/kogito-jbpm/jbpm-bpmn2/src/main/java/org/jbpm/bpmn2/xml/AbstractNodeHandler.java
index 09165c17dcc..e219cd9b984 100755
---
a/kogito-jbpm/jbpm-bpmn2/src/main/java/org/jbpm/bpmn2/xml/AbstractNodeHandler.java
+++
b/kogito-jbpm/jbpm-bpmn2/src/main/java/org/jbpm/bpmn2/xml/AbstractNodeHandler.java
@@ -406,7 +406,7 @@ public abstract class AbstractNodeHandler extends
BaseAbstractHandler implements
protected DataDefinition getVariableDataSpec(Parser parser, String
propertyIdRef) {
RuleFlowProcess process = (RuleFlowProcess) ((ProcessBuildData)
parser.getData()).getMetaData(ProcessHandler.CURRENT_PROCESS);
- Optional<Variable> var =
process.getVariableScope().getVariables().stream().filter(e ->
e.getId().equals(propertyIdRef)).findAny();
+ Optional<Variable> var =
process.getVariableScope().getVariables().stream().filter(e ->
e.matchByIdOrName(propertyIdRef)).findAny();
if (var.isEmpty()) {
return null;
}
@@ -637,10 +637,9 @@ public abstract class AbstractNodeHandler extends
BaseAbstractHandler implements
}
/**
- * Simplifies variable expression in order to improve performance
+ * Simplifies variable expression in order to improve performance.
+ * If the expression contains just one variable, we can skip MVEL
expression evaluation.
*
- * If the expression contains just once variable, we can skip MVEL
expression evaluation
- *
* @param expression MVEL expression to evaluate
* @return Variable name if expression evaluation is not needed, original
expression otherwise
*/
@@ -651,7 +650,6 @@ public abstract class AbstractNodeHandler extends
BaseAbstractHandler implements
return matcher.group(1);
}
}
-
return expression;
}
diff --git
a/kogito-jbpm/jbpm-flow-builder/src/main/java/org/jbpm/compiler/canonical/WorkItemModelMetaData.java
b/kogito-jbpm/jbpm-flow-builder/src/main/java/org/jbpm/compiler/canonical/WorkItemModelMetaData.java
index e3f20ed8735..d422bcf4174 100644
---
a/kogito-jbpm/jbpm-flow-builder/src/main/java/org/jbpm/compiler/canonical/WorkItemModelMetaData.java
+++
b/kogito-jbpm/jbpm-flow-builder/src/main/java/org/jbpm/compiler/canonical/WorkItemModelMetaData.java
@@ -68,6 +68,7 @@ import static com.github.javaparser.StaticJavaParser.parse;
import static com.github.javaparser.StaticJavaParser.parseClassOrInterfaceType;
import static org.jbpm.ruleflow.core.Metadata.CUSTOM_AUTO_START;
import static org.kie.kogito.internal.utils.ConversionUtils.sanitizeClassName;
+import static org.kie.kogito.internal.utils.ConversionUtils.sanitizeJavaName;
public class WorkItemModelMetaData {
@@ -200,15 +201,16 @@ public class WorkItemModelMetaData {
variable.setName(entry.getKey());
DataType type =
DataTypeResolver.fromType(inputTypes.get(entry.getKey()),
Thread.currentThread().getContextClassLoader());
variable.setType(type);
- if
(!PatternConstants.PARAMETER_MATCHER.matcher(entry.getValue()).find()) {
+ if (entry.getValue() != null &&
!PatternConstants.PARAMETER_MATCHER.matcher(entry.getValue()).find()) {
variable.setValue(type.readValue(entry.getValue()));
}
}
+ String fieldName = sanitizeJavaName(entry.getKey());
FieldDeclaration fd = new FieldDeclaration().addVariable(
new VariableDeclarator()
.setType(variable.getType().getStringType())
- .setName(entry.getKey()))
+ .setName(fieldName))
.addModifier(Modifier.Keyword.PRIVATE);
modelClass.addMember(fd);
@@ -218,8 +220,8 @@ public class WorkItemModelMetaData {
fd.createGetter();
fd.createSetter();
- // from static method body
- FieldAccessExpr field = new FieldAccessExpr(item, entry.getKey());
+ // from static method body — sanitised name for the identifier,
original key for the runtime map lookup
+ FieldAccessExpr field = new FieldAccessExpr(item, fieldName);
ClassOrInterfaceType type =
parseClassOrInterfaceType(variable.getType().getStringType());
staticFromMap.addStatement(new AssignExpr(field, new CastExpr(
@@ -237,10 +239,11 @@ public class WorkItemModelMetaData {
continue;
}
+ String fieldName = sanitizeJavaName(entry.getKey());
FieldDeclaration fd = new FieldDeclaration().addVariable(
new VariableDeclarator()
.setType(entry.getValue().getClass().getCanonicalName())
- .setName(entry.getKey()))
+ .setName(fieldName))
.addModifier(Modifier.Keyword.PRIVATE);
modelClass.addMember(fd);
addUserTaskParamAnnotation(fd, UserTaskParam.ParamType.INPUT);
@@ -248,8 +251,8 @@ public class WorkItemModelMetaData {
fd.createGetter();
fd.createSetter();
- // from static method body
- FieldAccessExpr field = new FieldAccessExpr(item, entry.getKey());
+ // from static method body — sanitised name for the identifier,
original key for the runtime map lookup
+ FieldAccessExpr field = new FieldAccessExpr(item, fieldName);
ClassOrInterfaceType type =
parseClassOrInterfaceType(entry.getValue().getClass().getCanonicalName());
staticFromMap.addStatement(new AssignExpr(field, new CastExpr(
@@ -312,15 +315,16 @@ public class WorkItemModelMetaData {
variable.setName(entry.getKey());
DataType type =
DataTypeResolver.fromType(outputTypes.get(entry.getKey()),
Thread.currentThread().getContextClassLoader());
variable.setType(type);
- if
(!PatternConstants.PARAMETER_MATCHER.matcher(entry.getValue()).find()) {
+ if (entry.getValue() != null &&
!PatternConstants.PARAMETER_MATCHER.matcher(entry.getValue()).find()) {
variable.setValue(type.readValue(entry.getValue()));
}
}
+ String fieldName = sanitizeJavaName(entry.getKey());
FieldDeclaration fd = new FieldDeclaration().addVariable(
new VariableDeclarator()
.setType(variable.getType().getStringType())
- .setName(entry.getKey()))
+ .setName(fieldName))
.addModifier(Modifier.Keyword.PRIVATE);
modelClass.addMember(fd);
addUserTaskParamAnnotation(fd, UserTaskParam.ParamType.OUTPUT);
@@ -328,14 +332,14 @@ public class WorkItemModelMetaData {
fd.createGetter();
fd.createSetter();
- // toMap method body
+ // toMap method body — original key in the map, sanitised name for
field access
MethodCallExpr putVariable = new MethodCallExpr(params, "put");
putVariable.addArgument(new StringLiteralExpr(entry.getKey()));
- putVariable.addArgument(new FieldAccessExpr(new ThisExpr(),
entry.getKey()));
+ putVariable.addArgument(new FieldAccessExpr(new ThisExpr(),
fieldName));
toMapBody.addStatement(putVariable);
- // fromMap method body
- fromMapBody.addStatement(new AssignExpr(new
FieldAccessExpr(fromMapReturn, entry.getKey()),
+ // fromMap method body — original key for params.get(), sanitised
name for field assignment
+ fromMapBody.addStatement(new AssignExpr(new
FieldAccessExpr(fromMapReturn, fieldName),
new CastExpr(new ClassOrInterfaceType(null,
variable.getType().getStringType()),
new MethodCallExpr(params, "get").addArgument(new
StringLiteralExpr(entry.getKey()))),
Operator.ASSIGN));
diff --git
a/kogito-jbpm/jbpm-flow-builder/src/test/java/org/jbpm/compiler/canonical/ProcessToExecModelGeneratorTest.java
b/kogito-jbpm/jbpm-flow-builder/src/test/java/org/jbpm/compiler/canonical/ProcessToExecModelGeneratorTest.java
index dab02975850..ffca838e3b2 100644
---
a/kogito-jbpm/jbpm-flow-builder/src/test/java/org/jbpm/compiler/canonical/ProcessToExecModelGeneratorTest.java
+++
b/kogito-jbpm/jbpm-flow-builder/src/test/java/org/jbpm/compiler/canonical/ProcessToExecModelGeneratorTest.java
@@ -19,6 +19,7 @@
package org.jbpm.compiler.canonical;
import java.util.Collections;
+import java.util.List;
import org.jbpm.process.core.datatype.impl.type.IntegerDataType;
import org.jbpm.process.core.datatype.impl.type.ObjectDataType;
@@ -33,7 +34,9 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.fasterxml.jackson.annotation.JsonProperty;
+import com.github.javaparser.StaticJavaParser;
import com.github.javaparser.ast.CompilationUnit;
+import com.github.javaparser.ast.body.FieldDeclaration;
import com.github.javaparser.ast.body.MethodDeclaration;
import com.github.javaparser.ast.expr.NormalAnnotationExpr;
@@ -250,4 +253,63 @@ public class ProcessToExecModelGeneratorTest {
.asString())
.isEqualTo("FirstName");
}
+
+ @Test
+ public void testWorkItemModelHyphenatedParamsSanitization() {
+ RuleFlowProcessFactory factory =
RuleFlowProcessFactory.createProcess("demo.test");
+ factory
+ .name("test-process")
+ .packageName("com.myspace.demo")
+ .dynamic(false)
+ .version("1.0")
+ .workItemNode(one)
+ .name("RestService")
+ .workName("Rest")
+ .inMapping("contentType", "HEADER_content-type")
+ .inMapping("maxSize", "QUERY_max-size")
+ .outMapping("Result", "result")
+ .done()
+ .startNode(two)
+ .name("start")
+ .done()
+ .endNode(three)
+ .name("end")
+ .terminate(false)
+ .done()
+ .connection(two, one)
+ .connection(one, three);
+
+ WorkflowProcess process = factory.validate().getProcess();
+
+ List<WorkItemModelMetaData> workItemModels =
ProcessToExecModelGenerator.INSTANCE.generateWorkItemModel(process);
+ assertThat(workItemModels).hasSize(1);
+
+ WorkItemModelMetaData metaData = workItemModels.get(0);
+ String inputSource = metaData.generateInput();
+ String outputSource = metaData.generateOutput();
+
+ CompilationUnit inputUnit = StaticJavaParser.parse(inputSource);
+ CompilationUnit outputUnit = StaticJavaParser.parse(outputSource);
+
+ // Check input model field names are sanitized
+ List<FieldDeclaration> inputFields =
inputUnit.findAll(FieldDeclaration.class);
+ List<String> inputFieldNames = inputFields.stream()
+ .flatMap(f -> f.getVariables().stream())
+ .map(v -> v.getNameAsString())
+ .toList();
+ assertThat(inputFieldNames).contains("HEADER_content_type",
"QUERY_max_size");
+ assertThat(inputFieldNames).doesNotContain("HEADER_content-type",
"QUERY_max-size");
+
+ // Verify that the original hyphenated name is preserved as string
literal for map lookups
+ assertThat(inputSource).contains("\"HEADER_content-type\"");
+ assertThat(inputSource).contains("\"QUERY_max-size\"");
+
+ // Check output model field names
+ List<FieldDeclaration> outputFields =
outputUnit.findAll(FieldDeclaration.class);
+ List<String> outputFieldNames = outputFields.stream()
+ .flatMap(f -> f.getVariables().stream())
+ .map(v -> v.getNameAsString())
+ .toList();
+ assertThat(outputFieldNames).contains("Result");
+ }
}
diff --git
a/kogito-jbpm/jbpm-flow/src/main/java/org/jbpm/workflow/core/impl/IOSpecification.java
b/kogito-jbpm/jbpm-flow/src/main/java/org/jbpm/workflow/core/impl/IOSpecification.java
index 8b93746446d..12357a370c3 100644
---
a/kogito-jbpm/jbpm-flow/src/main/java/org/jbpm/workflow/core/impl/IOSpecification.java
+++
b/kogito-jbpm/jbpm-flow/src/main/java/org/jbpm/workflow/core/impl/IOSpecification.java
@@ -81,7 +81,8 @@ public class IOSpecification implements Serializable {
if (da.getAssignments().isEmpty()) {
mapping.put(da.getTarget().getLabel(),
da.getSources().get(0).getLabel());
} else if (da.getAssignments().get(0).getDialect() == null) {
- mapping.put(da.getAssignments().get(0).getTo().getLabel(),
da.getAssignments().get(0).getFrom().getExpression());
+ DataDefinition from = da.getAssignments().get(0).getFrom();
+ mapping.put(da.getAssignments().get(0).getTo().getLabel(),
from.hasExpression() ? from.getExpression() : from.getLabel());
}
}
return mapping;
@@ -93,7 +94,8 @@ public class IOSpecification implements Serializable {
if (da.getAssignments().isEmpty()) {
mapping.put(da.getTarget().getLabel(),
da.getSources().get(0).getLabel());
} else if (da.getAssignments().get(0).getDialect() == null) {
-
mapping.put(da.getAssignments().get(0).getTo().getExpression(),
da.getAssignments().get(0).getFrom().getLabel());
+ DataDefinition to = da.getAssignments().get(0).getTo();
+ mapping.put(to.hasExpression() ? to.getExpression() :
to.getLabel(), da.getAssignments().get(0).getFrom().getLabel());
}
}
return mapping;
@@ -105,7 +107,8 @@ public class IOSpecification implements Serializable {
if (da.getAssignments().isEmpty()) {
mapping.put(da.getSources().get(0).getLabel(),
da.getTarget().getLabel());
} else if (da.getAssignments().get(0).getDialect() == null) {
- mapping.put(da.getAssignments().get(0).getFrom().getLabel(),
da.getAssignments().get(0).getTo().getExpression());
+ DataDefinition to = da.getAssignments().get(0).getTo();
+ mapping.put(da.getAssignments().get(0).getFrom().getLabel(),
to.hasExpression() ? to.getExpression() : to.getLabel());
}
}
return mapping;
diff --git
a/kogito-jbpm/jbpm-flow/src/main/java/org/jbpm/workflow/core/impl/InputExpressionAssignment.java
b/kogito-jbpm/jbpm-flow/src/main/java/org/jbpm/workflow/core/impl/InputExpressionAssignment.java
index 9da077dbfe0..aa7202cde41 100644
---
a/kogito-jbpm/jbpm-flow/src/main/java/org/jbpm/workflow/core/impl/InputExpressionAssignment.java
+++
b/kogito-jbpm/jbpm-flow/src/main/java/org/jbpm/workflow/core/impl/InputExpressionAssignment.java
@@ -61,25 +61,31 @@ public class InputExpressionAssignment implements
AssignmentAction {
}
private Object evalInput(ImmutableDefaultFactory factory, String
expression) {
- String outcome = expression;
Matcher matcher =
PatternConstants.PARAMETER_MATCHER.matcher(expression);
+ // keyed by the full #{token} string so we can do a direct lookup below
Map<String, Object> values = new HashMap<>();
- if (matcher.find()) {
- matcher.reset();
- while (matcher.find()) {
- String paramName = matcher.group(1);
+ while (matcher.find()) {
+ String paramName = matcher.group(1);
+ try {
Object value = MVELProcessHelper.evaluator().eval(paramName,
factory);
if (value != null) {
- values.put(paramName, value);
+ values.put("#{" + paramName + "}", value);
}
+ } catch (Exception e) {
+ throw new IllegalArgumentException("Cannot resolve expression
variable '" + paramName + "' in '" + expression + "'", e);
}
}
- if (values.size() == 1) {
- return values.values().iterator().next();
+
+ // Sole-token passthrough: return the typed value directly to avoid
toString() coercion.
+ // Template expressions (e.g. {"message-value": "#{token}"}) fall
through to string replacement.
+ Object result = values.get(expression);
+ if (result != null) {
+ return result;
}
+ String outcome = expression;
for (Map.Entry<String, Object> entry : values.entrySet()) {
- outcome = outcome.replace("#{" + entry.getKey() + "}",
entry.getValue().toString());
+ outcome = outcome.replace(entry.getKey(),
entry.getValue().toString());
}
return outcome;
}
diff --git
a/kogito-jbpm/jbpm-flow/src/test/java/org/jbpm/workflow/core/impl/IOSpecificationTest.java
b/kogito-jbpm/jbpm-flow/src/test/java/org/jbpm/workflow/core/impl/IOSpecificationTest.java
new file mode 100644
index 00000000000..06d61b2f47f
--- /dev/null
+++
b/kogito-jbpm/jbpm-flow/src/test/java/org/jbpm/workflow/core/impl/IOSpecificationTest.java
@@ -0,0 +1,149 @@
+/*
+ * 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.jbpm.workflow.core.impl;
+
+import java.util.Collections;
+import java.util.Map;
+
+import org.jbpm.workflow.core.node.Assignment;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class IOSpecificationTest {
+
+ /**
+ * Single variable mapping via {@code <assignment>} without expression.
+ * Should resolve the mapping from label / variable name (e.g. "var1").
+ */
+ @Test
+ void testInputMappingWithAssignmentsNoExpression() {
+ IOSpecification ioSpec = new IOSpecification();
+ DataAssociation da = new DataAssociation(
+ Collections.emptyList(),
+ null,
+ Collections.singletonList(new Assignment(null, new
DataDefinition("_var1", "var1", "String"), new DataDefinition("_in1", "Input1",
"String"))),
+ null);
+ ioSpec.getDataInputAssociation().add(da);
+
+ Map<String, String> inputMapping = ioSpec.getInputMapping();
+ assertThat(inputMapping).containsEntry("Input1", "var1");
+ }
+
+ /**
+ * Expression / static constant mapping via {@code <assignment>} with an
expression.
+ * Should resolve the mapping from expression (e.g. "#{var1}" or constant
value).
+ */
+ @Test
+ void testInputMappingWithAssignmentsHavingExpression() {
+ IOSpecification ioSpec = new IOSpecification();
+ DataDefinition fromExpr = new DataDefinition("_expr1", "EXPRESSION
(#{var1})", "String", "#{var1}");
+ DataAssociation da = new DataAssociation(
+ Collections.emptyList(),
+ null,
+ Collections.singletonList(new Assignment(null, fromExpr, new
DataDefinition("_in1", "Input1", "String"))),
+ null);
+ ioSpec.getDataInputAssociation().add(da);
+
+ Map<String, String> inputMapping = ioSpec.getInputMapping();
+ assertThat(inputMapping).containsEntry("Input1", "#{var1}");
+ }
+
+ /**
+ * Single variable mapping via direct {@code <sourceRef>} (no assignment).
+ * Should resolve the mapping from label / variable name (e.g. "var1").
+ */
+ @Test
+ void testInputMappingWithSourcesOnly() {
+ IOSpecification ioSpec = new IOSpecification();
+ DataAssociation da = new DataAssociation(
+ Collections.singletonList(new DataDefinition("_var1", "var1",
"String")),
+ new DataDefinition("_in1", "Input1", "String"),
+ Collections.emptyList(),
+ null);
+ ioSpec.getDataInputAssociation().add(da);
+
+ Map<String, String> inputMapping = ioSpec.getInputMapping();
+ assertThat(inputMapping).containsEntry("Input1", "var1");
+ }
+
+ /**
+ * Single variable mapping via {@code <assignment>} without expression.
+ * Should resolve the mapping from label / variable name (e.g. "var1").
+ */
+ @Test
+ void testOutputMappingWithAssignmentsNoExpression() {
+ IOSpecification ioSpec = new IOSpecification();
+ DataAssociation da = new DataAssociation(
+ Collections.emptyList(),
+ null,
+ Collections.singletonList(new Assignment(null, new
DataDefinition("_out1", "Output1", "String"), new DataDefinition("_var1",
"var1", "String"))),
+ null);
+ ioSpec.getDataOutputAssociation().add(da);
+
+ Map<String, String> outputMapping = ioSpec.getOutputMapping();
+ assertThat(outputMapping).containsEntry("var1", "Output1");
+
+ Map<String, String> outputMappingBySources =
ioSpec.getOutputMappingBySources();
+ assertThat(outputMappingBySources).containsEntry("Output1", "var1");
+ }
+
+ /**
+ * Expression mapping via {@code <assignment>} with an expression.
+ * Should resolve the mapping from expression (e.g. "#{var1}").
+ */
+ @Test
+ void testOutputMappingWithAssignmentsHavingExpression() {
+ IOSpecification ioSpec = new IOSpecification();
+ DataDefinition toExpr = new DataDefinition("_expr1", "EXPRESSION
(#{var1})", "String", "#{var1}");
+ DataAssociation da = new DataAssociation(
+ Collections.emptyList(),
+ null,
+ Collections.singletonList(new Assignment(null, new
DataDefinition("_out1", "Output1", "String"), toExpr)),
+ null);
+ ioSpec.getDataOutputAssociation().add(da);
+
+ Map<String, String> outputMapping = ioSpec.getOutputMapping();
+ assertThat(outputMapping).containsEntry("#{var1}", "Output1");
+
+ Map<String, String> outputMappingBySources =
ioSpec.getOutputMappingBySources();
+ assertThat(outputMappingBySources).containsEntry("Output1", "#{var1}");
+ }
+
+ /**
+ * Single variable mapping via direct {@code <sourceRef>} (no assignment).
+ * Should resolve the mapping from label / variable name (e.g. "var1").
+ */
+ @Test
+ void testOutputMappingWithSourcesOnly() {
+ IOSpecification ioSpec = new IOSpecification();
+ DataAssociation da = new DataAssociation(
+ Collections.singletonList(new DataDefinition("_out1",
"Output1", "String")),
+ new DataDefinition("_var1", "var1", "String"),
+ Collections.emptyList(),
+ null);
+ ioSpec.getDataOutputAssociation().add(da);
+
+ Map<String, String> outputMapping = ioSpec.getOutputMapping();
+ assertThat(outputMapping).containsEntry("var1", "Output1");
+
+ Map<String, String> outputMappingBySources =
ioSpec.getOutputMappingBySources();
+ assertThat(outputMappingBySources).containsEntry("Output1", "var1");
+ }
+}
diff --git
a/kogito-jbpm/jbpm-flow/src/test/java/org/jbpm/workflow/core/impl/InputExpressionAssignmentTest.java
b/kogito-jbpm/jbpm-flow/src/test/java/org/jbpm/workflow/core/impl/InputExpressionAssignmentTest.java
new file mode 100644
index 00000000000..c34a075b180
--- /dev/null
+++
b/kogito-jbpm/jbpm-flow/src/test/java/org/jbpm/workflow/core/impl/InputExpressionAssignmentTest.java
@@ -0,0 +1,123 @@
+/*
+ * 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.jbpm.workflow.core.impl;
+
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.Function;
+
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static
org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
+
+class InputExpressionAssignmentTest {
+
+ // Helper: runs the assignment and returns whatever the producer received.
+ // Uses toExpression() for `from` so that expression is always set (even
when there are no #{} tokens).
+ private Object eval(String fromExpr, String toLabel, Function<String,
Object> sourceResolver) throws Exception {
+ DataDefinition from = DataDefinition.toExpression(fromExpr);
+ DataDefinition to = DataDefinition.toSimpleDefinition(toLabel);
+ InputExpressionAssignment assignment = new
InputExpressionAssignment(from, to);
+
+ AtomicReference<Object> result = new AtomicReference<>();
+ assignment.execute(sourceResolver, ignored -> null, (label, value) ->
result.set(value));
+ return result.get();
+ }
+
+ //
-------------------------------------------------------------------------
+ // Sole-token cases — value must be returned as-is (no toString coercion)
+ //
-------------------------------------------------------------------------
+
+ @Test
+ void soleToken_stringValue_returnsString() throws Exception {
+ Object result = eval("#{message}", "target", name -> "hello");
+ assertThat(result).isEqualTo("hello");
+ }
+
+ @Test
+ void soleToken_mapValue_returnsMapNotToString() throws Exception {
+ Map<String, Object> payload = Map.of("key", "val");
+ Object result = eval("#{payload}", "target", name -> payload);
+ // Must be the original Map, not its toString representation
+ assertThat(result).isSameAs(payload);
+ }
+
+ @Test
+ void soleToken_integerValue_returnsInteger() throws Exception {
+ Object result = eval("#{count}", "target", name -> 42);
+ assertThat(result).isEqualTo(42);
+ }
+
+ //
-------------------------------------------------------------------------
+ // Template cases — surrounding text forces string interpolation
+ //
-------------------------------------------------------------------------
+
+ @Test
+ void template_singleTokenEmbedded_interpolatesIntoString() throws
Exception {
+ // Expression: {"message-value": "#{message}"} → {"message-value":
"hello"}
+ Object result = eval("{\"message-value\": \"#{message}\"}", "target",
name -> "hello");
+ assertThat(result).isEqualTo("{\"message-value\": \"hello\"}");
+ }
+
+ @Test
+ void template_multipleTokens_allInterpolated() throws Exception {
+ // Expression: "#{first}-#{second}" → "foo-bar"
+ Object result = eval("#{first}-#{second}", "target", name -> {
+ if ("first".equals(name))
+ return "foo";
+ if ("second".equals(name))
+ return "bar";
+ return null;
+ });
+ assertThat(result).isEqualTo("foo-bar");
+ }
+
+ //
-------------------------------------------------------------------------
+ // Unresolvable token — variable not present in source context →
controlled failure
+ //
-------------------------------------------------------------------------
+
+ @Test
+ void soleToken_unresolvable_throwsIllegalArgument() {
+ assertThatIllegalArgumentException()
+ .isThrownBy(() -> eval("#{missing}", "target", name -> null))
+ .withMessageContaining("missing")
+ .withMessageContaining("#{missing}");
+ }
+
+ @Test
+ void template_unresolvableToken_throwsIllegalArgument() {
+ // #{b} is unresolvable even when #{a} resolves — fail fast rather
than silently embed garbage
+ assertThatIllegalArgumentException()
+ .isThrownBy(() -> eval("#{a}-#{b}", "target", name ->
"a".equals(name) ? "X" : null))
+ .withMessageContaining("b");
+ }
+
+ //
-------------------------------------------------------------------------
+ // No-token case — plain literal, nothing to resolve or replace
+ //
-------------------------------------------------------------------------
+
+ @Test
+ void noToken_plainLiteralReturnedAsIs() throws Exception {
+ // Expression has no #{} tokens → values map stays empty → outcome =
expression unchanged
+ Object result = eval("{\"message-value\": \"value\"}", "target", name
-> null);
+ assertThat(result).isEqualTo("{\"message-value\": \"value\"}");
+ }
+
+}
diff --git
a/kogito-jbpm/jbpm-tests/src/test/bpmn/org/jbpm/bpmn2/data/BPMN2-DataInputAssociationsVariable.bpmn2
b/kogito-jbpm/jbpm-tests/src/test/bpmn/org/jbpm/bpmn2/data/BPMN2-DataInputAssociationsVariable.bpmn2
new file mode 100644
index 00000000000..23b3daf07d9
--- /dev/null
+++
b/kogito-jbpm/jbpm-tests/src/test/bpmn/org/jbpm/bpmn2/data/BPMN2-DataInputAssociationsVariable.bpmn2
@@ -0,0 +1,68 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ 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.
+ -->
+
+<definitions id="Definition"
+ targetNamespace=""
+ typeLanguage="http://www.java.com/javaTypes"
+ expressionLanguage="http://www.mvel.org/2.0"
+ xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://www.omg.org/spec/BPMN/20100524/MODEL
BPMN20.xsd"
+ xmlns:g="http://www.jboss.org/drools/flow/gpd"
+ xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI"
+ xmlns:dc="http://www.omg.org/spec/DD/20100524/DC"
+ xmlns:di="http://www.omg.org/spec/DD/20100524/DI"
+ xmlns:tns="http://www.jboss.org/drools">
+
+ <itemDefinition id="_myVarItem" structureRef="String" />
+
+ <process processType="Private" isExecutable="true"
id="DataInputAssociationsVariable" name="DataInputAssociationsVariable"
tns:packageName="org.jbpm.bpmn2.data" >
+
+ <!-- process variables -->
+ <property id="myVar" itemSubjectRef="_myVarItem"/>
+
+ <!-- nodes -->
+ <startEvent id="_1" name="Start" />
+ <userTask id="_2" name="Task">
+ <ioSpecification>
+ <dataInput id='taskInput' name='taskInput'/>
+ <inputSet>
+ <dataInputRefs>taskInput</dataInputRefs>
+ </inputSet>
+ <outputSet>
+ </outputSet>
+ </ioSpecification>
+ <dataInputAssociation>
+ <targetRef>taskInput</targetRef>
+ <assignment>
+ <from xsi:type="tFormalExpression"><![CDATA[#{myVar}]]></from>
+ <to xsi:type="tFormalExpression"><![CDATA[taskInput]]></to>
+ </assignment>
+ </dataInputAssociation>
+ </userTask>
+ <endEvent id="_3" name="End" />
+
+ <!-- connections -->
+ <sequenceFlow id="_1-_2" sourceRef="_1" targetRef="_2" />
+ <sequenceFlow id="_2-_3" sourceRef="_2" targetRef="_3" />
+
+ </process>
+
+</definitions>
diff --git a/kogito-jbpm/jbpm-tests/src/test/java/org/jbpm/bpmn2/DataTest.java
b/kogito-jbpm/jbpm-tests/src/test/java/org/jbpm/bpmn2/DataTest.java
index 95f5c0bdcf5..052c58bb8f3 100755
--- a/kogito-jbpm/jbpm-tests/src/test/java/org/jbpm/bpmn2/DataTest.java
+++ b/kogito-jbpm/jbpm-tests/src/test/java/org/jbpm/bpmn2/DataTest.java
@@ -44,6 +44,8 @@ import
org.jbpm.bpmn2.data.DataInputAssociationsStringNoQuotesProcess;
import org.jbpm.bpmn2.data.DataInputAssociationsStringObjectModel;
import org.jbpm.bpmn2.data.DataInputAssociationsStringObjectProcess;
import org.jbpm.bpmn2.data.DataInputAssociationsStringProcess;
+import org.jbpm.bpmn2.data.DataInputAssociationsVariableModel;
+import org.jbpm.bpmn2.data.DataInputAssociationsVariableProcess;
import org.jbpm.bpmn2.data.DataInputAssociationsXmlLiteralModel;
import org.jbpm.bpmn2.data.DataInputAssociationsXmlLiteralProcess;
import org.jbpm.bpmn2.data.DataObjectModel;
@@ -325,6 +327,26 @@ public class DataTest extends JbpmBpmn2TestCase {
assertThat(instance.status()).isEqualTo(ProcessInstance.STATE_ACTIVE);
}
+ @Test
+ public void testDataInputAssociationsWithVariable() {
+ Application app = ProcessTestHelper.newApplication();
+ ProcessTestHelper.registerHandler(app, "Human Task", new
DefaultKogitoWorkItemHandler() {
+ @Override
+ public Optional<WorkItemTransition>
activateWorkItemHandler(KogitoWorkItemManager manager, KogitoWorkItemHandler
handler, KogitoWorkItem workItem, WorkItemTransition transition) {
+
assertThat(workItem.getParameter("taskInput")).isEqualTo("variableValue123");
+ return Optional.empty();
+ }
+ });
+
+ org.kie.kogito.process.Process<DataInputAssociationsVariableModel>
processDefinition = DataInputAssociationsVariableProcess.newProcess(app);
+ DataInputAssociationsVariableModel model =
processDefinition.createModel();
+ model.setMyVar("variableValue123");
+
+
org.kie.kogito.process.ProcessInstance<DataInputAssociationsVariableModel>
instance = processDefinition.createInstance(model);
+ instance.start();
+ assertThat(instance.status()).isEqualTo(ProcessInstance.STATE_ACTIVE);
+ }
+
@Test
public void testDataInputAssociationsWithXMLLiteral() {
Application app = ProcessTestHelper.newApplication();
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]