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

tkobayas 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 3059b2237a0 [incubator-kie-issues#2419] fix(drools): collect facts, 
not subnetwork tuples, in GenericCollectAccumulator (#7224)
3059b2237a0 is described below

commit 3059b2237a05af13884adeeb423455282e42b091
Author: Thebas <[email protected]>
AuthorDate: Mon Oct 26 07:38:57 2026 +0000

    [incubator-kie-issues#2419] fix(drools): collect facts, not subnetwork 
tuples, in GenericCollectAccumulator (#7224)
    
    With the executable model, `from collect` into a concrete collection class
    (ArrayList, LinkedList, HashSet, custom classes) goes through
    GenericCollectAccumulator. When the collected pattern runs in a subnetwork
    (an inner `from`, `from accumulate` or nested `from collect`), accumulate()
    added the SubnetworkTuple itself to the collection instead of the matched
    fact. Rules reading the elements broke, and marshalling the session failed
    with NotSerializableException on AccumulateNode$AccumulateContext.
    
    Unwrap the tuple through its fact handle, as the classic CollectAccumulator
    does.
    
    Co-authored-by: MARTINS FERRAZ Andre <[email protected]>
---
 .../model/codegen/execmodel/AccumulateTest.java    | 342 +++++++++++++++++++++
 .../constraints/GenericCollectAccumulator.java     |   5 +
 .../compiler/integrationtests/AccumulateTest.java  |  45 +++
 3 files changed, 392 insertions(+)

diff --git 
a/drools-model/drools-model-codegen/src/test/java/org/drools/model/codegen/execmodel/AccumulateTest.java
 
b/drools-model/drools-model-codegen/src/test/java/org/drools/model/codegen/execmodel/AccumulateTest.java
index 0ca109467f1..08ea148dcca 100644
--- 
a/drools-model/drools-model-codegen/src/test/java/org/drools/model/codegen/execmodel/AccumulateTest.java
+++ 
b/drools-model/drools-model-codegen/src/test/java/org/drools/model/codegen/execmodel/AccumulateTest.java
@@ -52,6 +52,7 @@ import org.drools.core.rule.consequence.InternalMatch;
 import org.drools.model.functions.accumulate.GroupKey;
 import org.junit.jupiter.params.ParameterizedTest;
 import org.junit.jupiter.params.provider.MethodSource;
+import org.drools.model.codegen.execmodel.domain.Address;
 import org.drools.model.codegen.execmodel.domain.Adult;
 import org.drools.model.codegen.execmodel.domain.Child;
 import org.drools.model.codegen.execmodel.domain.Customer;
@@ -4235,6 +4236,347 @@ public class AccumulateTest extends BaseModelTest {
         }
     }
 
+    @ParameterizedTest
+       @MethodSource("parameters")
+    public void testCollectIntoArrayListFromSubnetwork(RUN_TYPE runType) {
+        String str = "import " + Person.class.getCanonicalName() + ";\n" +
+                     "import " + Address.class.getCanonicalName() + ";\n" +
+                     "import " + ArrayList.class.getCanonicalName() + ";\n" +
+                     "global java.util.List result;\n" +
+                     "rule R when\n" +
+                     "  $p: Person()\n" +
+                     "  $list: ArrayList() from collect (\n" +
+                     "    Address() from $p.addresses\n" +
+                     "  )\n" +
+                     "then\n" +
+                     "  result.addAll($list);\n" +
+                     "end";
+
+        KieSession ksession = getKieSession(runType, str);
+        List<Object> result = new ArrayList<>();
+        ksession.setGlobal("result", result);
+
+        Address rome = new Address("Rome");
+        Address milan = new Address("Milan");
+        Person mario = new Person("Mario", 40);
+        mario.addAddress(rome);
+        mario.addAddress(milan);
+        ksession.insert(mario);
+
+        ksession.fireAllRules();
+
+        // the collected list must hold the facts, not the subnetwork tuples 
wrapping them
+        assertThat(result).hasOnlyElementsOfType(Address.class);
+        assertThat(result).containsExactlyInAnyOrder(rome, milan);
+    }
+
+    // a custom concrete Collection target routed through 
GenericCollectAccumulator;
+    // it needs a public no-arg constructor because the generic path uses 
CustomType::new as a Supplier
+    public static class MyAddressCollection extends ArrayList<Object> {
+        public MyAddressCollection() {
+            super();
+        }
+    }
+
+    @ParameterizedTest
+       @MethodSource("parameters")
+    public void testCollectIntoSetFromSubnetwork(RUN_TYPE runType) {
+        // Set target: routed through the collectSet path in FromCollectVisitor
+        String str = "import " + Person.class.getCanonicalName() + ";\n" +
+                     "import " + Address.class.getCanonicalName() + ";\n" +
+                     "global java.util.List result;\n" +
+                     "rule R when\n" +
+                     "  $p: Person()\n" +
+                     "  $set: java.util.Set() from collect (\n" +
+                     "    Address() from $p.addresses\n" +
+                     "  )\n" +
+                     "then\n" +
+                     "  result.addAll($set);\n" +
+                     "end";
+
+        KieSession ksession = getKieSession(runType, str);
+        List<Object> result = new ArrayList<>();
+        ksession.setGlobal("result", result);
+
+        Address rome = new Address("Rome");
+        Address milan = new Address("Milan");
+        Person mario = new Person("Mario", 40);
+        mario.addAddress(rome);
+        mario.addAddress(milan);
+        ksession.insert(mario);
+
+        ksession.fireAllRules();
+
+        // the collected set must hold the facts, not the subnetwork tuples 
wrapping them
+        assertThat(result).hasOnlyElementsOfType(Address.class);
+        assertThat(result).containsExactlyInAnyOrder(rome, milan);
+    }
+
+    @ParameterizedTest
+       @MethodSource("parameters")
+    public void testCollectIntoHashSetFromSubnetwork(RUN_TYPE runType) {
+        // HashSet is a concrete type that is not List/Collection/Set, so it 
is routed
+        // through GenericCollectAccumulator
+        String str = "import " + Person.class.getCanonicalName() + ";\n" +
+                     "import " + Address.class.getCanonicalName() + ";\n" +
+                     "import " + HashSet.class.getCanonicalName() + ";\n" +
+                     "global java.util.List result;\n" +
+                     "rule R when\n" +
+                     "  $p: Person()\n" +
+                     "  $set: HashSet() from collect (\n" +
+                     "    Address() from $p.addresses\n" +
+                     "  )\n" +
+                     "then\n" +
+                     "  result.addAll($set);\n" +
+                     "end";
+
+        KieSession ksession = getKieSession(runType, str);
+        List<Object> result = new ArrayList<>();
+        ksession.setGlobal("result", result);
+
+        Address rome = new Address("Rome");
+        Address milan = new Address("Milan");
+        Person mario = new Person("Mario", 40);
+        mario.addAddress(rome);
+        mario.addAddress(milan);
+        ksession.insert(mario);
+
+        ksession.fireAllRules();
+
+        assertThat(result).hasOnlyElementsOfType(Address.class);
+        assertThat(result).containsExactlyInAnyOrder(rome, milan);
+    }
+
+    @ParameterizedTest
+       @MethodSource("parameters")
+    public void testCollectIntoLinkedListFromSubnetwork(RUN_TYPE runType) {
+        // LinkedList is a concrete type that is not List/Collection/Set, so 
it is routed
+        // through GenericCollectAccumulator
+        String str = "import " + Person.class.getCanonicalName() + ";\n" +
+                     "import " + Address.class.getCanonicalName() + ";\n" +
+                     "import " + java.util.LinkedList.class.getCanonicalName() 
+ ";\n" +
+                     "global java.util.List result;\n" +
+                     "rule R when\n" +
+                     "  $p: Person()\n" +
+                     "  $list: LinkedList() from collect (\n" +
+                     "    Address() from $p.addresses\n" +
+                     "  )\n" +
+                     "then\n" +
+                     "  result.addAll($list);\n" +
+                     "end";
+
+        KieSession ksession = getKieSession(runType, str);
+        List<Object> result = new ArrayList<>();
+        ksession.setGlobal("result", result);
+
+        Address rome = new Address("Rome");
+        Address milan = new Address("Milan");
+        Person mario = new Person("Mario", 40);
+        mario.addAddress(rome);
+        mario.addAddress(milan);
+        ksession.insert(mario);
+
+        ksession.fireAllRules();
+
+        assertThat(result).hasOnlyElementsOfType(Address.class);
+        assertThat(result).containsExactlyInAnyOrder(rome, milan);
+    }
+
+    @ParameterizedTest
+       @MethodSource("parameters")
+    public void testCollectIntoCustomCollectionFromSubnetwork(RUN_TYPE 
runType) {
+        // a user-defined concrete Collection goes through 
GenericCollectAccumulator, which
+        // instantiates it via a no-arg constructor supplier (CustomType::new)
+        String str = "import " + Person.class.getCanonicalName() + ";\n" +
+                     "import " + Address.class.getCanonicalName() + ";\n" +
+                     "import " + MyAddressCollection.class.getCanonicalName() 
+ ";\n" +
+                     "global java.util.List result;\n" +
+                     "rule R when\n" +
+                     "  $p: Person()\n" +
+                     "  $coll: MyAddressCollection() from collect (\n" +
+                     "    Address() from $p.addresses\n" +
+                     "  )\n" +
+                     "then\n" +
+                     "  result.addAll($coll);\n" +
+                     "end";
+
+        KieSession ksession = getKieSession(runType, str);
+        List<Object> result = new ArrayList<>();
+        ksession.setGlobal("result", result);
+
+        Address rome = new Address("Rome");
+        Address milan = new Address("Milan");
+        Person mario = new Person("Mario", 40);
+        mario.addAddress(rome);
+        mario.addAddress(milan);
+        ksession.insert(mario);
+
+        ksession.fireAllRules();
+
+        assertThat(result).hasOnlyElementsOfType(Address.class);
+        assertThat(result).containsExactlyInAnyOrder(rome, milan);
+    }
+
+    @ParameterizedTest
+       @MethodSource("parameters")
+    public void testCollectIntoArrayListFromSubnetworkWithUpdate(RUN_TYPE 
runType) {
+        // after removing an address and updating the Person through the 
subnetwork, the
+        // collected ArrayList must reflect only the remaining address, with 
no leaked
+        // SubnetworkTuple and no stale element
+        String str = "import " + Person.class.getCanonicalName() + ";\n" +
+                     "import " + Address.class.getCanonicalName() + ";\n" +
+                     "import " + ArrayList.class.getCanonicalName() + ";\n" +
+                     "global java.util.List result;\n" +
+                     "rule R when\n" +
+                     "  $p: Person()\n" +
+                     "  $list: ArrayList() from collect (\n" +
+                     "    Address() from $p.addresses\n" +
+                     "  )\n" +
+                     "then\n" +
+                     "  result.clear();\n" +
+                     "  result.addAll($list);\n" +
+                     "end";
+
+        KieSession ksession = getKieSession(runType, str);
+        List<Object> result = new ArrayList<>();
+        ksession.setGlobal("result", result);
+
+        Address rome = new Address("Rome");
+        Address milan = new Address("Milan");
+        Person mario = new Person("Mario", 40);
+        mario.addAddress(rome);
+        mario.addAddress(milan);
+        FactHandle marioFH = ksession.insert(mario);
+
+        ksession.fireAllRules();
+
+        assertThat(result).hasOnlyElementsOfType(Address.class);
+        assertThat(result).containsExactlyInAnyOrder(rome, milan);
+
+        // drop one address and re-evaluate through the subnetwork
+        mario.getAddresses().remove(milan);
+        ksession.update(marioFH, mario);
+        ksession.fireAllRules();
+
+        assertThat(result).hasOnlyElementsOfType(Address.class);
+        assertThat(result).containsExactly(rome);
+    }
+
+    @ParameterizedTest
+       @MethodSource("parameters")
+    public void testCollectIntoArrayListFromSubnetworkWithDelete(RUN_TYPE 
runType) {
+        // deleting the Person must retract the whole subnetwork match; the 
rule should no
+        // longer fire for it and the collected content must reflect the 
deletion
+        String str = "import " + Person.class.getCanonicalName() + ";\n" +
+                     "import " + Address.class.getCanonicalName() + ";\n" +
+                     "import " + ArrayList.class.getCanonicalName() + ";\n" +
+                     "global java.util.List result;\n" +
+                     "rule R when\n" +
+                     "  $p: Person()\n" +
+                     "  $list: ArrayList() from collect (\n" +
+                     "    Address() from $p.addresses\n" +
+                     "  )\n" +
+                     "then\n" +
+                     "  result.clear();\n" +
+                     "  result.addAll($list);\n" +
+                     "end";
+
+        KieSession ksession = getKieSession(runType, str);
+        List<Object> result = new ArrayList<>();
+        ksession.setGlobal("result", result);
+
+        Address rome = new Address("Rome");
+        Address milan = new Address("Milan");
+        Person mario = new Person("Mario", 40);
+        mario.addAddress(rome);
+        mario.addAddress(milan);
+        FactHandle marioFH = ksession.insert(mario);
+
+        int fired = ksession.fireAllRules();
+        assertThat(fired).isEqualTo(1);
+        assertThat(result).hasOnlyElementsOfType(Address.class);
+        assertThat(result).containsExactlyInAnyOrder(rome, milan);
+
+        // deleting the person removes the only match: the rule must not fire 
again
+        ksession.delete(marioFH);
+        assertThat(ksession.fireAllRules()).isEqualTo(0);
+    }
+
+    @ParameterizedTest
+       @MethodSource("parameters")
+    public void 
testCollectIntoArrayListFromSubnetworkWithInnerAccumulate(RUN_TYPE runType) {
+        // the inner source of the collect is a pattern sourced from 
accumulate, which also
+        // builds a subnetwork; ArrayList target exercises the 
GenericCollectAccumulator path.
+        // count(...) yields a single Long equal to the number of Address 
facts (2), so the
+        // outer collect gathers exactly that one Long - not the 
SubnetworkTuple wrapping it.
+        String str = "import " + Person.class.getCanonicalName() + ";\n" +
+                     "import " + Address.class.getCanonicalName() + ";\n" +
+                     "import " + ArrayList.class.getCanonicalName() + ";\n" +
+                     "global java.util.List result;\n" +
+                     "rule R when\n" +
+                     "  $list: ArrayList() from collect (\n" +
+                     "    Long() from accumulate ( $a: Address(); count($a) 
)\n" +
+                     "  )\n" +
+                     "then\n" +
+                     "  result.addAll($list);\n" +
+                     "end";
+
+        KieSession ksession = getKieSession(runType, str);
+        List<Object> result = new ArrayList<>();
+        ksession.setGlobal("result", result);
+
+        ksession.insert(new Address("Rome"));
+        ksession.insert(new Address("Milan"));
+
+        ksession.fireAllRules();
+
+        // the collected list must hold the Long produced by the inner 
accumulate,
+        // not the subnetwork tuple wrapping it
+        assertThat(result).hasOnlyElementsOfType(Long.class);
+        assertThat(result).containsExactly(2L);
+    }
+
+    @ParameterizedTest
+       @MethodSource("parameters")
+    public void 
testCollectIntoArrayListFromSubnetworkWithNestedCollect(RUN_TYPE runType) {
+        // the inner source of the collect is itself a "from collect", which 
builds a
+        // subnetwork too. The inner collect produces a single ArrayList 
holding the Address
+        // facts; the outer ArrayList (GenericCollectAccumulator path) must 
gather that one
+        // inner list as its single element - not the SubnetworkTuple wrapping 
it.
+        String str = "import " + Person.class.getCanonicalName() + ";\n" +
+                     "import " + Address.class.getCanonicalName() + ";\n" +
+                     "import " + ArrayList.class.getCanonicalName() + ";\n" +
+                     "global java.util.List result;\n" +
+                     "rule R when\n" +
+                     "  $outer: ArrayList() from collect (\n" +
+                     "    ArrayList() from collect (\n" +
+                     "      Address()\n" +
+                     "    )\n" +
+                     "  )\n" +
+                     "then\n" +
+                     "  result.addAll($outer);\n" +
+                     "end";
+
+        KieSession ksession = getKieSession(runType, str);
+        List<Object> result = new ArrayList<>();
+        ksession.setGlobal("result", result);
+
+        Address rome = new Address("Rome");
+        Address milan = new Address("Milan");
+        ksession.insert(rome);
+        ksession.insert(milan);
+
+        ksession.fireAllRules();
+
+        // the outer list holds exactly one element: the ArrayList produced by 
the inner
+        // collect, not the subnetwork tuple wrapping it
+        assertThat(result).hasOnlyElementsOfType(ArrayList.class);
+        assertThat(result).hasSize(1);
+        assertThat((ArrayList<Object>) result.get(0))
+                .hasOnlyElementsOfType(Address.class)
+                .containsExactlyInAnyOrder(rome, milan);
+    }
+
     @ParameterizedTest
        @MethodSource("parameters")
     public void testExistsFromAccumulate(RUN_TYPE runType) {
diff --git 
a/drools-model/drools-model-compiler/src/main/java/org/drools/modelcompiler/constraints/GenericCollectAccumulator.java
 
b/drools-model/drools-model-compiler/src/main/java/org/drools/modelcompiler/constraints/GenericCollectAccumulator.java
index 14497755e0b..ecdefcd95f2 100644
--- 
a/drools-model/drools-model-compiler/src/main/java/org/drools/modelcompiler/constraints/GenericCollectAccumulator.java
+++ 
b/drools-model/drools-model-compiler/src/main/java/org/drools/modelcompiler/constraints/GenericCollectAccumulator.java
@@ -22,6 +22,7 @@ import org.drools.base.base.ValueResolver;
 import org.drools.base.reteoo.BaseTuple;
 import org.drools.base.rule.Declaration;
 import org.drools.base.rule.accessor.Accumulator;
+import org.drools.core.reteoo.SubnetworkTuple;
 import org.kie.api.runtime.rule.FactHandle;
 
 import java.util.Collection;
@@ -59,6 +60,10 @@ public class GenericCollectAccumulator implements 
Accumulator {
                              Declaration[] innerDeclarations,
                              ValueResolver valueResolver) {
         Object value = handle.getObject();
+        if (value instanceof SubnetworkTuple) {
+            // as in CollectAccumulator: collect the fact matched by the 
subnetwork, not the tuple wrapping it
+            value = ( (SubnetworkTuple) value ).getFactHandle().getObject();
+        }
         ((Collection) context).add( value );
         return value;
     }
diff --git 
a/drools-test-coverage/test-compiler-integration/src/test/java/org/drools/compiler/integrationtests/AccumulateTest.java
 
b/drools-test-coverage/test-compiler-integration/src/test/java/org/drools/compiler/integrationtests/AccumulateTest.java
index 011cb1753e1..ec4c831fd98 100644
--- 
a/drools-test-coverage/test-compiler-integration/src/test/java/org/drools/compiler/integrationtests/AccumulateTest.java
+++ 
b/drools-test-coverage/test-compiler-integration/src/test/java/org/drools/compiler/integrationtests/AccumulateTest.java
@@ -36,9 +36,12 @@ import java.util.stream.Stream;
 
 import org.drools.compiler.integrationtests.incrementalcompilation.TestUtil;
 import org.drools.core.RuleSessionConfiguration;
+import org.drools.core.marshalling.ClassObjectMarshallingStrategyAcceptor;
+import org.drools.core.marshalling.SerializablePlaceholderResolverStrategy;
 import org.drools.commands.runtime.rule.InsertElementsCommand;
 import org.drools.kiesession.rulebase.InternalKnowledgeBase;
 import org.drools.mvel.compiler.Primitives;
+import org.drools.testcoverage.common.model.Address;
 import org.drools.testcoverage.common.model.Cheese;
 import org.drools.testcoverage.common.model.Cheesery;
 import org.drools.testcoverage.common.model.Order;
@@ -49,6 +52,7 @@ import org.drools.testcoverage.common.model.StockTick;
 import org.drools.testcoverage.common.util.KieBaseTestConfiguration;
 import org.drools.testcoverage.common.util.KieBaseUtil;
 import org.drools.testcoverage.common.util.KieUtil;
+import org.drools.testcoverage.common.util.SerializationHelper;
 import org.drools.testcoverage.common.util.TestParametersUtil2;
 import org.junit.jupiter.api.Timeout;
 import org.junit.jupiter.params.ParameterizedTest;
@@ -60,7 +64,9 @@ import org.kie.api.builder.Message;
 import org.kie.api.builder.ReleaseId;
 import org.kie.api.event.rule.AfterMatchFiredEvent;
 import org.kie.api.event.rule.AgendaEventListener;
+import org.kie.api.marshalling.ObjectMarshallingStrategy;
 import org.kie.api.runtime.KieContainer;
+import org.kie.api.runtime.EnvironmentName;
 import org.kie.api.runtime.KieSession;
 import org.kie.api.runtime.KieSessionConfiguration;
 import org.kie.api.runtime.rule.AccumulateFunction;
@@ -4289,4 +4295,43 @@ public class AccumulateTest {
             ksession.dispose();
         }
     }
+
+    @ParameterizedTest(name = "KieBase type={0}")
+    @MethodSource("parameters")
+    public void 
testMarshallCollectIntoArrayListFromSubnetwork(KieBaseTestConfiguration 
kieBaseTestConfiguration) throws Exception {
+        final String drl =
+                "import " + Person.class.getCanonicalName() + ";\n" +
+                "import " + Address.class.getCanonicalName() + ";\n" +
+                "import " + ArrayList.class.getCanonicalName() + ";\n" +
+                "rule R when\n" +
+                "  $p: Person()\n" +
+                "  $list: ArrayList() from collect (\n" +
+                "    Address() from $p.addresses\n" +
+                "  )\n" +
+                "then\n" +
+                "end";
+
+        final KieBase kbase = 
KieBaseUtil.getKieBaseFromKieModuleFromDrl("accumulate-test", 
kieBaseTestConfiguration, drl);
+        KieSession ksession = kbase.newKieSession();
+        
System.setProperty(org.drools.core.util.KeyStoreConstants.PROP_ALLOWED_DESER_CLASS_PATTERNS,
+                "org.drools.testcoverage.common.model.Person;" +
+                "org.drools.testcoverage.common.model.Address");
+        try {
+            
ksession.getEnvironment().set(EnvironmentName.OBJECT_MARSHALLING_STRATEGIES,
+                    new ObjectMarshallingStrategy[]{new 
SerializablePlaceholderResolverStrategy(ClassObjectMarshallingStrategyAcceptor.DEFAULT)});
+
+            final Person mario = new Person("Mario", 40);
+            mario.addAddress(new Address("Via Roma", 1, "Rome"));
+            mario.addAddress(new Address("Via Milano", 2, "Milan"));
+            ksession.insert(mario);
+            assertThat(ksession.fireAllRules()).isEqualTo(1);
+
+            ksession = 
SerializationHelper.getSerialisedStatefulKnowledgeSession(ksession, true);
+
+            assertThat(ksession.fireAllRules()).isEqualTo(0);
+        } finally {
+            
System.clearProperty(org.drools.core.util.KeyStoreConstants.PROP_ALLOWED_DESER_CLASS_PATTERNS);
+            ksession.dispose();
+        }
+    }
 }


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

Reply via email to