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]

Reply via email to