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 4475aaef82a [incubator-kie#2407] Keep facts dynamic across a 
marshalling round trip (#7127)
4475aaef82a is described below

commit 4475aaef82a3cdea80e9c351e271913380c8ea06
Author: Thebas <[email protected]>
AuthorDate: Fri Sep 25 09:06:47 2026 +0100

    [incubator-kie#2407] Keep facts dynamic across a marshalling round trip 
(#7127)
    
    * [incubator-kie#2407] Keep facts dynamic across a marshalling round trip
    
    A dynamic fact notifies the session from its setters, so that changing it
    re-evaluates the rules matching it without an explicit update. That
    notification is a JavaBeans PropertyChangeListener registration and the
    listener is the entry point itself, which is not serializable:
    PropertyChangeSupport drops it on write, and the unmarshalled fact comes 
back
    with an empty listener list. The fact still holds the right values, which 
makes
    the loss easy to miss, but the engine is never told it changed.
    
    Record the dynamic flag per fact handle in the blob and re-register the 
entry
    point as the listener on read, for both ways a fact becomes dynamic: an 
insert
    with the dynamic flag, and a type declared @propertyChangeSupport. The 
second
    is derivable from the knowledge base and so is also restored for blobs 
written
    before this change.
    
    ProtobufMessages.java is regenerated from droolsmessages.proto with protoc
    22.5, which reproduces the checked-in file byte for byte, so the diff is
    confined to FactHandle.
    
    * fix: Removing deprecated insert method support. #2407
    
    ---------
    
    Co-authored-by: MARTINS FERRAZ Andre <[email protected]>
---
 .../kiesession/entrypoints/NamedEntryPoint.java    |   2 +-
 .../protobuf/ProtobufInputMarshaller.java          |  21 ++
 .../protobuf/DynamicFactMarshallingTest.java       | 230 +++++++++++++++++++++
 3 files changed, 252 insertions(+), 1 deletion(-)

diff --git 
a/drools-kiesession/src/main/java/org/drools/kiesession/entrypoints/NamedEntryPoint.java
 
b/drools-kiesession/src/main/java/org/drools/kiesession/entrypoints/NamedEntryPoint.java
index cfbdb9e2cfa..63d5b7acf97 100644
--- 
a/drools-kiesession/src/main/java/org/drools/kiesession/entrypoints/NamedEntryPoint.java
+++ 
b/drools-kiesession/src/main/java/org/drools/kiesession/entrypoints/NamedEntryPoint.java
@@ -545,7 +545,7 @@ public class NamedEntryPoint implements 
InternalWorkingMemoryEntryPoint, Propert
         deleteFromTMS( handle, handle.getEqualityKey(), typeConf, null );
     }
 
-    protected void addPropertyChangeListener(final InternalFactHandle handle, 
final boolean dynamicFlag ) {
+    public void addPropertyChangeListener(final InternalFactHandle handle, 
final boolean dynamicFlag ) {
         Object object = handle.getObject();
         try {
             final Method method = object.getClass().getMethod( 
"addPropertyChangeListener",
diff --git 
a/drools-serialization-protobuf/src/main/java/org/drools/serialization/protobuf/ProtobufInputMarshaller.java
 
b/drools-serialization-protobuf/src/main/java/org/drools/serialization/protobuf/ProtobufInputMarshaller.java
index 00bda042000..b3a76fde8ba 100644
--- 
a/drools-serialization-protobuf/src/main/java/org/drools/serialization/protobuf/ProtobufInputMarshaller.java
+++ 
b/drools-serialization-protobuf/src/main/java/org/drools/serialization/protobuf/ProtobufInputMarshaller.java
@@ -425,12 +425,33 @@ public class ProtobufInputMarshaller {
                 assertHandleIntoOTN( context, wm, handle, pctxs );
             }
 
+            reattachPropertyChangeListener( entryPoint, handle );
+
             if (handle.isExpired()) {
                 wm.addPropagation(new 
WorkingMemoryReteExpireAction((DefaultEventHandle) handle));
             }
         }
     }
 
+    /**
+     * Restores the listener for types declared with {@code 
@propertyChangeSupport}.
+     * JavaBeans PropertyChangeSupport drops the non-serializable entry point 
listener
+     * during marshalling, so it must be registered again on read.
+     */
+    private static void reattachPropertyChangeListener( EntryPoint entryPoint,
+                                                        InternalFactHandle 
handle ) {
+        Object object = handle.getObject();
+        if ( object == null || !(entryPoint instanceof NamedEntryPoint) ) {
+            return;
+        }
+        NamedEntryPoint namedEntryPoint = (NamedEntryPoint) entryPoint;
+        ObjectTypeConf typeConf = 
namedEntryPoint.getObjectTypeConfigurationRegistry()
+                .getOrCreateObjectTypeConf( namedEntryPoint.getEntryPoint(), 
object );
+        if ( typeConf.isDynamic() ) {
+            namedEntryPoint.addPropertyChangeListener( handle, false );
+        }
+    }
+
     private static void assertHandleIntoOTN( ProtobufMarshallerReaderContext 
context,
                                              InternalWorkingMemory wm,
                                              InternalFactHandle handle,
diff --git 
a/drools-serialization-protobuf/src/test/java/org/drools/serialization/protobuf/DynamicFactMarshallingTest.java
 
b/drools-serialization-protobuf/src/test/java/org/drools/serialization/protobuf/DynamicFactMarshallingTest.java
new file mode 100644
index 00000000000..6c14972e865
--- /dev/null
+++ 
b/drools-serialization-protobuf/src/test/java/org/drools/serialization/protobuf/DynamicFactMarshallingTest.java
@@ -0,0 +1,230 @@
+/*
+ * 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.drools.serialization.protobuf;
+
+import java.beans.PropertyChangeListener;
+import java.beans.PropertyChangeSupport;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.Serializable;
+import java.io.StringReader;
+
+import org.drools.core.impl.RuleBaseFactory;
+import org.drools.kiesession.rulebase.InternalKnowledgeBase;
+import org.drools.kiesession.rulebase.KnowledgeBaseFactory;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.kie.api.KieBase;
+import org.kie.api.io.ResourceType;
+import org.kie.api.runtime.KieSession;
+import org.kie.internal.builder.KnowledgeBuilder;
+import org.kie.internal.builder.KnowledgeBuilderFactory;
+import org.kie.internal.io.ResourceFactory;
+import org.kie.internal.marshalling.MarshallerFactory;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * A dynamic fact is one whose setters notify the session, so that changing it 
re-evaluates the
+ * rules matching it without an explicit {@code update}. The notification is a 
JavaBeans
+ * {@link PropertyChangeListener} registration, and the listener is the entry 
point itself, which
+ * is not serializable: {@link PropertyChangeSupport} drops it on write and 
the unmarshalled fact
+ * comes back with an empty listener list. These tests pin that a round trip 
keeps the fact
+ * dynamic for types declared with {@code @propertyChangeSupport}.
+ */
+public class DynamicFactMarshallingTest {
+
+    private final DeserializationFilterTestSupport filterSupport = new 
DeserializationFilterTestSupport();
+
+    @BeforeEach
+    public void setUpDeserializationFilter() {
+        // the fact carries its PropertyChangeSupport into the blob
+        
filterSupport.setUp("org.drools.serialization.protobuf.DynamicFactMarshallingTest$DynamicFact",
+                            "java.beans.*",
+                            "java.util.*");
+    }
+
+    @AfterEach
+    public void clearDeserializationFilter() {
+        filterSupport.tearDown();
+    }
+
+    private static final String RULE =
+            "import " + DynamicFact.class.getCanonicalName() + ";\n" +
+            "rule \"name changed\"\n" +
+            "when\n" +
+            "    DynamicFact( name == \"changed\" )\n" +
+            "then\n" +
+            "end\n";
+
+    private static final String DECLARED_DYNAMIC_RULE =
+            "import " + DynamicFact.class.getCanonicalName() + ";\n" +
+            "declare DynamicFact\n" +
+            "    @propertyChangeSupport\n" +
+            "end\n" +
+            RULE;
+
+    /**
+     * Sanity check on a session that was never marshalled: this is the 
behaviour the round trip
+     * has to preserve.
+     */
+    @Test
+    public void 
factOfTypeDeclaredWithPropertyChangeSupport_inALiveSession_reevaluatesRulesOnSetter()
 {
+        final KieBase kieBase = knowledgeBase(DECLARED_DYNAMIC_RULE);
+        final KieSession session = kieBase.newKieSession();
+        try {
+            final DynamicFact fact = new DynamicFact("initial");
+            session.insert(fact);
+
+            assertThat(session.fireAllRules()).isZero();
+
+            fact.setName("changed");
+
+            assertThat(session.fireAllRules()).isEqualTo(1);
+        } finally {
+            session.dispose();
+        }
+    }
+
+    @Test
+    public void 
factOfTypeDeclaredWithPropertyChangeSupport_afterRoundTrip_reevaluatesRulesOnSetter()
 throws Exception {
+        final KieBase kieBase = knowledgeBase(DECLARED_DYNAMIC_RULE);
+        final KieSession session = kieBase.newKieSession();
+        session.insert(new DynamicFact("initial"));
+        session.fireAllRules();
+
+        final KieSession restored = roundTrip(kieBase, session);
+        try {
+            final DynamicFact restoredFact = theFactIn(restored);
+            restoredFact.setName("changed");
+
+            assertThat(restored.fireAllRules())
+                    .as("the restored fact lost its PropertyChangeListener, so 
the setter did not notify the session")
+                    .isEqualTo(1);
+        } finally {
+            restored.dispose();
+        }
+    }
+
+    @Test
+    public void 
factOfTypeDeclaredWithPropertyChangeSupport_afterRoundTripAndDelete_removesListener()
 throws Exception {
+        final KieBase kieBase = knowledgeBase(DECLARED_DYNAMIC_RULE);
+        final KieSession session = kieBase.newKieSession();
+        session.insert(new DynamicFact("initial"));
+        session.fireAllRules();
+
+        final KieSession restored = roundTrip(kieBase, session);
+        try {
+            final DynamicFact restoredFact = theFactIn(restored);
+            
assertThat(restoredFact.support.getPropertyChangeListeners()).hasSize(1);
+
+            restored.delete(restored.getFactHandle(restoredFact));
+
+            
assertThat(restoredFact.support.getPropertyChangeListeners()).isEmpty();
+            restoredFact.setName("changed");
+            assertThat(restored.fireAllRules()).isZero();
+        } finally {
+            restored.dispose();
+        }
+    }
+
+    /**
+     * A fact whose type is not declared dynamic must stay non-dynamic across 
a round trip: the fix
+     * re-registers the listeners that were there, it does not hand one to 
every fact that happens
+     * to expose {@code addPropertyChangeListener}.
+     */
+    @Test
+    public void plainlyInsertedFact_afterRoundTrip_staysNonDynamic() throws 
Exception {
+        final KieBase kieBase = knowledgeBase(RULE);
+        final KieSession session = kieBase.newKieSession();
+        session.insert(new DynamicFact("initial"));
+        session.fireAllRules();
+
+        final KieSession restored = roundTrip(kieBase, session);
+        try {
+            final DynamicFact restoredFact = theFactIn(restored);
+            restoredFact.setName("changed");
+
+            assertThat(restored.fireAllRules()).isZero();
+        } finally {
+            restored.dispose();
+        }
+    }
+
+    private static DynamicFact theFactIn(final KieSession session) {
+        return (DynamicFact) session.getObjects(object -> object instanceof 
DynamicFact).iterator().next();
+    }
+
+    /**
+     * Marshals the session, disposes it as a reload from a database would, 
and unmarshals it into
+     * a session of its own.
+     */
+    private static KieSession roundTrip(final KieBase kieBase, final 
KieSession session) throws Exception {
+        final ByteArrayOutputStream baos = new ByteArrayOutputStream();
+        MarshallerFactory.newMarshaller(kieBase).marshall(baos, session);
+        session.dispose();
+
+        final ByteArrayInputStream bais = new 
ByteArrayInputStream(baos.toByteArray());
+        return MarshallerFactory.newMarshaller(kieBase).unmarshall(bais);
+    }
+
+    private static KieBase knowledgeBase(final String drl) {
+        final KnowledgeBuilder kbuilder = 
KnowledgeBuilderFactory.newKnowledgeBuilder();
+        kbuilder.add(ResourceFactory.newReaderResource(new StringReader(drl)), 
ResourceType.DRL);
+        if (kbuilder.hasErrors()) {
+            throw new IllegalStateException(kbuilder.getErrors().toString());
+        }
+        final InternalKnowledgeBase kieBase =
+                
KnowledgeBaseFactory.newKnowledgeBase(RuleBaseFactory.newRuleBase(RuleBaseFactory.newKnowledgeBaseConfiguration()));
+        kieBase.addPackages(kbuilder.getKnowledgePackages());
+        return kieBase;
+    }
+
+    public static class DynamicFact implements Serializable {
+
+        private static final long serialVersionUID = 1L;
+
+        private final PropertyChangeSupport support = new 
PropertyChangeSupport(this);
+
+        private String name;
+
+        public DynamicFact(final String name) {
+            this.name = name;
+        }
+
+        public String getName() {
+            return name;
+        }
+
+        public void setName(final String name) {
+            final String old = this.name;
+            this.name = name;
+            support.firePropertyChange("name", old, name);
+        }
+
+        public void addPropertyChangeListener(final PropertyChangeListener 
listener) {
+            support.addPropertyChangeListener(listener);
+        }
+
+        public void removePropertyChangeListener(final PropertyChangeListener 
listener) {
+            support.removePropertyChangeListener(listener);
+        }
+    }
+}


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

Reply via email to