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]