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

quantranhong1999 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/james-project.git


The following commit(s) were added to refs/heads/master by this push:
     new 0c8d732cf1 JAMES-4212 Use a string based representation for groups
0c8d732cf1 is described below

commit 0c8d732cf1147dea0f0bed791e71129f224da869
Author: Benoit TELLIER <[email protected]>
AuthorDate: Wed Jun 3 17:06:21 2026 +0200

    JAMES-4212 Use a string based representation for groups
---
 .../main/java/org/apache/james/events/Group.java   | 62 +++++++++++++++-------
 .../james/events/DispatchingFailureGroupTest.java  | 10 ++--
 .../java/org/apache/james/events/GroupTest.java    | 30 ++++++++---
 3 files changed, 72 insertions(+), 30 deletions(-)

diff --git a/event-bus/api/src/main/java/org/apache/james/events/Group.java 
b/event-bus/api/src/main/java/org/apache/james/events/Group.java
index 555a793102..6c12321949 100644
--- a/event-bus/api/src/main/java/org/apache/james/events/Group.java
+++ b/event-bus/api/src/main/java/org/apache/james/events/Group.java
@@ -19,58 +19,84 @@
 
 package org.apache.james.events;
 
-import java.lang.reflect.InvocationTargetException;
 import java.util.Objects;
 
 import org.apache.james.mailbox.events.GenericGroup;
 
-import com.google.common.base.Preconditions;
 import com.google.common.base.Strings;
 
 public class Group {
     public static class GroupDeserializationException extends Exception {
+        GroupDeserializationException(String message) {
+            super(message);
+        }
+
         GroupDeserializationException(Throwable cause) {
             super(cause);
         }
     }
 
+    /**
+     * A {@link Group} identified solely by its serialized form.
+     *
+     * Groups are encoded as a plain String, historically the fully qualified 
name of the concrete
+     * {@link Group} subclass. Deserialization purposely no longer 
instantiates that class: requiring the
+     * class to be loadable broke the reprocessing of dead-lettered events 
whose Group is defined in an
+     * extension that is not part of the default class path (extensions are 
loaded in a dedicated class
+     * loader). Since {@link Group} identity (see {@link #equals(Object)} / 
{@link #hashCode()}) relies on
+     * {@link #asString()}, a {@code DeserializedGroup} is interchangeable 
with the concrete instance
+     * registered by the running extension, both for lookups in registration 
maps and for redelivery.
+     */
+    private static class DeserializedGroup extends Group {
+        private final String value;
+
+        private DeserializedGroup(String value) {
+            this.value = value;
+        }
+
+        @Override
+        public String asString() {
+            return value;
+        }
+    }
+
     public static Group deserialize(String serializedGroup) throws 
GroupDeserializationException {
+        if (Strings.isNullOrEmpty(serializedGroup)) {
+            throw new GroupDeserializationException("A serialized group can 
not be null or empty");
+        }
         try {
-            Preconditions.checkNotNull(serializedGroup, "A serialized group 
can not be null");
-            
Preconditions.checkArgument(!Strings.isNullOrEmpty(serializedGroup), "A 
serialized group can not be empty");
-
             if (serializedGroup.startsWith(GenericGroup.class.getName() + 
GenericGroup.DELIMITER)) {
                 return new 
GenericGroup(serializedGroup.substring(GenericGroup.class.getName().length() + 
1));
-            } else if 
(serializedGroup.startsWith(DispatchingFailureGroup.class.getName() + 
DispatchingFailureGroup.DELIMITER)) {
+            }
+            if 
(serializedGroup.startsWith(DispatchingFailureGroup.class.getName() + 
DispatchingFailureGroup.DELIMITER)) {
                 return DispatchingFailureGroup.from(serializedGroup);
             }
-
-            Class<?> groupClass = Class.forName(serializedGroup);
-            return instantiateGroup(groupClass);
+            if (!serializedGroup.contains(".")) {
+                throw new GroupDeserializationException("A serialized group 
must be a fully qualified name: " + serializedGroup);
+            }
+            return new DeserializedGroup(serializedGroup);
+        } catch (GroupDeserializationException e) {
+            throw e;
         } catch (Exception e) {
             throw new GroupDeserializationException(e);
         }
     }
 
-    private static Group instantiateGroup(Class<?> aClass) throws 
InstantiationException, IllegalAccessException, NoSuchMethodException, 
InvocationTargetException {
-        Preconditions.checkArgument(Group.class.isAssignableFrom(aClass), "The 
supplied class is not a group: %s", aClass.getName());
-        return (Group) aClass.getDeclaredConstructor().newInstance();
-    }
-
     public String asString() {
         return getClass().getName();
     }
 
     @Override
     public boolean equals(Object o) {
-        if (o == null) {
-            return false;
+        if (o instanceof Group) {
+            Group group = (Group) o;
+            return Objects.equals(asString(), group.asString());
         }
-        return this.getClass().equals(o.getClass());
+        return false;
     }
 
     @Override
     public int hashCode() {
-        return Objects.hash(getClass());
+        return Objects.hash(asString());
     }
 }
diff --git 
a/event-bus/api/src/test/java/org/apache/james/events/DispatchingFailureGroupTest.java
 
b/event-bus/api/src/test/java/org/apache/james/events/DispatchingFailureGroupTest.java
index 43c0d03a10..a94816ec5f 100644
--- 
a/event-bus/api/src/test/java/org/apache/james/events/DispatchingFailureGroupTest.java
+++ 
b/event-bus/api/src/test/java/org/apache/james/events/DispatchingFailureGroupTest.java
@@ -20,7 +20,6 @@
 package org.apache.james.events;
 
 import static org.assertj.core.api.Assertions.assertThat;
-import static org.assertj.core.api.Assertions.assertThatThrownBy;
 
 import org.junit.jupiter.api.Test;
 
@@ -39,8 +38,11 @@ public class DispatchingFailureGroupTest {
     }
 
     @Test
-    void deserializeShouldThrowWhenMissingDelimiter() {
-        assertThatThrownBy(() -> 
Group.deserialize(DispatchingFailureGroup.class.getName()))
-            .isInstanceOf(Group.GroupDeserializationException.class);
+    void 
deserializeShouldNotReturnDispatchingFailureGroupWhenMissingDelimiter() throws 
Group.GroupDeserializationException {
+        // Without the delimiter the value is treated as a plain group 
identity, not as a DispatchingFailureGroup.
+        assertThat(Group.deserialize(DispatchingFailureGroup.class.getName()))
+            .isNotInstanceOf(DispatchingFailureGroup.class)
+            .extracting(Group::asString)
+            .isEqualTo(DispatchingFailureGroup.class.getName());
     }
 }
diff --git a/event-bus/api/src/test/java/org/apache/james/events/GroupTest.java 
b/event-bus/api/src/test/java/org/apache/james/events/GroupTest.java
index 0d8a663a23..d4a5c8d5f0 100644
--- a/event-bus/api/src/test/java/org/apache/james/events/GroupTest.java
+++ b/event-bus/api/src/test/java/org/apache/james/events/GroupTest.java
@@ -103,20 +103,34 @@ class GroupTest {
     }
 
     @Test
-    void deserializeShouldThrowWhenClassNotFound() {
-        assertThatThrownBy(() -> 
Group.deserialize("org.apache.james.events.Noone"))
-            .isInstanceOf(Group.GroupDeserializationException.class);
+    void deserializeShouldNotRequireTheClassToBeLoadable() throws Exception {
+        // Groups defined in extensions are not part of the default class 
path: deserialization
+        // must not rely on Class.forName so that such groups can still be 
reprocessed.
+        
assertThat(Group.deserialize("org.apache.james.events.Noone").asString())
+            .isEqualTo("org.apache.james.events.Noone");
     }
 
     @Test
-    void deserializeShouldThrowWhenNotAGroup() {
-        assertThatThrownBy(() -> Group.deserialize("java.lang.String"))
-            .isInstanceOf(Group.GroupDeserializationException.class);
+    void deserializeShouldMatchConcreteGroupSharingItsName() throws Exception {
+        
assertThat(Group.deserialize("org.apache.james.events.EventBusTestFixture$GroupA"))
+            .isEqualTo(new EventBusTestFixture.GroupA());
+    }
+
+    @Test
+    void deserializeShouldNotRequireAGroupClass() throws Exception {
+        assertThat(Group.deserialize("java.lang.String").asString())
+            .isEqualTo("java.lang.String");
+    }
+
+    @Test
+    void deserializeShouldNotRequireANoArgConstructor() throws Exception {
+        
assertThat(Group.deserialize("org.apache.james.mailbox.events.GenericGroup").asString())
+            .isEqualTo("org.apache.james.mailbox.events.GenericGroup");
     }
 
     @Test
-    void deserializeShouldThrowWhenConstructorArgumentsRequired() {
-        assertThatThrownBy(() -> 
Group.deserialize("org.apache.james.mailbox.events.GenericGroup"))
+    void deserializeShouldThrowWhenNotAFullyQualifiedName() {
+        assertThatThrownBy(() -> Group.deserialize("invalid"))
             .isInstanceOf(Group.GroupDeserializationException.class);
     }
 


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

Reply via email to