This is an automated email from the ASF dual-hosted git repository.

tiagobento pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/incubator-kie-drools.git


The following commit(s) were added to refs/heads/main by this push:
     new 9458a87dfe2 [NO-ISSUE] Restore 6 kogito-runtimes sync commits lost in 
merge (PR #6804) (#6856)
9458a87dfe2 is described below

commit 9458a87dfe20a816770d6aea8b3a1ebcdc9e394b
Author: Kbowers <[email protected]>
AuthorDate: Thu Jul 30 16:35:25 2026 +0100

    [NO-ISSUE] Restore 6 kogito-runtimes sync commits lost in merge (PR #6804) 
(#6856)
    
    The merge commit 6bcfcfd10ca ('Merge incubator-kie-kogito-runtimes
    repository into drools') was constructed from a point-in-time snapshot
    of kogito-runtimes-rewrite-new as of Jul 9 (cb2fb63c7d6). Six commits
    were later synced to the branch tip (2c2cc9a8600) but their file
    changes were never reflected in the merge tree. This commit restores
    the correct content for all 22 affected files.
    
    Lost commits (in order):
    - 79c906871241  Upgrade SpringFramework to 7.0.8 for CVE fix (#4340)
    - d91ba08b269   Workflow Engine: null work_item_id audit event (#4326)
    - 00dc560b43d   PATCH API ignores explicit JSON null values (#4335)
    - fa7ff2f686f   Add @Column mappings to unmapped JPA entity fields (#4344)
    - b658c2316b9   fix CWE-470 validate rule unit class name (#4341)
    - 2c2cc9a8600   Migrate testMultiInstanceLoopCharacteristicsProcess2 (#4343)
---
 .../jbpm/usertask/jpa/model/AttachmentEntity.java  |   3 +
 .../org/jbpm/usertask/jpa/model/CommentEntity.java |   2 +
 .../usertask/jpa/model/TaskTimerConfigEntity.java  |   2 +
 .../usertask/jpa/model/UserTaskInstanceEntity.java |   2 +
 .../src/main/java/org/kie/kogito/Model.java        |  13 +-
 .../src/main/java/org/kie/kogito/Models.java       |   8 +-
 .../uow/events/UnitOfWorkProcessEventListener.java |   4 +-
 .../kie/kogito/codegen/tests/PublishEventIT.java   |  50 +++++++
 .../RestResourceQuarkusTemplate.java               |   4 +-
 .../RestResourceSpringTemplate.java                |   4 +-
 .../org/jbpm/compiler/canonical/ModelMetaData.java |  25 +++-
 .../canonical/ProcessToExecModelGenerator.java     |   2 +
 .../class-templates/ModelNoIDTemplate.java         |  21 ++-
 .../resources/class-templates/ModelTemplate.java   |  35 ++++-
 .../src/test/java/org/jbpm/bpmn2/FlowTest.java     |  34 +++--
 .../quarkus/support/RuleUnitServiceImpl.java       |  12 +-
 .../support/StatefulRuleUnitServiceImpl.java       |  12 +-
 .../integrationtests/quarkus/BasicRestIT.java      |  86 +++++++++++
 .../kie/kogito/integrationtests/quarkus/OASIT.java |  21 +++
 kogito-springboot/bom/pom.xml                      |   2 +-
 .../src/main/resources/AdHocFragments.bpmn         |   2 +
 .../integrationtests/springboot/BasicRestTest.java | 159 +++++++++++++++++++++
 22 files changed, 468 insertions(+), 35 deletions(-)

diff --git 
a/kogito-addons/common/jbpm-usertask-storage-jpa/src/main/java/org/jbpm/usertask/jpa/model/AttachmentEntity.java
 
b/kogito-addons/common/jbpm-usertask-storage-jpa/src/main/java/org/jbpm/usertask/jpa/model/AttachmentEntity.java
index 0ab9751bf95..42d1c45040a 100644
--- 
a/kogito-addons/common/jbpm-usertask-storage-jpa/src/main/java/org/jbpm/usertask/jpa/model/AttachmentEntity.java
+++ 
b/kogito-addons/common/jbpm-usertask-storage-jpa/src/main/java/org/jbpm/usertask/jpa/model/AttachmentEntity.java
@@ -28,10 +28,13 @@ import jakarta.persistence.*;
 public class AttachmentEntity {
 
     @Id
+    @Column(name = "id")
     private String id;
 
+    @Column(name = "name")
     private String name;
 
+    @Column(name = "url")
     private String url;
 
     @Column(name = "updated_by")
diff --git 
a/kogito-addons/common/jbpm-usertask-storage-jpa/src/main/java/org/jbpm/usertask/jpa/model/CommentEntity.java
 
b/kogito-addons/common/jbpm-usertask-storage-jpa/src/main/java/org/jbpm/usertask/jpa/model/CommentEntity.java
index 83132a3c35a..4dd5fb15681 100644
--- 
a/kogito-addons/common/jbpm-usertask-storage-jpa/src/main/java/org/jbpm/usertask/jpa/model/CommentEntity.java
+++ 
b/kogito-addons/common/jbpm-usertask-storage-jpa/src/main/java/org/jbpm/usertask/jpa/model/CommentEntity.java
@@ -28,8 +28,10 @@ import jakarta.persistence.*;
 public class CommentEntity {
 
     @Id
+    @Column(name = "id")
     private String id;
 
+    @Column(name = "comment")
     private String comment;
 
     @Column(name = "updated_by")
diff --git 
a/kogito-addons/common/jbpm-usertask-storage-jpa/src/main/java/org/jbpm/usertask/jpa/model/TaskTimerConfigEntity.java
 
b/kogito-addons/common/jbpm-usertask-storage-jpa/src/main/java/org/jbpm/usertask/jpa/model/TaskTimerConfigEntity.java
index 1f59855a808..4aa514837d4 100644
--- 
a/kogito-addons/common/jbpm-usertask-storage-jpa/src/main/java/org/jbpm/usertask/jpa/model/TaskTimerConfigEntity.java
+++ 
b/kogito-addons/common/jbpm-usertask-storage-jpa/src/main/java/org/jbpm/usertask/jpa/model/TaskTimerConfigEntity.java
@@ -21,6 +21,7 @@ package org.jbpm.usertask.jpa.model;
 
 import java.util.Objects;
 
+import jakarta.persistence.Column;
 import jakarta.persistence.GeneratedValue;
 import jakarta.persistence.GenerationType;
 import jakarta.persistence.Id;
@@ -33,6 +34,7 @@ public abstract class TaskTimerConfigEntity<T> extends 
AbstractTaskEntity<T> {
 
     @Id
     @GeneratedValue(strategy = GenerationType.AUTO)
+    @Column(name = "id")
     protected Integer id;
 
     @ManyToOne(optional = false)
diff --git 
a/kogito-addons/common/jbpm-usertask-storage-jpa/src/main/java/org/jbpm/usertask/jpa/model/UserTaskInstanceEntity.java
 
b/kogito-addons/common/jbpm-usertask-storage-jpa/src/main/java/org/jbpm/usertask/jpa/model/UserTaskInstanceEntity.java
index fdd0ff06c07..276a65ccab2 100644
--- 
a/kogito-addons/common/jbpm-usertask-storage-jpa/src/main/java/org/jbpm/usertask/jpa/model/UserTaskInstanceEntity.java
+++ 
b/kogito-addons/common/jbpm-usertask-storage-jpa/src/main/java/org/jbpm/usertask/jpa/model/UserTaskInstanceEntity.java
@@ -54,6 +54,7 @@ public class UserTaskInstanceEntity {
     public static final String STATUS_FILTER_CLAUSE = " and userTask.status in 
(:statusFilter)";
 
     @Id
+    @Column(name = "id")
     private String id;
 
     @Column(name = "user_task_id")
@@ -68,6 +69,7 @@ public class UserTaskInstanceEntity {
     @Column(name = "task_priority")
     private String taskPriority;
 
+    @Column(name = "status")
     private String status;
 
     @Column(name = "termination_type")
diff --git a/kogito-api/kogito-api/src/main/java/org/kie/kogito/Model.java 
b/kogito-api/kogito-api/src/main/java/org/kie/kogito/Model.java
index fbe9acb8ded..a4583aa74f5 100644
--- a/kogito-api/kogito-api/src/main/java/org/kie/kogito/Model.java
+++ b/kogito-api/kogito-api/src/main/java/org/kie/kogito/Model.java
@@ -19,8 +19,7 @@
 package org.kie.kogito;
 
 import java.util.Map;
-import java.util.Map.Entry;
-import java.util.stream.Collectors;
+import java.util.Set;
 
 /**
  * Represents data model type of objects that are usually descriptor of data 
holders.
@@ -33,8 +32,16 @@ public interface Model extends MapInput, MapOutput {
     }
 
     default Map<String, Object> updatePartially(Map<String, Object> params) {
-        params = params.entrySet().stream().filter(e -> e.getValue() != 
null).collect(Collectors.toMap(Entry::getKey, Entry::getValue));
         update(params);
         return params;
     }
+
+    /**
+     * Returns the set of field names that were explicitly set (e.g. during a 
PATCH request).
+     * Returns null when tracking is not active — all fields are included in 
toMap().
+     * Returns a non-null set when tracking is active — only those fields are 
included.
+     */
+    default Set<String> getModifiedFields() {
+        return null;
+    }
 }
diff --git a/kogito-api/kogito-api/src/main/java/org/kie/kogito/Models.java 
b/kogito-api/kogito-api/src/main/java/org/kie/kogito/Models.java
index c1e57052d69..c91ca522fe2 100644
--- a/kogito-api/kogito-api/src/main/java/org/kie/kogito/Models.java
+++ b/kogito-api/kogito-api/src/main/java/org/kie/kogito/Models.java
@@ -21,6 +21,7 @@ package org.kie.kogito;
 import java.lang.reflect.Field;
 import java.util.LinkedHashMap;
 import java.util.Map;
+import java.util.Set;
 
 import com.fasterxml.jackson.annotation.JsonProperty;
 
@@ -30,12 +31,17 @@ public class Models {
     }
 
     @SuppressWarnings("squid:S3011")
-    public static Map<String, Object> toMap(Object m) {
+    public static Map<String, Object> toMap(MapOutput m) {
+        Set<String> modifiedFields = (m instanceof Model model) ? 
model.getModifiedFields() : null;
         Map<String, Object> map = new LinkedHashMap<>();
         for (Field field : m.getClass().getDeclaredFields()) {
             JsonProperty jsonAnnotation = 
field.getAnnotation(JsonProperty.class);
             if (jsonAnnotation != null) {
                 String name = jsonAnnotation.value();
+                // null = not tracking (include all); non-null = include only 
modified fields
+                if (modifiedFields != null && !modifiedFields.contains(name)) {
+                    continue;
+                }
                 field.setAccessible(true);
                 try {
                     map.put(name, field.get(m));
diff --git 
a/kogito-api/kogito-api/src/main/java/org/kie/kogito/uow/events/UnitOfWorkProcessEventListener.java
 
b/kogito-api/kogito-api/src/main/java/org/kie/kogito/uow/events/UnitOfWorkProcessEventListener.java
index 7d33accfe7f..cee3039b41b 100644
--- 
a/kogito-api/kogito-api/src/main/java/org/kie/kogito/uow/events/UnitOfWorkProcessEventListener.java
+++ 
b/kogito-api/kogito-api/src/main/java/org/kie/kogito/uow/events/UnitOfWorkProcessEventListener.java
@@ -68,12 +68,12 @@ public class UnitOfWorkProcessEventListener extends 
DefaultKogitoProcessEventLis
 
     @Override
     public void beforeNodeTriggered(ProcessNodeTriggeredEvent event) {
-        intercept(event);
+
     }
 
     @Override
     public void afterNodeTriggered(ProcessNodeTriggeredEvent event) {
-
+        intercept(event);
     }
 
     @Override
diff --git 
a/kogito-codegen-modules/kogito-codegen-processes-integration-tests/src/test/java/org/kie/kogito/codegen/tests/PublishEventIT.java
 
b/kogito-codegen-modules/kogito-codegen-processes-integration-tests/src/test/java/org/kie/kogito/codegen/tests/PublishEventIT.java
index 6f75bfb6382..414676ef7dc 100644
--- 
a/kogito-codegen-modules/kogito-codegen-processes-integration-tests/src/test/java/org/kie/kogito/codegen/tests/PublishEventIT.java
+++ 
b/kogito-codegen-modules/kogito-codegen-processes-integration-tests/src/test/java/org/kie/kogito/codegen/tests/PublishEventIT.java
@@ -529,6 +529,56 @@ public class PublishEventIT extends AbstractCodegenIT {
                 .isNotNull().containsEntry("s", "Goodbye Hello john!!");
     }
 
+    @Test
+    public void testUserTaskNodeEnterEventContainsWorkItemId() throws 
Exception {
+        Application app = 
generateCodeProcessesOnly("usertask/UserTasksProcess.bpmn2");
+        assertThat(app).isNotNull();
+
+        Process<? extends Model> p = 
app.get(Processes.class).processById("UserTasksProcess");
+
+        Model model = p.createModel();
+        model.fromMap(Collections.emptyMap());
+
+        TestEventPublisher publisher = new TestEventPublisher();
+        app.unitOfWorkManager().eventManager().setService("http://myhost";);
+        app.unitOfWorkManager().eventManager().addPublisher(publisher);
+
+        UnitOfWork uow = app.unitOfWorkManager().newUnitOfWork();
+        uow.start();
+
+        ProcessInstance<?> processInstance = p.createInstance(model);
+        processInstance.start();
+
+        uow.end();
+
+        assertThat(processInstance.status())
+                .isEqualTo(ProcessInstance.STATE_ACTIVE);
+
+        List<WorkItem> workItems = processInstance.workItems(securityPolicy);
+
+        assertThat(workItems).hasSize(1);
+        assertThat(workItems.get(0).getName()).isEqualTo("FirstTask");
+        assertThat(workItems.get(0).getId()).isNotNull();
+
+        List<DataEvent<?>> events = publisher.extract();
+
+        List<ProcessInstanceNodeEventBody> nodeEnterEvents = 
findNodeInstanceEvents(
+                events,
+                ProcessInstanceNodeEventBody.EVENT_TYPE_ENTER);
+
+        ProcessInstanceNodeEventBody humanTaskNodeEvent = 
nodeEnterEvents.stream()
+                .filter(event -> "HumanTaskNode".equals(event.getNodeType()))
+                .filter(event -> "First Task".equals(event.getNodeName()))
+                .findFirst()
+                .orElseThrow(() -> new AssertionError(
+                        "HumanTaskNode ENTER event was not published"));
+
+        assertThat(humanTaskNodeEvent.getWorkItemId())
+                .as("HumanTaskNode ENTER event must contain the created work 
item ID")
+                .isNotNull()
+                .isEqualTo(workItems.get(0).getId());
+    }
+
     /*
      * Helper methods
      */
diff --git 
a/kogito-codegen-modules/kogito-codegen-processes/src/main/resources/class-templates/RestResourceQuarkusTemplate.java
 
b/kogito-codegen-modules/kogito-codegen-processes/src/main/resources/class-templates/RestResourceQuarkusTemplate.java
index 6202d1032e0..18d5e778b29 100644
--- 
a/kogito-codegen-modules/kogito-codegen-processes/src/main/resources/class-templates/RestResourceQuarkusTemplate.java
+++ 
b/kogito-codegen-modules/kogito-codegen-processes/src/main/resources/class-templates/RestResourceQuarkusTemplate.java
@@ -160,7 +160,9 @@ public class $Type$Resource {
     @Produces(MediaType.APPLICATION_JSON)
     @Operation(operationId = "updateProcessInstance_$name$", summary = 
"$documentation$", description = "$processInstanceDescription$")
     public $Type$Output updateModel_$name$(@PathParam("id") String id, 
$Type$Input resource) {
-        return processService.update(process, id, 
resource.toModel()).orElseThrow(NotFoundException::new);
+        $Type$ model = resource.toModel();
+        model.clearModifiedFields();
+        return processService.update(process, id, 
model).orElseThrow(NotFoundException::new);
     }
     
     @PATCH
diff --git 
a/kogito-codegen-modules/kogito-codegen-processes/src/main/resources/class-templates/RestResourceSpringTemplate.java
 
b/kogito-codegen-modules/kogito-codegen-processes/src/main/resources/class-templates/RestResourceSpringTemplate.java
index fe227d7538c..5328f80d171 100644
--- 
a/kogito-codegen-modules/kogito-codegen-processes/src/main/resources/class-templates/RestResourceSpringTemplate.java
+++ 
b/kogito-codegen-modules/kogito-codegen-processes/src/main/resources/class-templates/RestResourceSpringTemplate.java
@@ -112,7 +112,9 @@ public class $Type$Resource {
             consumes = MediaType.APPLICATION_JSON_VALUE)
     @Operation(operationId = "updateProcessInstance_$name$", summary = 
"$documentation$", description = "$processInstanceDescription$")
     public $Type$Output updateModel_$name$(@PathVariable("id") String id, 
@RequestBody(required = false) $Type$Input resource) {
-        return processService.update(process, id, 
resource.toModel()).orElseThrow(() -> new 
ResponseStatusException(HttpStatus.NOT_FOUND));
+        $Type$ model = resource.toModel();
+        model.clearModifiedFields();
+        return processService.update(process, id, model).orElseThrow(() -> new 
ResponseStatusException(HttpStatus.NOT_FOUND));
     }
     
     @PatchMapping(value = "/{id}", produces = MediaType.APPLICATION_JSON_VALUE,
diff --git 
a/kogito-jbpm/jbpm-flow-builder/src/main/java/org/jbpm/compiler/canonical/ModelMetaData.java
 
b/kogito-jbpm/jbpm-flow-builder/src/main/java/org/jbpm/compiler/canonical/ModelMetaData.java
index 62f35f59cdb..bd3d64d21af 100644
--- 
a/kogito-jbpm/jbpm-flow-builder/src/main/java/org/jbpm/compiler/canonical/ModelMetaData.java
+++ 
b/kogito-jbpm/jbpm-flow-builder/src/main/java/org/jbpm/compiler/canonical/ModelMetaData.java
@@ -190,6 +190,18 @@ public class ModelMetaData {
         }
         modelClass.setName(modelClassSimpleName);
 
+        // The modified-fields tracking field/getter must stay out of the 
generated OpenAPI
+        if (supportsOpenApiGeneration) {
+            NormalAnnotationExpr hiddenSchema = new NormalAnnotationExpr(new 
Name(Schema.class.getCanonicalName()),
+                    NodeList.nodeList(new MemberValuePair("hidden", new 
BooleanLiteralExpr(true))));
+            modelClass.findFirst(FieldDeclaration.class,
+                    fd -> fd.getVariables().stream().anyMatch(v -> 
v.getNameAsString().equals("__modifiedFields")))
+                    .ifPresent(fd -> fd.addAnnotation(hiddenSchema.clone()));
+            modelClass.findFirst(MethodDeclaration.class, md -> 
md.getNameAsString().equals("getModifiedFields"))
+                    .ifPresent(md -> md.addAnnotation(hiddenSchema.clone()));
+            compilationUnit.addImport(Schema.class.getCanonicalName());
+        }
+
         // setup of the toMap method body
         BlockStmt toMapBody = new BlockStmt();
         ClassOrInterfaceType toMap = new ClassOrInterfaceType(null, new 
SimpleName(Map.class.getSimpleName()),
@@ -225,7 +237,14 @@ public class ModelMetaData {
             applyOpenApiSchemaAnnotation(fd);
 
             fd.createGetter();
-            fd.createSetter();
+            MethodDeclaration setter = fd.createSetter();
+            if (isInputModel()) {
+                // Only Input setters run from request deserialization - 
Model/Output setters
+                // are also called by generated copy loops, which would 
wrongly count as "set".
+                setter.getBody().ifPresent(setterBody -> 
setterBody.addStatement(
+                        new MethodCallExpr(null, "markModified",
+                                NodeList.nodeList(new 
StringLiteralExpr(varName)))));
+            }
 
         }
 
@@ -265,6 +284,10 @@ public class ModelMetaData {
                 .addModifier(Modifier.Keyword.PRIVATE);
     }
 
+    private boolean isInputModel() {
+        return "/class-templates/ModelNoIDTemplate.java".equals(templateName);
+    }
+
     public String getModelClassSimpleName() {
         return modelClassSimpleName;
     }
diff --git 
a/kogito-jbpm/jbpm-flow-builder/src/main/java/org/jbpm/compiler/canonical/ProcessToExecModelGenerator.java
 
b/kogito-jbpm/jbpm-flow-builder/src/main/java/org/jbpm/compiler/canonical/ProcessToExecModelGenerator.java
index b78ed1741f2..1692b928f53 100644
--- 
a/kogito-jbpm/jbpm-flow-builder/src/main/java/org/jbpm/compiler/canonical/ProcessToExecModelGenerator.java
+++ 
b/kogito-jbpm/jbpm-flow-builder/src/main/java/org/jbpm/compiler/canonical/ProcessToExecModelGenerator.java
@@ -222,6 +222,8 @@ public class ProcessToExecModelGenerator {
                         "get" +
                                 fieldName)));
             }
+            body.addStatement(new MethodCallExpr(returnName, 
"addModifiedFields",
+                    NodeList.nodeList(new MethodCallExpr(null, 
"getModifiedFields"))));
             body.addStatement(new ReturnStmt(returnName));
             method.setBody(body);
         }
diff --git 
a/kogito-jbpm/jbpm-flow-builder/src/main/resources/class-templates/ModelNoIDTemplate.java
 
b/kogito-jbpm/jbpm-flow-builder/src/main/resources/class-templates/ModelNoIDTemplate.java
index c5d6fdd0498..f66bd652d5a 100644
--- 
a/kogito-jbpm/jbpm-flow-builder/src/main/resources/class-templates/ModelNoIDTemplate.java
+++ 
b/kogito-jbpm/jbpm-flow-builder/src/main/resources/class-templates/ModelNoIDTemplate.java
@@ -24,11 +24,30 @@ import org.kie.kogito.MapOutput;
 
 import java.util.Map;
 import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Set;
 
 import org.kie.kogito.MappableToModel;
 import org.kie.kogito.Model;
 
+import com.fasterxml.jackson.annotation.JsonIgnore;
+
 public class XXXModel implements Model, MapInput, MapInputId, MapOutput,
                                  MappableToModel<$modelClass$> {
 
-}
\ No newline at end of file
+    // Ephemeral REST request-body carrier - always tracks modifications from 
construction.
+    @JsonIgnore
+    private transient final Set<String> __modifiedFields = new HashSet<>();
+
+    @JsonIgnore
+    @Override
+    public Set<String> getModifiedFields() {
+        return __modifiedFields;
+    }
+
+    // Marks a field as explicitly set, so PATCH can tell a null apart from an 
omitted field.
+    void markModified(String field) {
+        __modifiedFields.add(field);
+    }
+
+}
diff --git 
a/kogito-jbpm/jbpm-flow-builder/src/main/resources/class-templates/ModelTemplate.java
 
b/kogito-jbpm/jbpm-flow-builder/src/main/resources/class-templates/ModelTemplate.java
index b33106e9e81..b74cb6e7b38 100644
--- 
a/kogito-jbpm/jbpm-flow-builder/src/main/resources/class-templates/ModelTemplate.java
+++ 
b/kogito-jbpm/jbpm-flow-builder/src/main/resources/class-templates/ModelTemplate.java
@@ -24,20 +24,51 @@ import org.kie.kogito.MapOutput;
 
 import java.util.Map;
 import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Set;
 
 import org.kie.kogito.MappableToModel;
 import org.kie.kogito.Model;
 
+import com.fasterxml.jackson.annotation.JsonIgnore;
+
 public class XXXModel implements org.kie.kogito.Model, MapInput, MapInputId, 
MapOutput, MappableToModel<$modelClass$> {
 
     private String id;
 
+    @JsonIgnore
+    private transient Set<String> __modifiedFields;
+
     public void setId(String id) {
         this.id = id;
     }
-    
+
     public String getId() {
         return this.id;
     }
 
-}
\ No newline at end of file
+    @JsonIgnore
+    @Override
+    public Set<String> getModifiedFields() {
+        return __modifiedFields;
+    }
+
+    // Codegen-internal: carries the modified-fields set from a PATCH Input 
onto
+    // its toModel() target. Left null otherwise, so toMap() includes every 
field.
+    void addModifiedFields(Set<String> fields) {
+        if (fields == null) {
+            return;
+        }
+        if (__modifiedFields == null) {
+            __modifiedFields = new HashSet<>();
+        }
+        __modifiedFields.addAll(fields);
+    }
+
+    // Called by the generated PUT handler before a full-replace update, so 
every
+    // field counts as significant - unlike PATCH, which only touches tracked 
fields.
+    void clearModifiedFields() {
+        __modifiedFields = null;
+    }
+
+}
diff --git a/kogito-jbpm/jbpm-tests/src/test/java/org/jbpm/bpmn2/FlowTest.java 
b/kogito-jbpm/jbpm-tests/src/test/java/org/jbpm/bpmn2/FlowTest.java
index 8c08ed637e1..52a99b74982 100755
--- a/kogito-jbpm/jbpm-tests/src/test/java/org/jbpm/bpmn2/FlowTest.java
+++ b/kogito-jbpm/jbpm-tests/src/test/java/org/jbpm/bpmn2/FlowTest.java
@@ -94,6 +94,8 @@ import 
org.jbpm.bpmn2.flow.MultiInstanceLoopCharacteristicsProcessWithOutputMode
 import 
org.jbpm.bpmn2.flow.MultiInstanceLoopCharacteristicsProcessWithOutputProcess;
 import org.jbpm.bpmn2.flow.MultiInstanceLoopNumberingModel;
 import org.jbpm.bpmn2.flow.MultiInstanceLoopNumberingProcess;
+import org.jbpm.bpmn2.flow.MultiInstanceProcessWithOutputOnTaskModel;
+import org.jbpm.bpmn2.flow.MultiInstanceProcessWithOutputOnTaskProcess;
 import org.jbpm.bpmn2.flow.MultipleGatewaysProcessModel;
 import org.jbpm.bpmn2.flow.MultipleGatewaysProcessProcess;
 import 
org.jbpm.bpmn2.loop.MultiInstanceLoopCharacteristicsProcessWithOutputAndScriptsModel;
@@ -1056,40 +1058,36 @@ public class FlowTest extends JbpmBpmn2TestCase {
 
     @Test
     public void testMultiInstanceLoopCharacteristicsProcess2() throws 
Exception {
-        kruntime = 
createKogitoProcessRuntime("org/jbpm/bpmn2/flow/BPMN2-MultiInstanceProcessWithOutputOnTask.bpmn2");
-
+        Application app = ProcessTestHelper.newApplication();
         TestWorkItemHandler handler = new TestWorkItemHandler();
-        kruntime.getKogitoWorkItemManager().registerWorkItemHandler("Human 
Task", handler);
-        Map<String, Object> params = new HashMap<>();
+        ProcessTestHelper.registerHandler(app, "Human Task", handler);
+        
org.kie.kogito.process.Process<MultiInstanceProcessWithOutputOnTaskModel> 
processDefinition = MultiInstanceProcessWithOutputOnTaskProcess.newProcess(app);
+        MultiInstanceProcessWithOutputOnTaskModel model = 
processDefinition.createModel();
         List<String> myList = new ArrayList<>();
-        List<String> myOutList = null;
         myList.add("John");
         myList.add("Mary");
-        params.put("miinput", myList);
+        model.setMiinput(myList);
 
-        KogitoProcessInstance processInstance = 
kruntime.startProcess("MultiInstanceProcessWithOutputOnTask", params);
+        ProcessInstance<MultiInstanceProcessWithOutputOnTaskModel> 
processInstance = processDefinition.createInstance(model);
+        processInstance.start();
         List<KogitoWorkItem> workItems = handler.getWorkItems();
         assertThat(workItems).isNotNull().hasSize(2);
 
-        myOutList = (List<String>) kruntime.getKieSession().execute(new 
GetProcessVariableCommand(processInstance.getStringId(), "mioutput"));
-        assertThat(myOutList).isNull();
+        assertThat(processInstance.variables().getMioutput()).isNull();
 
         Map<String, Object> results = new HashMap<>();
         results.put("reply", "Hello John");
-        
kruntime.getKogitoWorkItemManager().completeWorkItem(workItems.get(0).getStringId(),
 results);
-        myOutList = (List<String>) kruntime.getKieSession().execute(new 
GetProcessVariableCommand(processInstance.getStringId(), "mioutput"));
-        assertThat(myOutList).isNull();
+        processInstance.completeWorkItem(workItems.get(0).getStringId(), 
results);
+        assertThat(processInstance.variables().getMioutput()).isNull();
 
         results = new HashMap<>();
         results.put("reply", "Hello Mary");
-        
kruntime.getKogitoWorkItemManager().completeWorkItem(workItems.get(1).getStringId(),
 results);
+        processInstance.completeWorkItem(workItems.get(1).getStringId(), 
results);
 
-        myOutList = (List<String>) kruntime.getKieSession().execute(new 
GetProcessVariableCommand(processInstance.getStringId(), "mioutput"));
-        assertThat(myOutList).isNotNull().hasSize(2).contains("Hello John", 
"Hello Mary");
-
-        
kruntime.getKogitoWorkItemManager().completeWorkItem(handler.getWorkItem().getStringId(),
 null);
-        assertProcessInstanceFinished(processInstance, kruntime);
+        
assertThat(processInstance.variables().getMioutput()).isNotNull().hasSize(2).contains("Hello
 John", "Hello Mary");
 
+        processInstance.completeWorkItem(handler.getWorkItem().getStringId(), 
null);
+        
assertThat(processInstance.status()).isEqualTo(ProcessInstance.STATE_COMPLETED);
     }
 
     @Test
diff --git 
a/kogito-quarkus/extensions/kogito-quarkus-rules-extension/kogito-quarkus-rules/src/main/java/org/kie/kogito/core/rules/incubation/quarkus/support/RuleUnitServiceImpl.java
 
b/kogito-quarkus/extensions/kogito-quarkus-rules-extension/kogito-quarkus-rules/src/main/java/org/kie/kogito/core/rules/incubation/quarkus/support/RuleUnitServiceImpl.java
index 5c4e6cd9843..ec1a04f0cd5 100644
--- 
a/kogito-quarkus/extensions/kogito-quarkus-rules-extension/kogito-quarkus-rules/src/main/java/org/kie/kogito/core/rules/incubation/quarkus/support/RuleUnitServiceImpl.java
+++ 
b/kogito-quarkus/extensions/kogito-quarkus-rules-extension/kogito-quarkus-rules/src/main/java/org/kie/kogito/core/rules/incubation/quarkus/support/RuleUnitServiceImpl.java
@@ -20,6 +20,7 @@ package org.kie.kogito.core.rules.incubation.quarkus.support;
 
 import java.util.List;
 import java.util.Map;
+import java.util.regex.Pattern;
 import java.util.stream.Stream;
 
 import org.drools.ruleunits.api.RuleUnit;
@@ -36,6 +37,9 @@ import 
org.kie.kogito.incubation.rules.services.RuleUnitService;
 
 class RuleUnitServiceImpl implements RuleUnitService {
 
+    // CWE-470: validate class name is a legal Java FQCN before passing to 
loadClass
+    private static final Pattern SAFE_CLASS_NAME = 
Pattern.compile("[\\w$]+(\\.[\\w$]+)*");
+
     private final RuleUnits ruleUnits;
 
     public RuleUnitServiceImpl(RuleUnits ruleUnits) {
@@ -64,12 +68,16 @@ class RuleUnitServiceImpl implements RuleUnitService {
     }
 
     private RuleUnitData convertValue(Map<String, Object> payload, RuleUnitId 
ruleUnitId) {
+        String className = ruleUnitId.ruleUnitId();
+        if (!SAFE_CLASS_NAME.matcher(className).matches()) {
+            throw new IllegalArgumentException("Invalid rule unit class name: 
" + className);
+        }
         try {
             // converts the identifier into a Class object for conversion
-            Class<RuleUnitData> type = (Class<RuleUnitData>) 
Thread.currentThread().getContextClassLoader().loadClass(ruleUnitId.ruleUnitId());
+            Class<RuleUnitData> type = (Class<RuleUnitData>) 
Thread.currentThread().getContextClassLoader().loadClass(className);
             return InternalObjectMapper.objectMapper().convertValue(payload, 
type);
         } catch (ClassNotFoundException e) {
-            throw new IllegalArgumentException("Cannot load class " + 
ruleUnitId.ruleUnitId(), e);
+            throw new IllegalArgumentException("Cannot load class " + 
className, e);
         }
     }
 
diff --git 
a/kogito-quarkus/extensions/kogito-quarkus-rules-extension/kogito-quarkus-rules/src/main/java/org/kie/kogito/core/rules/incubation/quarkus/support/StatefulRuleUnitServiceImpl.java
 
b/kogito-quarkus/extensions/kogito-quarkus-rules-extension/kogito-quarkus-rules/src/main/java/org/kie/kogito/core/rules/incubation/quarkus/support/StatefulRuleUnitServiceImpl.java
index 59588caba9e..46ccdf5e1f6 100644
--- 
a/kogito-quarkus/extensions/kogito-quarkus-rules-extension/kogito-quarkus-rules/src/main/java/org/kie/kogito/core/rules/incubation/quarkus/support/StatefulRuleUnitServiceImpl.java
+++ 
b/kogito-quarkus/extensions/kogito-quarkus-rules-extension/kogito-quarkus-rules/src/main/java/org/kie/kogito/core/rules/incubation/quarkus/support/StatefulRuleUnitServiceImpl.java
@@ -21,6 +21,7 @@ package org.kie.kogito.core.rules.incubation.quarkus.support;
 import java.util.List;
 import java.util.Map;
 import java.util.UUID;
+import java.util.regex.Pattern;
 import java.util.stream.Stream;
 
 import org.drools.ruleunits.api.RuleUnit;
@@ -41,6 +42,9 @@ import 
org.kie.kogito.incubation.rules.services.StatefulRuleUnitService;
 
 class StatefulRuleUnitServiceImpl implements StatefulRuleUnitService {
 
+    // CWE-470: validate class name is a legal Java FQCN before passing to 
loadClass
+    private static final Pattern SAFE_CLASS_NAME = 
Pattern.compile("[\\w$]+(\\.[\\w$]+)*");
+
     private final RuleUnits ruleUnits;
 
     public StatefulRuleUnitServiceImpl(RuleUnits ruleUnits) {
@@ -71,10 +75,14 @@ class StatefulRuleUnitServiceImpl implements 
StatefulRuleUnitService {
     }
 
     private Class<RuleUnitData> toClass(RuleUnitId ruleUnitId) {
+        String className = ruleUnitId.ruleUnitId();
+        if (!SAFE_CLASS_NAME.matcher(className).matches()) {
+            throw new IllegalArgumentException("Invalid rule unit class name: 
" + className);
+        }
         try {
-            return (Class<RuleUnitData>) 
Thread.currentThread().getContextClassLoader().loadClass(ruleUnitId.ruleUnitId());
+            return (Class<RuleUnitData>) 
Thread.currentThread().getContextClassLoader().loadClass(className);
         } catch (ClassNotFoundException e) {
-            throw new IllegalArgumentException(e);
+            throw new IllegalArgumentException("Cannot load class " + 
className, e);
         }
     }
 
diff --git 
a/kogito-quarkus/integration-tests/integration-tests-quarkus-processes/src/test/java/org/kie/kogito/integrationtests/quarkus/BasicRestIT.java
 
b/kogito-quarkus/integration-tests/integration-tests-quarkus-processes/src/test/java/org/kie/kogito/integrationtests/quarkus/BasicRestIT.java
index 48fc895af4d..39f5268b841 100644
--- 
a/kogito-quarkus/integration-tests/integration-tests-quarkus-processes/src/test/java/org/kie/kogito/integrationtests/quarkus/BasicRestIT.java
+++ 
b/kogito-quarkus/integration-tests/integration-tests-quarkus-processes/src/test/java/org/kie/kogito/integrationtests/quarkus/BasicRestIT.java
@@ -252,6 +252,92 @@ class BasicRestIT {
         assertExpectedUnitOfWorkEvents(2);
     }
 
+    @Test
+    void testPatchWithNullValue() {
+        String id = given()
+                .contentType(ContentType.JSON)
+                .when()
+                .body(getParams())
+                .post("/AdHocFragments")
+                .then()
+                .statusCode(201)
+                .header("Location", not(emptyOrNullString()))
+                .body("id", not(emptyOrNullString()))
+                .body("var1", equalTo("Kermit"))
+                .body("var2", equalTo(34))
+                .extract()
+                .path("id");
+
+        // var1 set to null must be cleared, unlike an omitted field
+        given()
+                .contentType(ContentType.JSON)
+                .when()
+                .body("{\"var1\": null}")
+                .patch("/AdHocFragments/{customId}", id)
+                .then()
+                .statusCode(200)
+                .body("id", equalTo(id))
+                .body("var1", nullValue())
+                .body("var2", is(34));
+
+        assertExpectedUnitOfWorkEvents(2);
+    }
+
+    @Test
+    void testPatchAndPutSequence() {
+        String id = given()
+                .contentType(ContentType.JSON)
+                .when()
+                .body(getParams())
+                .post("/AdHocFragments")
+                .then()
+                .statusCode(201)
+                .header("Location", not(emptyOrNullString()))
+                .body("id", not(emptyOrNullString()))
+                .body("var1", equalTo("Kermit"))
+                .body("var2", equalTo(34))
+                .extract()
+                .path("id");
+
+        // PATCH: var1 explicitly nulled, var2 untouched
+        given()
+                .contentType(ContentType.JSON)
+                .when()
+                .body("{\"var1\": null}")
+                .patch("/AdHocFragments/{customId}", id)
+                .then()
+                .statusCode(200)
+                .body("id", equalTo(id))
+                .body("var1", nullValue())
+                .body("var2", is(34));
+
+        // PUT right after a PATCH on the same resource: var2 omitted -> must 
still be nulled out
+        given()
+                .contentType(ContentType.JSON)
+                .when()
+                .body("{\"var1\": \"Gonzo\"}")
+                .put("/AdHocFragments/{customId}", id)
+                .then()
+                .statusCode(200)
+                .body("id", equalTo(id))
+                .body("var1", equalTo("Gonzo"))
+                .body("var2", nullValue());
+
+        // PATCH right after a PUT: empty body must leave both fields untouched
+        given()
+                .contentType(ContentType.JSON)
+                .when()
+                .body("{}")
+                .patch("/AdHocFragments/{customId}", id)
+                .then()
+                .statusCode(200)
+                .body("id", equalTo(id))
+                .body("var1", equalTo("Gonzo"))
+                .body("var2", nullValue());
+
+        assertExpectedUnitOfWorkEvents(4);
+    }
+
     @Test
     void testDelete() {
         Map<String, String> params = new HashMap<>();
diff --git 
a/kogito-quarkus/integration-tests/integration-tests-quarkus-processes/src/test/java/org/kie/kogito/integrationtests/quarkus/OASIT.java
 
b/kogito-quarkus/integration-tests/integration-tests-quarkus-processes/src/test/java/org/kie/kogito/integrationtests/quarkus/OASIT.java
index 50575452aef..9db03fe82fb 100644
--- 
a/kogito-quarkus/integration-tests/integration-tests-quarkus-processes/src/test/java/org/kie/kogito/integrationtests/quarkus/OASIT.java
+++ 
b/kogito-quarkus/integration-tests/integration-tests-quarkus-processes/src/test/java/org/kie/kogito/integrationtests/quarkus/OASIT.java
@@ -27,6 +27,7 @@ import io.quarkus.test.junit.QuarkusIntegrationTest;
 import io.restassured.RestAssured;
 import io.swagger.v3.oas.models.OpenAPI;
 import io.swagger.v3.oas.models.PathItem;
+import io.swagger.v3.oas.models.media.Schema;
 import io.swagger.v3.parser.OpenAPIV3Parser;
 import io.swagger.v3.parser.core.models.ParseOptions;
 import io.swagger.v3.parser.core.models.SwaggerParseResult;
@@ -60,4 +61,24 @@ class OASIT {
         assertThat(p1.getPost()).isNotNull();
     }
 
+    @Test
+    public void testApprovalsSchemaProperties() {
+        String url = rootUrl.toString() + "/q/openapi";
+        ParseOptions parseOptions = new ParseOptions();
+        parseOptions.setResolve(true);
+        SwaggerParseResult result = new OpenAPIV3Parser().readLocation(url, 
null, parseOptions);
+
+        OpenAPI openAPI = result.getOpenAPI();
+        PathItem p1 = openAPI.getPaths().get("/approvals");
+        assertThat(p1).isNotNull();
+
+        Schema requestSchema = 
p1.getPost().getRequestBody().getContent().get("application/json").getSchema();
+        String schemaName = 
requestSchema.get$ref().substring(requestSchema.get$ref().lastIndexOf('/') + 1);
+        Schema resolvedSchema = 
openAPI.getComponents().getSchemas().get(schemaName);
+        assertThat(resolvedSchema).isNotNull();
+
+        assertThat(resolvedSchema.getProperties().keySet())
+                .containsExactlyInAnyOrder("approver", "traveller", 
"firstLineApproval", "secondLineApproval");
+    }
+
 }
diff --git a/kogito-springboot/bom/pom.xml b/kogito-springboot/bom/pom.xml
index 20b87c5cb60..3b913a1158b 100644
--- a/kogito-springboot/bom/pom.xml
+++ b/kogito-springboot/bom/pom.xml
@@ -41,7 +41,7 @@
     <!-- This is needed to override the default version inherited from 
kie-parent-drools -->
     <version.mongodb-driver-sync>5.6.4</version.mongodb-driver-sync>
     <version.org.springdoc>2.8.13</version.org.springdoc>
-    <version.org.springframework>7.0.7</version.org.springframework>
+    <version.org.springframework>7.0.8</version.org.springframework>
     <version.org.springframework.boot>4.0.7</version.org.springframework.boot>
     <!-- Spring Boot Cloud aligned with Spring Boot Framework version. See: 
https://spring.io/projects/spring-cloud -->
     <!-- 2025.1.x (Oakwood) is the first Spring Cloud train compatible with 
Spring Boot 4.0.x;
diff --git 
a/kogito-springboot/integration-tests/integration-tests-springboot-processes-it/src/main/resources/AdHocFragments.bpmn
 
b/kogito-springboot/integration-tests/integration-tests-springboot-processes-it/src/main/resources/AdHocFragments.bpmn
index a9bdf9430c1..02675c1d611 100644
--- 
a/kogito-springboot/integration-tests/integration-tests-springboot-processes-it/src/main/resources/AdHocFragments.bpmn
+++ 
b/kogito-springboot/integration-tests/integration-tests-springboot-processes-it/src/main/resources/AdHocFragments.bpmn
@@ -19,6 +19,7 @@
   -->
 <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:bpsim="http://www.bpsim.org/schemas/1.0"; 
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="_WL3jkCdwED2KYMa5towL6A" 
xsi:schemaLocation="http://www.omg.org/spec/BPMN/20100524/MODEL BPMN20.xsd 
http://www. [...]
   <bpmn2:itemDefinition id="_var1Item" structureRef="String"/>
+  <bpmn2:itemDefinition id="_var2Item" structureRef="Integer"/>
   <bpmn2:itemDefinition 
id="__731518E6-88A2-4CCC-A2C0-97C45189A7F3_ConditionInputXItem" 
structureRef="String"/>
   <bpmn2:itemDefinition 
id="__17762652-67B8-46FF-ADFE-D7AF3EA2ADAB_SkippableInputXItem" 
structureRef="Object"/>
   <bpmn2:itemDefinition 
id="__17762652-67B8-46FF-ADFE-D7AF3EA2ADAB_PriorityInputXItem" 
structureRef="Object"/>
@@ -64,6 +65,7 @@
   </bpmn2:collaboration>
   <bpmn2:process id="TestCase.AdHocFragments" 
drools:packageName="org.kie.kogito" drools:version="1.0" drools:adHoc="true" 
name="AdHocFragments" isExecutable="true" processType="Public">
     <bpmn2:property id="var1" itemSubjectRef="_var1Item" name="var1"/>
+    <bpmn2:property id="var2" itemSubjectRef="_var2Item" name="var2"/>
     <bpmn2:sequenceFlow id="_830D53F8-A344-45AE-A1A1-2173DD983A43" 
sourceRef="_89FFDFF9-8D8A-4B86-B48C-76F30299D8D3" 
targetRef="_319B612D-CD70-4FB0-8390-2F825115847E">
       <bpmn2:extensionElements>
         <drools:metaData name="isAutoConnection.source">
diff --git 
a/kogito-springboot/integration-tests/integration-tests-springboot-processes-it/src/test/java/org/kie/kogito/integrationtests/springboot/BasicRestTest.java
 
b/kogito-springboot/integration-tests/integration-tests-springboot-processes-it/src/test/java/org/kie/kogito/integrationtests/springboot/BasicRestTest.java
index db13274bf8a..c4655363d4c 100644
--- 
a/kogito-springboot/integration-tests/integration-tests-springboot-processes-it/src/test/java/org/kie/kogito/integrationtests/springboot/BasicRestTest.java
+++ 
b/kogito-springboot/integration-tests/integration-tests-springboot-processes-it/src/test/java/org/kie/kogito/integrationtests/springboot/BasicRestTest.java
@@ -38,6 +38,7 @@ import static org.hamcrest.CoreMatchers.equalTo;
 import static org.hamcrest.CoreMatchers.is;
 import static org.hamcrest.CoreMatchers.not;
 import static org.hamcrest.Matchers.emptyOrNullString;
+import static org.hamcrest.Matchers.nullValue;
 import static org.junit.jupiter.api.Assertions.assertNotEquals;
 
 @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, 
classes = KogitoSpringbootApplication.class)
@@ -207,6 +208,164 @@ class BasicRestTest extends BaseRestTest {
         assertExpectedUnitOfWorkEvents(2);
     }
 
+    @Test
+    void testUpdateNullsOmittedField() {
+        Map<String, Object> params = new HashMap<>();
+        params.put("var1", "Kermit");
+        params.put("var2", 34);
+
+        String id = given()
+                .contentType(ContentType.JSON)
+                .when()
+                .body(params)
+                .post("/AdHocFragments")
+                .then()
+                .statusCode(201)
+                .header("Location", not(emptyOrNullString()))
+                .body("id", not(emptyOrNullString()))
+                .body("var1", equalTo("Kermit"))
+                .body("var2", equalTo(34))
+                .extract()
+                .path("id");
+
+        // var2 omitted from PUT body -> full replace must null it
+        given()
+                .contentType(ContentType.JSON)
+                .when()
+                .body("{\"var1\": \"Gonzo\"}")
+                .put("/AdHocFragments/{customId}", id)
+                .then()
+                .statusCode(200)
+                .body("id", equalTo(id))
+                .body("var1", equalTo("Gonzo"))
+                .body("var2", nullValue());
+
+        assertExpectedUnitOfWorkEvents(2);
+    }
+
+    @Test
+    void testPatch() {
+        Map<String, String> params = new HashMap<>();
+        params.put("var1", "Kermit");
+
+        String id = given()
+                .contentType(ContentType.JSON)
+                .when()
+                .body(params)
+                .post("/AdHocFragments")
+                .then()
+                .statusCode(201)
+                .header("Location", not(emptyOrNullString()))
+                .body("id", not(emptyOrNullString()))
+                .body("var1", equalTo("Kermit"))
+                .extract()
+                .path("id");
+
+        // An empty PATCH body must leave existing variables untouched.
+        given()
+                .contentType(ContentType.JSON)
+                .when()
+                .body("{}")
+                .patch("/AdHocFragments/{customId}", id)
+                .then()
+                .statusCode(200)
+                .body("id", equalTo(id))
+                .body("var1", equalTo("Kermit"));
+
+        assertExpectedUnitOfWorkEvents(2);
+    }
+
+    @Test
+    void testPatchWithNullValue() {
+        Map<String, String> params = new HashMap<>();
+        params.put("var1", "Kermit");
+
+        String id = given()
+                .contentType(ContentType.JSON)
+                .when()
+                .body(params)
+                .post("/AdHocFragments")
+                .then()
+                .statusCode(201)
+                .header("Location", not(emptyOrNullString()))
+                .body("id", not(emptyOrNullString()))
+                .body("var1", equalTo("Kermit"))
+                .extract()
+                .path("id");
+
+        // var1 set to null must be cleared, unlike an omitted field
+        given()
+                .contentType(ContentType.JSON)
+                .when()
+                .body("{\"var1\": null}")
+                .patch("/AdHocFragments/{customId}", id)
+                .then()
+                .statusCode(200)
+                .body("id", equalTo(id))
+                .body("var1", nullValue());
+
+        assertExpectedUnitOfWorkEvents(2);
+    }
+
+    @Test
+    void testPatchAndPutSequence() {
+        Map<String, Object> params = new HashMap<>();
+        params.put("var1", "Kermit");
+        params.put("var2", 34);
+
+        String id = given()
+                .contentType(ContentType.JSON)
+                .when()
+                .body(params)
+                .post("/AdHocFragments")
+                .then()
+                .statusCode(201)
+                .header("Location", not(emptyOrNullString()))
+                .body("id", not(emptyOrNullString()))
+                .body("var1", equalTo("Kermit"))
+                .body("var2", equalTo(34))
+                .extract()
+                .path("id");
+
+        // PATCH: var1 explicitly nulled, var2 untouched
+        given()
+                .contentType(ContentType.JSON)
+                .when()
+                .body("{\"var1\": null}")
+                .patch("/AdHocFragments/{customId}", id)
+                .then()
+                .statusCode(200)
+                .body("id", equalTo(id))
+                .body("var1", nullValue())
+                .body("var2", equalTo(34));
+
+        // PUT right after a PATCH on the same resource: var2 omitted -> must 
still be nulled out
+        given()
+                .contentType(ContentType.JSON)
+                .when()
+                .body("{\"var1\": \"Gonzo\"}")
+                .put("/AdHocFragments/{customId}", id)
+                .then()
+                .statusCode(200)
+                .body("id", equalTo(id))
+                .body("var1", equalTo("Gonzo"))
+                .body("var2", nullValue());
+
+        // PATCH right after a PUT: empty body must leave both fields untouched
+        given()
+                .contentType(ContentType.JSON)
+                .when()
+                .body("{}")
+                .patch("/AdHocFragments/{customId}", id)
+                .then()
+                .statusCode(200)
+                .body("id", equalTo(id))
+                .body("var1", equalTo("Gonzo"))
+                .body("var2", nullValue());
+
+        assertExpectedUnitOfWorkEvents(4);
+    }
+
     @Test
     void testDelete() {
         Map<String, String> params = new HashMap<>();


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to