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 4c0deeab712 [incubator-kie-7079] RuleUnit DSL: Support STREAM event 
processing and CEP temporal operators (#7104)
4c0deeab712 is described below

commit 4c0deeab712226ac2f674dda8a7a831e977ed2fd
Author: Toshiya Kobayashi <[email protected]>
AuthorDate: Mon Sep 14 17:48:24 2026 +0900

    [incubator-kie-7079] RuleUnit DSL: Support STREAM event processing and CEP 
temporal operators (#7104)
    
    Read @EventProcessing annotation in the DSL provider and pass
    EventProcessingOption when creating the KieBase. Add bounded after()
    and before() temporal methods to Pattern2Def, rejecting unsupported
    calls on aggregate-result builders.
    
    Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]>
---
 .../ruleunits/dsl/RuleUnitProviderForDSL.java      |  12 +-
 .../dsl/accumulate/AccumulatePattern2.java         |   6 +
 .../ruleunits/dsl/accumulate/GroupByPattern1.java  |   6 +
 .../drools/ruleunits/dsl/patterns/Pattern2Def.java |  18 +
 .../ruleunits/dsl/patterns/Pattern2DefImpl.java    |  17 +
 .../java/org/drools/ruleunits/dsl/CepTest.java     | 545 +++++++++++++++++++++
 .../drools/ruleunits/dsl/domain/StockTick.java}    |  42 +-
 7 files changed, 631 insertions(+), 15 deletions(-)

diff --git 
a/drools-ruleunits/drools-ruleunits-dsl/src/main/java/org/drools/ruleunits/dsl/RuleUnitProviderForDSL.java
 
b/drools-ruleunits/drools-ruleunits-dsl/src/main/java/org/drools/ruleunits/dsl/RuleUnitProviderForDSL.java
index 6b8896244c8..964696c8de0 100644
--- 
a/drools-ruleunits/drools-ruleunits-dsl/src/main/java/org/drools/ruleunits/dsl/RuleUnitProviderForDSL.java
+++ 
b/drools-ruleunits/drools-ruleunits-dsl/src/main/java/org/drools/ruleunits/dsl/RuleUnitProviderForDSL.java
@@ -30,12 +30,15 @@ import org.drools.ruleunits.api.DataSource;
 import org.drools.ruleunits.api.RuleUnit;
 import org.drools.ruleunits.api.RuleUnitData;
 import org.drools.ruleunits.api.RuleUnitInstance;
+import org.drools.ruleunits.api.conf.EventProcessing;
+import org.drools.ruleunits.api.conf.EventProcessingType;
 import org.drools.ruleunits.api.conf.RuleConfig;
 import org.drools.ruleunits.impl.EntryPointDataProcessor;
 import org.drools.ruleunits.impl.ReteEvaluatorBasedRuleUnitInstance;
 import org.drools.ruleunits.impl.RuleUnitProviderImpl;
 import org.drools.ruleunits.impl.factory.AbstractRuleUnit;
 import org.drools.ruleunits.impl.sessions.RuleUnitExecutorImpl;
+import org.kie.api.conf.EventProcessingOption;
 import org.kie.api.runtime.rule.EntryPoint;
 
 import java.util.Map;
@@ -69,7 +72,14 @@ public class RuleUnitProviderForDSL extends 
RuleUnitProviderImpl {
         public ModelRuleUnit(Class<T> type, Model model, UnitGlobalsResolver 
unitGlobalsResolver) {
             super(type);
             this.unitGlobalsResolver = unitGlobalsResolver;
-            this.ruleBase = KieBaseBuilder.createKieBaseFromModel( model );
+            EventProcessing annotation = 
type.getAnnotation(EventProcessing.class);
+            if (annotation == null) {
+                this.ruleBase = KieBaseBuilder.createKieBaseFromModel(model);
+            } else {
+                EventProcessingOption option = annotation.value() == 
EventProcessingType.STREAM
+                        ? EventProcessingOption.STREAM : 
EventProcessingOption.CLOUD;
+                this.ruleBase = KieBaseBuilder.createKieBaseFromModel(model, 
option);
+            }
             if (DUMP_GENERATED_RETE) {
                 ReteDumper.dumpRete(this.ruleBase);
             }
diff --git 
a/drools-ruleunits/drools-ruleunits-dsl/src/main/java/org/drools/ruleunits/dsl/accumulate/AccumulatePattern2.java
 
b/drools-ruleunits/drools-ruleunits-dsl/src/main/java/org/drools/ruleunits/dsl/accumulate/AccumulatePattern2.java
index d9e2cb0e296..05605972fff 100644
--- 
a/drools-ruleunits/drools-ruleunits-dsl/src/main/java/org/drools/ruleunits/dsl/accumulate/AccumulatePattern2.java
+++ 
b/drools-ruleunits/drools-ruleunits-dsl/src/main/java/org/drools/ruleunits/dsl/accumulate/AccumulatePattern2.java
@@ -18,6 +18,7 @@
  */
 package org.drools.ruleunits.dsl.accumulate;
 
+import org.drools.model.functions.temporal.TemporalPredicate;
 import org.drools.model.view.ViewItem;
 import org.drools.ruleunits.dsl.patterns.Pattern1DefImpl;
 import org.drools.ruleunits.dsl.patterns.Pattern2DefImpl;
@@ -34,6 +35,11 @@ public class AccumulatePattern2<A, B, C> extends 
Pattern2DefImpl<A, C> {
         this.acc = acc;
     }
 
+    @Override
+    protected Pattern2DefImpl<A, C> addTemporalConstraint(TemporalPredicate 
temporalPredicate) {
+        throw new UnsupportedOperationException("Temporal constraints are not 
supported on accumulate results");
+    }
+
     @Override
     public ViewItem toExecModelItem() {
         return createAccumulate1Item(patternB, getVariable(), acc);
diff --git 
a/drools-ruleunits/drools-ruleunits-dsl/src/main/java/org/drools/ruleunits/dsl/accumulate/GroupByPattern1.java
 
b/drools-ruleunits/drools-ruleunits-dsl/src/main/java/org/drools/ruleunits/dsl/accumulate/GroupByPattern1.java
index fa49aed51bb..4fc82d8a7e2 100644
--- 
a/drools-ruleunits/drools-ruleunits-dsl/src/main/java/org/drools/ruleunits/dsl/accumulate/GroupByPattern1.java
+++ 
b/drools-ruleunits/drools-ruleunits-dsl/src/main/java/org/drools/ruleunits/dsl/accumulate/GroupByPattern1.java
@@ -21,6 +21,7 @@ package org.drools.ruleunits.dsl.accumulate;
 import org.drools.model.DSL;
 import org.drools.model.Variable;
 import org.drools.model.functions.Function1;
+import org.drools.model.functions.temporal.TemporalPredicate;
 import org.drools.model.view.ExprViewItem;
 import org.drools.model.view.ViewItem;
 import org.drools.ruleunits.dsl.patterns.InternalPatternDef;
@@ -45,6 +46,11 @@ public class GroupByPattern1<A, K, V> extends 
Pattern2DefImpl<K, V> {
         this.acc = acc;
     }
 
+    @Override
+    protected Pattern2DefImpl<K, V> addTemporalConstraint(TemporalPredicate 
temporalPredicate) {
+        throw new UnsupportedOperationException("Temporal constraints are not 
supported on groupBy results");
+    }
+
     @Override
     public ViewItem toExecModelItem() {
         return createGroupByItem(pattern, groupingFunction, acc, 
patternA.getVariable(), patternB.getVariable());
diff --git 
a/drools-ruleunits/drools-ruleunits-dsl/src/main/java/org/drools/ruleunits/dsl/patterns/Pattern2Def.java
 
b/drools-ruleunits/drools-ruleunits-dsl/src/main/java/org/drools/ruleunits/dsl/patterns/Pattern2Def.java
index 38cbd5276b3..c59b14ce290 100644
--- 
a/drools-ruleunits/drools-ruleunits-dsl/src/main/java/org/drools/ruleunits/dsl/patterns/Pattern2Def.java
+++ 
b/drools-ruleunits/drools-ruleunits-dsl/src/main/java/org/drools/ruleunits/dsl/patterns/Pattern2Def.java
@@ -18,6 +18,8 @@
  */
 package org.drools.ruleunits.dsl.patterns;
 
+import java.util.concurrent.TimeUnit;
+
 import org.drools.model.Index;
 import org.drools.model.functions.Block1;
 import org.drools.model.functions.Block2;
@@ -39,6 +41,22 @@ public interface Pattern2Def<A, B> extends PatternDef {
 
     <V> Pattern2Def<A, B> filter(String fieldName, Function1<B, V> 
leftExtractor, Index.ConstraintType constraintType, Function1<A, V> 
rightExtractor);
 
+    /**
+     * Constrains pattern B to occur after pattern A within inclusive bounds 
{@code [min, max]}
+     * converted to the given time unit. The gap is measured from {@code 
end(A)} to {@code start(B)}.
+     * Both operands must be events annotated with {@code 
@Role(Role.Type.EVENT)}.
+     * Supported time units: MILLISECONDS, SECONDS, MINUTES, HOURS, DAYS.
+     */
+    Pattern2Def<A, B> after(long min, long max, TimeUnit unit);
+
+    /**
+     * Constrains pattern B to occur before pattern A within inclusive bounds 
{@code [min, max]}
+     * converted to the given time unit. The gap is measured from {@code 
end(B)} to {@code start(A)}.
+     * Both operands must be events annotated with {@code 
@Role(Role.Type.EVENT)}.
+     * Supported time units: MILLISECONDS, SECONDS, MINUTES, HOURS, DAYS.
+     */
+    Pattern2Def<A, B> before(long min, long max, TimeUnit unit);
+
     <C> Pattern3Def<A, B, C> on(DataSource<C> dataSource);
 
     <C> Pattern3Def<A, B, C> join(Function1<RuleFactory, Pattern1Def<C>> 
patternBuilder);
diff --git 
a/drools-ruleunits/drools-ruleunits-dsl/src/main/java/org/drools/ruleunits/dsl/patterns/Pattern2DefImpl.java
 
b/drools-ruleunits/drools-ruleunits-dsl/src/main/java/org/drools/ruleunits/dsl/patterns/Pattern2DefImpl.java
index 9c102d8a708..da7108f82ed 100644
--- 
a/drools-ruleunits/drools-ruleunits-dsl/src/main/java/org/drools/ruleunits/dsl/patterns/Pattern2DefImpl.java
+++ 
b/drools-ruleunits/drools-ruleunits-dsl/src/main/java/org/drools/ruleunits/dsl/patterns/Pattern2DefImpl.java
@@ -19,10 +19,12 @@
 package org.drools.ruleunits.dsl.patterns;
 
 import java.util.UUID;
+import java.util.concurrent.TimeUnit;
 
 import org.drools.model.Condition;
 import org.drools.model.DSL;
 import org.drools.model.Index;
+import org.drools.model.functions.temporal.TemporalPredicate;
 import org.drools.model.functions.Block1;
 import org.drools.model.functions.Block2;
 import org.drools.model.functions.Block3;
@@ -72,6 +74,21 @@ public class Pattern2DefImpl<A, B> extends 
SinglePatternDef<B> implements Patter
         return this;
     }
 
+    @Override
+    public Pattern2DefImpl<A, B> after(long min, long max, TimeUnit unit) {
+        return addTemporalConstraint(DSL.after(min, unit, max, unit));
+    }
+
+    @Override
+    public Pattern2DefImpl<A, B> before(long min, long max, TimeUnit unit) {
+        return addTemporalConstraint(DSL.before(min, unit, max, unit));
+    }
+
+    protected Pattern2DefImpl<A, B> addTemporalConstraint(TemporalPredicate 
temporalPredicate) {
+        patternB.constraints.add(patternDef -> 
patternDef.expr(UUID.randomUUID().toString(), patternA.variable, 
temporalPredicate));
+        return this;
+    }
+
     @Override
     public <C> Pattern3Def<A, B, C> on(DataSource<C> dataSource) {
         return join(rule.on(dataSource));
diff --git 
a/drools-ruleunits/drools-ruleunits-dsl/src/test/java/org/drools/ruleunits/dsl/CepTest.java
 
b/drools-ruleunits/drools-ruleunits-dsl/src/test/java/org/drools/ruleunits/dsl/CepTest.java
new file mode 100644
index 00000000000..10b9253ea79
--- /dev/null
+++ 
b/drools-ruleunits/drools-ruleunits-dsl/src/test/java/org/drools/ruleunits/dsl/CepTest.java
@@ -0,0 +1,545 @@
+/*
+ * 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.ruleunits.dsl;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+import org.drools.core.WorkingMemoryEntryPoint;
+import org.drools.core.common.InternalFactHandle;
+import org.drools.core.common.ReteEvaluator;
+import org.drools.model.Index;
+import org.drools.ruleunits.api.DataSource;
+import org.drools.ruleunits.api.DataStream;
+import org.drools.ruleunits.api.RuleUnitInstance;
+import org.drools.ruleunits.api.RuleUnitProvider;
+import org.drools.ruleunits.api.conf.ClockType;
+import org.drools.ruleunits.api.conf.EventProcessing;
+import org.drools.ruleunits.api.conf.EventProcessingType;
+import org.drools.ruleunits.api.conf.RuleConfig;
+import org.drools.ruleunits.dsl.domain.StockTick;
+import org.drools.ruleunits.impl.AbstractRuleUnitInstance;
+import org.junit.jupiter.api.Test;
+import org.kie.api.conf.EventProcessingOption;
+import org.kie.api.runtime.rule.EntryPoint;
+import org.kie.api.runtime.rule.FactHandle;
+import org.kie.api.time.SessionPseudoClock;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.drools.model.Index.ConstraintType.EQUAL;
+import static org.drools.ruleunits.dsl.Accumulators.sum;
+
+public class CepTest {
+
+    // --- Configuration tests ---
+
+    @Test
+    public void streamAnnotationProducesStreamKieBase() {
+        StreamAfterUnit unit = new StreamAfterUnit();
+        RuleConfig config = RuleUnitProvider.get().newRuleConfig();
+        config.setClockType(ClockType.PSEUDO);
+        try (RuleUnitInstance<StreamAfterUnit> instance = 
RuleUnitProvider.get().createRuleUnitInstance(unit, config)) {
+            ReteEvaluator evaluator = getEvaluator(instance);
+            
assertThat(evaluator.getKnowledgeBase().getRuleBaseConfiguration().getEventProcessingMode())
+                    .isEqualTo(EventProcessingOption.STREAM);
+        }
+    }
+
+    @Test
+    public void explicitCloudAnnotationProducesCloudKieBase() {
+        ExplicitCloudUnit unit = new ExplicitCloudUnit();
+        try (RuleUnitInstance<ExplicitCloudUnit> instance = 
RuleUnitProvider.get().createRuleUnitInstance(unit)) {
+            ReteEvaluator evaluator = getEvaluator(instance);
+            
assertThat(evaluator.getKnowledgeBase().getRuleBaseConfiguration().getEventProcessingMode())
+                    .isEqualTo(EventProcessingOption.CLOUD);
+        }
+    }
+
+    @Test
+    public void unannotatedUnitDefaultsToCloud() {
+        UnannotatedUnit unit = new UnannotatedUnit();
+        try (RuleUnitInstance<UnannotatedUnit> instance = 
RuleUnitProvider.get().createRuleUnitInstance(unit)) {
+            ReteEvaluator evaluator = getEvaluator(instance);
+            
assertThat(evaluator.getKnowledgeBase().getRuleBaseConfiguration().getEventProcessingMode())
+                    .isEqualTo(EventProcessingOption.CLOUD);
+        }
+    }
+
+    // --- Temporal constraint: after ---
+
+    @Test
+    public void afterMatchesWithinBounds() {
+        StreamAfterUnit unit = new StreamAfterUnit();
+        RuleConfig config = RuleUnitProvider.get().newRuleConfig();
+        config.setClockType(ClockType.PSEUDO);
+        try (RuleUnitInstance<StreamAfterUnit> instance = 
RuleUnitProvider.get().createRuleUnitInstance(unit, config)) {
+            SessionPseudoClock clock = instance.getClock();
+            unit.getStockTicks().append(new StockTick("DROO"));
+            clock.advanceTime(6, TimeUnit.SECONDS);
+            unit.getStockTicks().append(new StockTick("ACME"));
+            instance.fire();
+
+            assertThat(unit.getResults()).hasSize(1);
+            
assertThat(unit.getResults().get(0).getCompany()).isEqualTo("ACME");
+        }
+    }
+
+    @Test
+    public void afterDoesNotMatchBelowLowerBound() {
+        StreamAfterUnit unit = new StreamAfterUnit();
+        RuleConfig config = RuleUnitProvider.get().newRuleConfig();
+        config.setClockType(ClockType.PSEUDO);
+        try (RuleUnitInstance<StreamAfterUnit> instance = 
RuleUnitProvider.get().createRuleUnitInstance(unit, config)) {
+            SessionPseudoClock clock = instance.getClock();
+            unit.getStockTicks().append(new StockTick("DROO"));
+            clock.advanceTime(4999, TimeUnit.MILLISECONDS);
+            unit.getStockTicks().append(new StockTick("ACME"));
+            instance.fire();
+
+            assertThat(unit.getResults()).isEmpty();
+        }
+    }
+
+    @Test
+    public void afterDoesNotMatchAboveUpperBound() {
+        StreamAfterUnit unit = new StreamAfterUnit();
+        RuleConfig config = RuleUnitProvider.get().newRuleConfig();
+        config.setClockType(ClockType.PSEUDO);
+        try (RuleUnitInstance<StreamAfterUnit> instance = 
RuleUnitProvider.get().createRuleUnitInstance(unit, config)) {
+            SessionPseudoClock clock = instance.getClock();
+            unit.getStockTicks().append(new StockTick("DROO"));
+            clock.advanceTime(8001, TimeUnit.MILLISECONDS);
+            unit.getStockTicks().append(new StockTick("ACME"));
+            instance.fire();
+
+            assertThat(unit.getResults()).isEmpty();
+        }
+    }
+
+    @Test
+    public void afterMatchesAtExactLowerBound() {
+        StreamAfterUnit unit = new StreamAfterUnit();
+        RuleConfig config = RuleUnitProvider.get().newRuleConfig();
+        config.setClockType(ClockType.PSEUDO);
+        try (RuleUnitInstance<StreamAfterUnit> instance = 
RuleUnitProvider.get().createRuleUnitInstance(unit, config)) {
+            SessionPseudoClock clock = instance.getClock();
+            unit.getStockTicks().append(new StockTick("DROO"));
+            clock.advanceTime(5000, TimeUnit.MILLISECONDS);
+            unit.getStockTicks().append(new StockTick("ACME"));
+            instance.fire();
+
+            assertThat(unit.getResults()).hasSize(1);
+            
assertThat(unit.getResults().get(0).getCompany()).isEqualTo("ACME");
+        }
+    }
+
+    @Test
+    public void afterMatchesAtExactUpperBound() {
+        StreamAfterUnit unit = new StreamAfterUnit();
+        RuleConfig config = RuleUnitProvider.get().newRuleConfig();
+        config.setClockType(ClockType.PSEUDO);
+        try (RuleUnitInstance<StreamAfterUnit> instance = 
RuleUnitProvider.get().createRuleUnitInstance(unit, config)) {
+            SessionPseudoClock clock = instance.getClock();
+            unit.getStockTicks().append(new StockTick("DROO"));
+            clock.advanceTime(8000, TimeUnit.MILLISECONDS);
+            unit.getStockTicks().append(new StockTick("ACME"));
+            instance.fire();
+
+            assertThat(unit.getResults()).hasSize(1);
+            
assertThat(unit.getResults().get(0).getCompany()).isEqualTo("ACME");
+        }
+    }
+
+    @Test
+    public void afterWithNonzeroDurationUsesEndToStart() {
+        StreamAfterUnit unit = new StreamAfterUnit();
+        RuleConfig config = RuleUnitProvider.get().newRuleConfig();
+        config.setClockType(ClockType.PSEUDO);
+        try (RuleUnitInstance<StreamAfterUnit> instance = 
RuleUnitProvider.get().createRuleUnitInstance(unit, config)) {
+            SessionPseudoClock clock = instance.getClock();
+            // DROO starts at 0 with 3s duration, so end(DROO) = 3s
+            unit.getStockTicks().append(new StockTick("DROO", 3000));
+            // ACME starts at 9s, gap = start(ACME) - end(DROO) = 9s - 3s = 
6s, within [5s, 8s]
+            clock.advanceTime(9, TimeUnit.SECONDS);
+            unit.getStockTicks().append(new StockTick("ACME"));
+            instance.fire();
+
+            assertThat(unit.getResults()).hasSize(1);
+            
assertThat(unit.getResults().get(0).getCompany()).isEqualTo("ACME");
+        }
+    }
+
+    @Test
+    public void afterWithMillisecondUnit() {
+        StreamAfterMillisUnit unit = new StreamAfterMillisUnit();
+        RuleConfig config = RuleUnitProvider.get().newRuleConfig();
+        config.setClockType(ClockType.PSEUDO);
+        try (RuleUnitInstance<StreamAfterMillisUnit> instance = 
RuleUnitProvider.get().createRuleUnitInstance(unit, config)) {
+            SessionPseudoClock clock = instance.getClock();
+            unit.getStockTicks().append(new StockTick("DROO"));
+            clock.advanceTime(600, TimeUnit.MILLISECONDS);
+            unit.getStockTicks().append(new StockTick("ACME"));
+            instance.fire();
+
+            assertThat(unit.getResults()).hasSize(1);
+            
assertThat(unit.getResults().get(0).getCompany()).isEqualTo("ACME");
+        }
+    }
+
+    // --- Temporal constraint: before ---
+
+    @Test
+    public void beforeMatchesWithinBounds() {
+        StreamBeforeUnit unit = new StreamBeforeUnit();
+        RuleConfig config = RuleUnitProvider.get().newRuleConfig();
+        config.setClockType(ClockType.PSEUDO);
+        try (RuleUnitInstance<StreamBeforeUnit> instance = 
RuleUnitProvider.get().createRuleUnitInstance(unit, config)) {
+            SessionPseudoClock clock = instance.getClock();
+            // ACME occurs first (the "before" event)
+            unit.getStockTicks().append(new StockTick("ACME"));
+            clock.advanceTime(6, TimeUnit.SECONDS);
+            // DROO occurs second
+            unit.getStockTicks().append(new StockTick("DROO"));
+            instance.fire();
+
+            assertThat(unit.getResults()).hasSize(1);
+            
assertThat(unit.getResults().get(0).getCompany()).isEqualTo("ACME");
+        }
+    }
+
+    @Test
+    public void beforeDoesNotMatchOutsideBounds() {
+        StreamBeforeUnit unit = new StreamBeforeUnit();
+        RuleConfig config = RuleUnitProvider.get().newRuleConfig();
+        config.setClockType(ClockType.PSEUDO);
+        try (RuleUnitInstance<StreamBeforeUnit> instance = 
RuleUnitProvider.get().createRuleUnitInstance(unit, config)) {
+            SessionPseudoClock clock = instance.getClock();
+            unit.getStockTicks().append(new StockTick("ACME"));
+            clock.advanceTime(4999, TimeUnit.MILLISECONDS);
+            unit.getStockTicks().append(new StockTick("DROO"));
+            instance.fire();
+
+            assertThat(unit.getResults()).isEmpty();
+        }
+    }
+
+    @Test
+    public void beforeWithNonzeroDurationUsesEndToStart() {
+        StreamBeforeUnit unit = new StreamBeforeUnit();
+        RuleConfig config = RuleUnitProvider.get().newRuleConfig();
+        config.setClockType(ClockType.PSEUDO);
+        try (RuleUnitInstance<StreamBeforeUnit> instance = 
RuleUnitProvider.get().createRuleUnitInstance(unit, config)) {
+            SessionPseudoClock clock = instance.getClock();
+            // ACME starts at 0 with 3s duration, so end(ACME) = 3s
+            unit.getStockTicks().append(new StockTick("ACME", 3000));
+            // DROO starts at 9s, gap = start(DROO) - end(ACME) = 9s - 3s = 
6s, within [5s, 8s]
+            clock.advanceTime(9, TimeUnit.SECONDS);
+            unit.getStockTicks().append(new StockTick("DROO"));
+            instance.fire();
+
+            assertThat(unit.getResults()).hasSize(1);
+            
assertThat(unit.getResults().get(0).getCompany()).isEqualTo("ACME");
+        }
+    }
+
+    // --- Explicit expiration ---
+
+    @Test
+    public void expiresRemovesEventAfterDuration() {
+        StreamExpirationUnit unit = new StreamExpirationUnit();
+        RuleConfig config = RuleUnitProvider.get().newRuleConfig();
+        config.setClockType(ClockType.PSEUDO);
+        try (RuleUnitInstance<StreamExpirationUnit> instance = 
RuleUnitProvider.get().createRuleUnitInstance(unit, config)) {
+            SessionPseudoClock clock = instance.getClock();
+            unit.getStockTicks().append(new StockTick("DROO"));
+            instance.fire();
+
+            EntryPoint entryPoint = findNonDefaultEntryPoint(instance);
+            assertThat(entryPoint.getFactHandles()).hasSize(1);
+
+            clock.advanceTime(11, TimeUnit.SECONDS);
+            instance.fire();
+
+            assertThat(entryPoint.getFactHandles()).isEmpty();
+        }
+    }
+
+    // --- Combined CEP scenario ---
+
+    @Test
+    public void combinedCepScenario() {
+        StreamAfterUnit unit = new StreamAfterUnit();
+        RuleConfig config = RuleUnitProvider.get().newRuleConfig();
+        config.setClockType(ClockType.PSEUDO);
+        try (RuleUnitInstance<StreamAfterUnit> instance = 
RuleUnitProvider.get().createRuleUnitInstance(unit, config)) {
+            SessionPseudoClock clock = instance.getClock();
+
+            // 1. Append a zero-duration DROO event at time zero
+            unit.getStockTicks().append(new StockTick("DROO"));
+
+            // 2. Advance 6 seconds and append ACME
+            clock.advanceTime(6, TimeUnit.SECONDS);
+            unit.getStockTicks().append(new StockTick("ACME"));
+
+            // 3. Fire and assert the after(5, 8, SECONDS) rule records ACME
+            instance.fire();
+            assertThat(unit.getResults()).hasSize(1);
+            
assertThat(unit.getResults().get(0).getCompany()).isEqualTo("ACME");
+
+            // 4. Both events should still be in the entry point
+            EntryPoint entryPoint = findNonDefaultEntryPoint(instance);
+            assertThat(entryPoint.getFactHandles()).hasSize(2);
+
+            // 5. Advance another 5 seconds (total = 11s) and fire
+            clock.advanceTime(5, TimeUnit.SECONDS);
+            instance.fire();
+
+            // 6. DROO was inserted at t=0 with @Expires("10s"), so it should 
be expired
+            //    ACME was inserted at t=6 with @Expires("10s"), so it should 
still be present
+            Collection<FactHandle> remaining = entryPoint.getFactHandles();
+            assertThat(remaining).hasSize(1);
+            assertThat(((StockTick) ((InternalFactHandle) 
remaining.iterator().next()).getObject()).getCompany())
+                    .isEqualTo("ACME");
+        }
+    }
+
+    // --- Unsupported aggregate calls ---
+
+    @Test
+    public void afterOnGroupByThrowsUnsupported() {
+        assertThatThrownBy(() -> {
+            new GroupByTemporalUnit();
+        }).isInstanceOf(UnsupportedOperationException.class)
+                .hasMessageContaining("groupBy");
+    }
+
+    @Test
+    public void afterOnAccumulateThrowsUnsupported() {
+        assertThatThrownBy(() -> {
+            new AccumulateTemporalUnit();
+        }).isInstanceOf(UnsupportedOperationException.class)
+                .hasMessageContaining("accumulate");
+    }
+
+    // --- Helpers ---
+
+    @SuppressWarnings("rawtypes")
+    private ReteEvaluator getEvaluator(RuleUnitInstance<?> instance) {
+        return (ReteEvaluator) ((AbstractRuleUnitInstance) 
instance).getEvaluator();
+    }
+
+    private EntryPoint findNonDefaultEntryPoint(RuleUnitInstance<?> instance) {
+        ReteEvaluator evaluator = getEvaluator(instance);
+        for (EntryPoint ep : evaluator.getEntryPoints()) {
+            if (!"DEFAULT".equals(ep.getEntryPointId())) {
+                return ep;
+            }
+        }
+        throw new IllegalStateException("No non-DEFAULT entry point found");
+    }
+
+    // --- Unit definitions ---
+
+    @EventProcessing(EventProcessingType.STREAM)
+    public static class StreamAfterUnit implements RuleUnitDefinition {
+        private final DataStream<StockTick> stockTicks;
+        private final List<StockTick> results = new ArrayList<>();
+
+        public StreamAfterUnit() {
+            this.stockTicks = DataSource.createStream();
+        }
+
+        public DataStream<StockTick> getStockTicks() { return stockTicks; }
+        public List<StockTick> getResults() { return results; }
+
+        @Override
+        public void defineRules(RulesFactory rulesFactory) {
+            // A=$a : /stockTicks [ company == "DROO" ]
+            // B=$b : /stockTicks [ company == "ACME", this after[5s,8s] $a ]
+            // → B after A: gap = start(B) - end(A) must be in [5s, 8s]
+            rulesFactory.rule("ACME after DROO")
+                    .on(stockTicks)                                      // 
pattern A
+                    .filter(StockTick::getCompany, EQUAL, "DROO")
+                    .join(rule -> rule.on(stockTicks)                    // 
pattern B
+                            .filter(StockTick::getCompany, EQUAL, "ACME"))
+                    .after(5, 8, TimeUnit.SECONDS)                      // B 
after A
+                    .execute(results, (r, droo, acme) -> r.add(acme));
+        }
+    }
+
+    @EventProcessing(EventProcessingType.STREAM)
+    public static class StreamAfterMillisUnit implements RuleUnitDefinition {
+        private final DataStream<StockTick> stockTicks;
+        private final List<StockTick> results = new ArrayList<>();
+
+        public StreamAfterMillisUnit() {
+            this.stockTicks = DataSource.createStream();
+        }
+
+        public DataStream<StockTick> getStockTicks() { return stockTicks; }
+        public List<StockTick> getResults() { return results; }
+
+        @Override
+        public void defineRules(RulesFactory rulesFactory) {
+            // Same as StreamAfterUnit but with millisecond bounds [500ms, 
1000ms]
+            rulesFactory.rule("ACME after DROO millis")
+                    .on(stockTicks)                                      // 
pattern A
+                    .filter(StockTick::getCompany, EQUAL, "DROO")
+                    .join(rule -> rule.on(stockTicks)                    // 
pattern B
+                            .filter(StockTick::getCompany, EQUAL, "ACME"))
+                    .after(500, 1000, TimeUnit.MILLISECONDS)             // B 
after A
+                    .execute(results, (r, droo, acme) -> r.add(acme));
+        }
+    }
+
+    @EventProcessing(EventProcessingType.STREAM)
+    public static class StreamBeforeUnit implements RuleUnitDefinition {
+        private final DataStream<StockTick> stockTicks;
+        private final List<StockTick> results = new ArrayList<>();
+
+        public StreamBeforeUnit() {
+            this.stockTicks = DataSource.createStream();
+        }
+
+        public DataStream<StockTick> getStockTicks() { return stockTicks; }
+        public List<StockTick> getResults() { return results; }
+
+        @Override
+        public void defineRules(RulesFactory rulesFactory) {
+            // A=$a : /stockTicks [ company == "DROO" ]
+            // B=$b : /stockTicks [ company == "ACME", this before[5s,8s] $a ]
+            // → B before A: gap = start(A) - end(B) must be in [5s, 8s]
+            rulesFactory.rule("ACME before DROO")
+                    .on(stockTicks)                                      // 
pattern A
+                    .filter(StockTick::getCompany, EQUAL, "DROO")
+                    .join(rule -> rule.on(stockTicks)                    // 
pattern B
+                            .filter(StockTick::getCompany, EQUAL, "ACME"))
+                    .before(5, 8, TimeUnit.SECONDS)                      // B 
before A
+                    .execute(results, (r, droo, acme) -> r.add(acme));
+        }
+    }
+
+    @EventProcessing(EventProcessingType.STREAM)
+    public static class StreamExpirationUnit implements RuleUnitDefinition {
+        private final DataStream<StockTick> stockTicks;
+        private final List<StockTick> results = new ArrayList<>();
+
+        public StreamExpirationUnit() {
+            this.stockTicks = DataSource.createStream();
+        }
+
+        public DataStream<StockTick> getStockTicks() { return stockTicks; }
+        public List<StockTick> getResults() { return results; }
+
+        @Override
+        public void defineRules(RulesFactory rulesFactory) {
+            rulesFactory.rule("collect all")
+                    .on(stockTicks)
+                    .execute(results, (r, tick) -> r.add(tick));
+        }
+    }
+
+    @EventProcessing(EventProcessingType.CLOUD)
+    public static class ExplicitCloudUnit implements RuleUnitDefinition {
+        private final DataStream<StockTick> stockTicks;
+        private final List<StockTick> results = new ArrayList<>();
+
+        public ExplicitCloudUnit() {
+            this.stockTicks = DataSource.createStream();
+        }
+
+        public DataStream<StockTick> getStockTicks() { return stockTicks; }
+        public List<StockTick> getResults() { return results; }
+
+        @Override
+        public void defineRules(RulesFactory rulesFactory) {
+            rulesFactory.rule("collect all")
+                    .on(stockTicks)
+                    .execute(results, (r, tick) -> r.add(tick));
+        }
+    }
+
+    public static class UnannotatedUnit implements RuleUnitDefinition {
+        private final DataStream<StockTick> stockTicks;
+        private final List<StockTick> results = new ArrayList<>();
+
+        public UnannotatedUnit() {
+            this.stockTicks = DataSource.createStream();
+        }
+
+        public DataStream<StockTick> getStockTicks() { return stockTicks; }
+        public List<StockTick> getResults() { return results; }
+
+        @Override
+        public void defineRules(RulesFactory rulesFactory) {
+            rulesFactory.rule("collect all")
+                    .on(stockTicks)
+                    .execute(results, (r, tick) -> r.add(tick));
+        }
+    }
+
+    @EventProcessing(EventProcessingType.STREAM)
+    public static class GroupByTemporalUnit implements RuleUnitDefinition {
+        private final DataStream<StockTick> stockTicks;
+        private final List<Integer> results = new ArrayList<>();
+
+        public GroupByTemporalUnit() {
+            this.stockTicks = DataSource.createStream();
+            defineRules(new RulesFactory(this));
+        }
+
+        public DataStream<StockTick> getStockTicks() { return stockTicks; }
+        public List<Integer> getResults() { return results; }
+
+        @Override
+        public void defineRules(RulesFactory rulesFactory) {
+            rulesFactory.rule("group temporal")
+                    .groupBy(rule -> rule.on(stockTicks),
+                            (StockTick t) -> t.getCompany(),
+                            sum(StockTick::getDuration))
+                    .after(5, 8, TimeUnit.SECONDS);
+        }
+    }
+
+    @EventProcessing(EventProcessingType.STREAM)
+    public static class AccumulateTemporalUnit implements RuleUnitDefinition {
+        private final DataStream<StockTick> stockTicks;
+        private final List<Integer> results = new ArrayList<>();
+
+        public AccumulateTemporalUnit() {
+            this.stockTicks = DataSource.createStream();
+            defineRules(new RulesFactory(this));
+        }
+
+        public DataStream<StockTick> getStockTicks() { return stockTicks; }
+        public List<Integer> getResults() { return results; }
+
+        @Override
+        public void defineRules(RulesFactory rulesFactory) {
+            rulesFactory.rule("accumulate temporal")
+                    .on(stockTicks)
+                    .accumulate(rule -> rule.on(stockTicks),
+                            sum(StockTick::getDuration))
+                    .after(5, 8, TimeUnit.SECONDS);
+        }
+    }
+}
diff --git 
a/drools-ruleunits/drools-ruleunits-dsl/src/main/java/org/drools/ruleunits/dsl/accumulate/AccumulatePattern2.java
 
b/drools-ruleunits/drools-ruleunits-dsl/src/test/java/org/drools/ruleunits/dsl/domain/StockTick.java
similarity index 51%
copy from 
drools-ruleunits/drools-ruleunits-dsl/src/main/java/org/drools/ruleunits/dsl/accumulate/AccumulatePattern2.java
copy to 
drools-ruleunits/drools-ruleunits-dsl/src/test/java/org/drools/ruleunits/dsl/domain/StockTick.java
index d9e2cb0e296..a72c6f19bdd 100644
--- 
a/drools-ruleunits/drools-ruleunits-dsl/src/main/java/org/drools/ruleunits/dsl/accumulate/AccumulatePattern2.java
+++ 
b/drools-ruleunits/drools-ruleunits-dsl/src/test/java/org/drools/ruleunits/dsl/domain/StockTick.java
@@ -16,26 +16,40 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.drools.ruleunits.dsl.accumulate;
+package org.drools.ruleunits.dsl.domain;
 
-import org.drools.model.view.ViewItem;
-import org.drools.ruleunits.dsl.patterns.Pattern1DefImpl;
-import org.drools.ruleunits.dsl.patterns.Pattern2DefImpl;
-import org.drools.ruleunits.dsl.util.RuleDefinition;
+import org.kie.api.definition.type.Duration;
+import org.kie.api.definition.type.Expires;
+import org.kie.api.definition.type.Role;
 
-import static 
org.drools.ruleunits.dsl.accumulate.AccumulatePattern1.createAccumulate1Item;
+@Role(Role.Type.EVENT)
+@Duration("duration")
+@Expires("10s")
+public class StockTick {
 
-public class AccumulatePattern2<A, B, C> extends Pattern2DefImpl<A, C> {
+    private final String company;
+    private final long duration;
 
-    private final Accumulator1<B, C> acc;
+    public StockTick(String company) {
+        this.company = company;
+        this.duration = 0;
+    }
+
+    public StockTick(String company, long duration) {
+        this.company = company;
+        this.duration = duration;
+    }
+
+    public String getCompany() {
+        return company;
+    }
 
-    public AccumulatePattern2(RuleDefinition rule, Pattern1DefImpl<A> 
patternA, Pattern1DefImpl<C> patternC, Accumulator1<B, C> acc) {
-        super(rule, patternA, patternC);
-        this.acc = acc;
+    public long getDuration() {
+        return duration;
     }
 
     @Override
-    public ViewItem toExecModelItem() {
-        return createAccumulate1Item(patternB, getVariable(), acc);
+    public String toString() {
+        return "StockTick [company=" + company + ", duration=" + duration + 
"]";
     }
-}
\ No newline at end of file
+}


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

Reply via email to