Repository: spineeventengine/core-java
Issue: Events.clearEnrichments should clear enrichments for all parent context

Current implementation clears enrichments only for the current context and its origin, but in theory, the `origin` of the current context origin can also have enrichment.

In order to be sure that **no** enrichments are stored, the `clearEnrichments` should recursively clear enrichments from origins. 

Changed files:
core/src/main/java/io/spine/core/Enrichments.java, server/src/test/java/io/spine/core/EventTest.java, core/src/main/java/io/spine/core/EventMixin.java

diff --git a/core/src/main/java/io/spine/core/Enrichments.java b/core/src/main/java/io/spine/core/Enrichments.java
index 5fe65c1331..416cd571f3 100644
--- a/core/src/main/java/io/spine/core/Enrichments.java
+++ b/core/src/main/java/io/spine/core/Enrichments.java
@@ -26,8 +26,10 @@ import io.spine.core.Enrichment.Container;
 import io.spine.core.Enrichment.ModeCase;
 import io.spine.type.TypeName;
 
+import java.util.Deque;
 import java.util.Optional;
 
+import static com.google.common.collect.Queues.newArrayDeque;
 import static io.spine.protobuf.AnyPacker.unpack;
 import static io.spine.util.Exceptions.newIllegalStateException;
 
@@ -86,4 +88,74 @@ final class Enrichments {
                                      .map(packed -> unpack(packed, enrichmentClass));
         return result;
     }
+
+    /**
+     * Clears the enrichments from the {@code event} and its origin.
+     */
+    @SuppressWarnings("deprecation") // Uses the deprecated field to be sure to clean up old data.
+    static Event clear(Event event) {
+        EventContext context = event.getContext();
+        EventContext.OriginCase originCase = context.getOriginCase();
+        EventContext.Builder resultContext = context.toBuilder()
+                                                    .clearEnrichment();
+        if (originCase == EventContext.OriginCase.EVENT_CONTEXT) {
+            resultContext.setEventContext(context.getEventContext()
+                                                 .toBuilder()
+                                                 .clearEnrichment()
+                                                 .build());
+        }
+        Event result = event.toBuilder()
+                            .setContext(resultContext.build())
+                            .build();
+        return result;
+    }
+
+    /**
+     * Clears the enrichments from the {@code event} and all of its parent contexts.
+     */
+    @SuppressWarnings({
+            "deprecation" /* Uses the deprecated field to be sure to clean up old data. */,
+            "ConstantConditions" /* Checked logically. */})
+    static Event clearAll(Event event) {
+        EventContext.Builder eventContext = event.getContext()
+                                                 .toBuilder()
+                                                 .clearEnrichment();
+        Deque<EventContext.Builder> contexts = eventContextHierarchy(eventContext);
+
+        EventContext.Builder context = contexts.pollLast();
+        EventContext.Builder next = contexts.pollLast();
+        while (next != null) {
+            context = next.setEventContext(context);
+            next = contexts.pollLast();
+        }
+        Event result = event.toBuilder()
+                            .setContext(context.build())
+                            .build();
+        return result;
+    }
+
+    /**
+     * Traverses the event parent context hierarchy until non-{@link EventContext} origin is found.
+     *
+     * <p>All of the {@link EventContext}-kind origins are collected and returned as
+     * {@code Deque<EventContext.Builder>}, where the deepest origin resides last.
+     */
+    @SuppressWarnings("deprecation") // Uses the deprecated field to be sure to clean up old data.
+    private static Deque<EventContext.Builder>
+    eventContextHierarchy(EventContext.Builder eventContext) {
+
+        EventContext.Builder child = eventContext;
+        EventContext.OriginCase originCase = child.getOriginCase();
+        Deque<EventContext.Builder> contexts = newArrayDeque();
+        contexts.add(child);
+        while (originCase == EventContext.OriginCase.EVENT_CONTEXT) {
+            EventContext.Builder origin = child.getEventContext()
+                                               .toBuilder()
+                                               .clearEnrichment();
+            contexts.add(origin);
+            child = origin;
+            originCase = child.getOriginCase();
+        }
+        return contexts;
+    }
 }
diff --git a/core/src/main/java/io/spine/core/EventMixin.java b/core/src/main/java/io/spine/core/EventMixin.java
index 6029aceed3..833c32949c 100644
--- a/core/src/main/java/io/spine/core/EventMixin.java
+++ b/core/src/main/java/io/spine/core/EventMixin.java
@@ -103,32 +103,42 @@ public interface EventMixin extends Signal<EventId, EventMessage, EventContext>,
      *     <li>the enrichment from the first-level origin.</li>
      * </ul>
      *
-     * <p>Enrichments will not be removed from second-level and deeper origins,
-     * because it's a heavy performance operation.
+     * <p>This method does not remove enrichments from second-level and deeper origins to avoid a
+     * heavy performance operation.
+     *
+     * <p>To remove enrichments from the whole parent context hierarchy, use
+     * {@link #clearAllEnrichments()}.
      *
      * @return the event without enrichments
      */
-    @SuppressWarnings({
-            "ClassReferencesSubclass", //`Event` is the only case of this mixin.
-            "deprecation" // Uses the `event_context` field to be sure to clean up old data.
-    })
+    @SuppressWarnings("ClassReferencesSubclass") // `Event` is the only case of this mixin.
     @Internal
     default Event clearEnrichments() {
-        EventContext context = context();
-        EventContext.OriginCase originCase = context.getOriginCase();
-        EventContext.Builder resultContext = context.toBuilder()
-                                                    .clearEnrichment();
-        if (originCase == EventContext.OriginCase.EVENT_CONTEXT) {
-            resultContext.setEventContext(context.getEventContext()
-                                                 .toBuilder()
-                                                 .clearEnrichment()
-                                                 .build());
-        }
-        Event thisEvent = (Event) this;
-        Event result = thisEvent.toBuilder()
-                                .setContext(resultContext.build())
-                                .build();
-        return result;
+        return Enrichments.clear((Event) this);
+    }
+
+    /**
+     * Creates a copy of this instance with enrichments cleared from self and all parent contexts.
+     *
+     * <p>Use this method to decrease a size of an event, if enrichments aren't important.
+     *
+     * <p>A result won't contain:
+     * <ul>
+     *     <li>the enrichment from the event context;</li>
+     *     <li>the enrichment from the first-level origin;</li>
+     *     <li>the enrichment from the second-level and deeper origins.</li>
+     * </ul>
+     *
+     * <p>This method is performance-heavy.
+     *
+     * <p>For the "lightweight" version of the method, see {@link #clearEnrichments()}.
+     *
+     * @return the event without enrichments
+     */
+    @SuppressWarnings("ClassReferencesSubclass") // `Event` is the only case of this mixin.
+    @Internal
+    default Event clearAllEnrichments() {
+        return Enrichments.clearAll((Event) this);
     }
 
     /**
diff --git a/server/src/test/java/io/spine/core/EventTest.java b/server/src/test/java/io/spine/core/EventTest.java
index e8009f78bd..1dcdc90b11 100644
--- a/server/src/test/java/io/spine/core/EventTest.java
+++ b/server/src/test/java/io/spine/core/EventTest.java
@@ -56,7 +56,7 @@ import static org.junit.jupiter.api.Assertions.assertNotNull;
  * <p>This test suite is placed under the {@code server} module to avoid dependency on the event
  * generation code which belongs to server-side.
  */
-@DisplayName("Event should")
+@DisplayName("`Event` should")
 public class EventTest extends UtilityClassTest<Events> {
 
     private static final TestActorRequestFactory requestFactory =
@@ -204,11 +204,6 @@ public class EventTest extends UtilityClassTest<Events> {
             assertThat(event.tenant())
                     .isEqualTo(targetTenantId);
         }
-
-        private EventContext.Builder contextWithoutOrigin() {
-            return context.toBuilder()
-                          .clearOrigin();
-        }
     }
 
     @Test
@@ -234,4 +229,70 @@ public class EventTest extends UtilityClassTest<Events> {
         assertThat(event.isRejection())
                 .isFalse();
     }
+
+    @SuppressWarnings("deprecation") // Required for backward compatibility.
+    @Test
+    @DisplayName("clear enrichments from the event and its origin")
+    void clearEnrichments() {
+        Enrichment someEnrichment = Enrichment
+                .newBuilder()
+                .setDoNotEnrich(true)
+                .build();
+        EventContext.Builder grandOriginContext =
+                context.toBuilder()
+                       .setEnrichment(someEnrichment);
+        EventContext.Builder originContext =
+                contextWithoutOrigin()
+                        .setEventContext(grandOriginContext)
+                        .setEnrichment(someEnrichment);
+        EventContext eventContext =
+                contextWithoutOrigin()
+                        .setEventContext(originContext)
+                        .setEnrichment(someEnrichment)
+                        .build();
+        Event event = event(eventContext);
+
+        Event eventWithoutEnrichments = event.clearEnrichments();
+
+        EventContext context = eventWithoutEnrichments.getContext();
+        EventContext origin = context.getEventContext();
+        EventContext grandOrigin = origin.getEventContext();
+
+        assertThat(context.hasEnrichment()).isFalse();
+        assertThat(origin.hasEnrichment()).isFalse();
+        assertThat(grandOrigin.hasEnrichment()).isTrue();
+    }
+
+    @SuppressWarnings("deprecation") // Required for backward compatibility.
+    @Test
+    @DisplayName("clear enrichment hierarchy")
+    void clearEnrichmentHierarchy() {
+        Enrichment someEnrichment = Enrichment
+                .newBuilder()
+                .setDoNotEnrich(true)
+                .build();
+        EventContext.Builder grandOriginContext =
+                context.toBuilder()
+                       .setEnrichment(someEnrichment);
+        EventContext.Builder originContext =
+                contextWithoutOrigin()
+                        .setEventContext(grandOriginContext);
+        EventContext context =
+                contextWithoutOrigin()
+                        .setEventContext(originContext)
+                        .build();
+        Event event = event(context);
+
+        Event eventWithoutEnrichments = event.clearAllEnrichments();
+
+        EventContext grandOrigin = eventWithoutEnrichments.getContext()
+                                                          .getEventContext()
+                                                          .getEventContext();
+        assertThat(grandOrigin.hasEnrichment()).isFalse();
+    }
+
+    private EventContext.Builder contextWithoutOrigin() {
+        return context.toBuilder()
+                      .clearOrigin();
+    }
 }