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

commit d39a9fc1e2242d1cb32cf3ca011dd544910e0793
Author: Quan Tran <[email protected]>
AuthorDate: Tue Jul 7 09:21:36 2026 +0700

    JAMES-4210 Resolve configured SASL factories with Guice
    
    Add shared Guice resolution for auth.saslMechanisms, supporting built-in 
simple names, external FQCNs, server-specific factory creation, duplicate 
mechanism deduplication, and configured order preservation.
---
 server/container/guice/common/pom.xml              |   8 +
 .../james/modules/SaslMechanismFactories.java      |  31 +++
 .../james/utils/GuiceSaslMechanismResolver.java    |  89 ++++++++
 .../TestingDefaultPackageSaslMechanismFactory.java |  33 +++
 .../ConfigurableFakeSaslMechanismFactory.java      |  32 +++
 .../utils/ExternalFakeSaslMechanismFactory.java    |  32 +++
 .../apache/james/utils/FixedNameSaslMechanism.java |  69 ++++++
 .../utils/GuiceSaslMechanismResolverTest.java      | 242 +++++++++++++++++++++
 8 files changed, 536 insertions(+)

diff --git a/server/container/guice/common/pom.xml 
b/server/container/guice/common/pom.xml
index c6a56f7028..f8001ee29e 100644
--- a/server/container/guice/common/pom.xml
+++ b/server/container/guice/common/pom.xml
@@ -74,6 +74,10 @@
             <groupId>${james.groupId}</groupId>
             <artifactId>james-server-guice-netty</artifactId>
         </dependency>
+        <dependency>
+            <groupId>${james.groupId}</groupId>
+            <artifactId>james-server-guice-utils</artifactId>
+        </dependency>
         <dependency>
             <groupId>${james.groupId}</groupId>
             <artifactId>james-server-mailrepository-memory</artifactId>
@@ -126,6 +130,10 @@
             <artifactId>testing-base</artifactId>
             <scope>test</scope>
         </dependency>
+        <dependency>
+            <groupId>${james.protocols.groupId}</groupId>
+            <artifactId>protocols-api</artifactId>
+        </dependency>
         <dependency>
             <groupId>com.github.stefanbirkner</groupId>
             <artifactId>system-lambda</artifactId>
diff --git 
a/server/container/guice/common/src/main/java/org/apache/james/modules/SaslMechanismFactories.java
 
b/server/container/guice/common/src/main/java/org/apache/james/modules/SaslMechanismFactories.java
new file mode 100644
index 0000000000..ad86c882d7
--- /dev/null
+++ 
b/server/container/guice/common/src/main/java/org/apache/james/modules/SaslMechanismFactories.java
@@ -0,0 +1,31 @@
+/****************************************************************
+ * 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.apache.james.modules;
+
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import java.lang.annotation.Retention;
+
+import com.google.inject.BindingAnnotation;
+
+@BindingAnnotation
+@Retention(RUNTIME)
+public @interface SaslMechanismFactories {
+}
diff --git 
a/server/container/guice/common/src/main/java/org/apache/james/utils/GuiceSaslMechanismResolver.java
 
b/server/container/guice/common/src/main/java/org/apache/james/utils/GuiceSaslMechanismResolver.java
new file mode 100644
index 0000000000..c34d2e5804
--- /dev/null
+++ 
b/server/container/guice/common/src/main/java/org/apache/james/utils/GuiceSaslMechanismResolver.java
@@ -0,0 +1,89 @@
+/****************************************************************
+ * 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.apache.james.utils;
+
+import java.util.Collection;
+import java.util.LinkedHashMap;
+import java.util.Locale;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+import jakarta.inject.Inject;
+
+import org.apache.commons.configuration2.HierarchicalConfiguration;
+import org.apache.commons.configuration2.ex.ConfigurationException;
+import org.apache.commons.configuration2.tree.ImmutableNode;
+import org.apache.james.protocols.api.sasl.SaslMechanism;
+import org.apache.james.protocols.api.sasl.SaslMechanismFactory;
+
+import com.github.fge.lambdas.Throwing;
+import com.google.common.collect.ImmutableList;
+
+public class GuiceSaslMechanismResolver {
+    private static final NamingScheme SASL_FACTORY_NAMING_SCHEME =
+        new 
NamingScheme.OptionalPackagePrefix(PackageName.of("org.apache.james.protocols.sasl"));
+
+    private final GuiceLoader.InvocationPerformer<SaslMechanismFactory> 
factoryLoader;
+
+    @Inject
+    public GuiceSaslMechanismResolver(GuiceLoader guiceLoader) {
+        this.factoryLoader = 
guiceLoader.withNamingSheme(SASL_FACTORY_NAMING_SCHEME);
+    }
+
+    public ImmutableList<SaslMechanism> resolve(Collection<String> 
configuredFactoryClassNames,
+                                                
ImmutableList<SaslMechanismFactory> enabledDefaultFactories,
+                                                
HierarchicalConfiguration<ImmutableNode> serverConfiguration) throws 
ConfigurationException {
+        try {
+            ImmutableList<SaslMechanismFactory> factories = 
configuredFactoryClassNames.isEmpty()
+                ? enabledDefaultFactories
+                : configuredFactoryClassNames.stream()
+                    .map(Throwing.function(this::instantiateFactory))
+                    .collect(ImmutableList.toImmutableList());
+
+            return factories.stream()
+                .map(Throwing.function(factory -> 
factory.create(serverConfiguration)))
+                .collect(Collectors.toMap(
+                    mechanism -> normalize(mechanism.name()),
+                    Function.identity(),
+                    (first, second) -> first,
+                    LinkedHashMap::new))
+                .values()
+                .stream()
+                .collect(ImmutableList.toImmutableList());
+        } catch (RuntimeException e) {
+            if (e.getCause() instanceof ConfigurationException 
configurationException) {
+                throw configurationException;
+            }
+            throw e;
+        }
+    }
+
+    private SaslMechanismFactory instantiateFactory(String className) throws 
ConfigurationException {
+        try {
+            return factoryLoader.instantiate(new ClassName(className));
+        } catch (ClassNotFoundException | RuntimeException e) {
+            throw new ConfigurationException("Can not load SASL mechanism 
factory " + className, e);
+        }
+    }
+
+    private String normalize(String mechanismName) {
+        return mechanismName.toUpperCase(Locale.US);
+    }
+}
diff --git 
a/server/container/guice/common/src/test/java/org/apache/james/protocols/sasl/TestingDefaultPackageSaslMechanismFactory.java
 
b/server/container/guice/common/src/test/java/org/apache/james/protocols/sasl/TestingDefaultPackageSaslMechanismFactory.java
new file mode 100644
index 0000000000..850ed4c1ce
--- /dev/null
+++ 
b/server/container/guice/common/src/test/java/org/apache/james/protocols/sasl/TestingDefaultPackageSaslMechanismFactory.java
@@ -0,0 +1,33 @@
+/****************************************************************
+ * 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.apache.james.protocols.sasl;
+
+import org.apache.commons.configuration2.HierarchicalConfiguration;
+import org.apache.commons.configuration2.tree.ImmutableNode;
+import org.apache.james.protocols.api.sasl.SaslMechanism;
+import org.apache.james.protocols.api.sasl.SaslMechanismFactory;
+import org.apache.james.utils.FixedNameSaslMechanism;
+
+public class TestingDefaultPackageSaslMechanismFactory implements 
SaslMechanismFactory {
+    @Override
+    public SaslMechanism create(HierarchicalConfiguration<ImmutableNode> 
serverConfiguration) {
+        return new FixedNameSaslMechanism("DEFAULT");
+    }
+}
diff --git 
a/server/container/guice/common/src/test/java/org/apache/james/utils/ConfigurableFakeSaslMechanismFactory.java
 
b/server/container/guice/common/src/test/java/org/apache/james/utils/ConfigurableFakeSaslMechanismFactory.java
new file mode 100644
index 0000000000..c1f59a2eef
--- /dev/null
+++ 
b/server/container/guice/common/src/test/java/org/apache/james/utils/ConfigurableFakeSaslMechanismFactory.java
@@ -0,0 +1,32 @@
+/****************************************************************
+ * 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.apache.james.utils;
+
+import org.apache.commons.configuration2.HierarchicalConfiguration;
+import org.apache.commons.configuration2.tree.ImmutableNode;
+import org.apache.james.protocols.api.sasl.SaslMechanism;
+import org.apache.james.protocols.api.sasl.SaslMechanismFactory;
+
+public class ConfigurableFakeSaslMechanismFactory implements 
SaslMechanismFactory {
+    @Override
+    public SaslMechanism create(HierarchicalConfiguration<ImmutableNode> 
serverConfiguration) {
+        return new 
FixedNameSaslMechanism(serverConfiguration.getString("auth.example.realm"));
+    }
+}
diff --git 
a/server/container/guice/common/src/test/java/org/apache/james/utils/ExternalFakeSaslMechanismFactory.java
 
b/server/container/guice/common/src/test/java/org/apache/james/utils/ExternalFakeSaslMechanismFactory.java
new file mode 100644
index 0000000000..d8607ac7a2
--- /dev/null
+++ 
b/server/container/guice/common/src/test/java/org/apache/james/utils/ExternalFakeSaslMechanismFactory.java
@@ -0,0 +1,32 @@
+/****************************************************************
+ * 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.apache.james.utils;
+
+import org.apache.commons.configuration2.HierarchicalConfiguration;
+import org.apache.commons.configuration2.tree.ImmutableNode;
+import org.apache.james.protocols.api.sasl.SaslMechanism;
+import org.apache.james.protocols.api.sasl.SaslMechanismFactory;
+
+public class ExternalFakeSaslMechanismFactory implements SaslMechanismFactory {
+    @Override
+    public SaslMechanism create(HierarchicalConfiguration<ImmutableNode> 
serverConfiguration) {
+        return new FixedNameSaslMechanism("EXTERNAL-FAKE");
+    }
+}
diff --git 
a/server/container/guice/common/src/test/java/org/apache/james/utils/FixedNameSaslMechanism.java
 
b/server/container/guice/common/src/test/java/org/apache/james/utils/FixedNameSaslMechanism.java
new file mode 100644
index 0000000000..9b82df2cb2
--- /dev/null
+++ 
b/server/container/guice/common/src/test/java/org/apache/james/utils/FixedNameSaslMechanism.java
@@ -0,0 +1,69 @@
+/****************************************************************
+ * 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.apache.james.utils;
+
+import org.apache.james.protocols.api.sasl.SaslAuthenticator;
+import org.apache.james.protocols.api.sasl.SaslExchange;
+import org.apache.james.protocols.api.sasl.SaslFailure;
+import org.apache.james.protocols.api.sasl.SaslInitialRequest;
+import org.apache.james.protocols.api.sasl.SaslMechanism;
+import org.apache.james.protocols.api.sasl.SaslStep;
+
+public class FixedNameSaslMechanism implements SaslMechanism {
+    private final String name;
+
+    public FixedNameSaslMechanism(String name) {
+        this.name = name;
+    }
+
+    @Override
+    public String name() {
+        return name;
+    }
+
+    @Override
+    public SaslExchange start(SaslInitialRequest request, SaslAuthenticator 
authenticator) {
+        return new FixedStepExchange();
+    }
+
+    private record FixedStepExchange() implements SaslExchange {
+        @Override
+        public SaslStep firstStep() {
+            return failure();
+        }
+
+        @Override
+        public SaslStep onResponse(byte[] clientResponse) {
+            return failure();
+        }
+
+        @Override
+        public void abort() {
+        }
+
+        @Override
+        public void close() {
+        }
+
+        private SaslStep failure() {
+            return new SaslStep.Failure(SaslFailure.malformed("not 
implemented"));
+        }
+    }
+}
diff --git 
a/server/container/guice/common/src/test/java/org/apache/james/utils/GuiceSaslMechanismResolverTest.java
 
b/server/container/guice/common/src/test/java/org/apache/james/utils/GuiceSaslMechanismResolverTest.java
new file mode 100644
index 0000000000..e3551ee400
--- /dev/null
+++ 
b/server/container/guice/common/src/test/java/org/apache/james/utils/GuiceSaslMechanismResolverTest.java
@@ -0,0 +1,242 @@
+/****************************************************************
+ * 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.apache.james.utils;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+import java.lang.reflect.InvocationTargetException;
+import java.util.Optional;
+
+import org.apache.commons.configuration2.BaseHierarchicalConfiguration;
+import org.apache.commons.configuration2.HierarchicalConfiguration;
+import org.apache.commons.configuration2.ex.ConfigurationException;
+import org.apache.commons.configuration2.tree.ImmutableNode;
+import org.apache.james.protocols.api.sasl.SaslMechanism;
+import org.apache.james.protocols.api.sasl.SaslMechanismFactory;
+import org.junit.jupiter.api.Test;
+
+import com.google.common.collect.ImmutableList;
+import com.google.inject.Module;
+
+class GuiceSaslMechanismResolverTest {
+    private static final HierarchicalConfiguration<ImmutableNode> 
EMPTY_CONFIGURATION = new BaseHierarchicalConfiguration();
+
+    @Test
+    void 
resolveShouldUseEnabledDefaultFactoriesWhenNoFactoryClassIsConfigured() throws 
Exception {
+        // GIVEN an absent auth.saslMechanisms configuration and an ordered 
default factory list
+        GuiceSaslMechanismResolver testee = new GuiceSaslMechanismResolver(new 
ReflectionGuiceLoader());
+
+        // WHEN resolving mechanisms for this server
+        ImmutableList<SaslMechanism> mechanisms = 
testee.resolve(ImmutableList.of(),
+            ImmutableList.of(factory("PLAIN"), factory("OAUTHBEARER")),
+            EMPTY_CONFIGURATION);
+
+        // THEN defaults are used in their declared order
+        assertThat(mechanisms)
+            .extracting(SaslMechanism::name)
+            .containsExactly("PLAIN", "OAUTHBEARER");
+    }
+
+    @Test
+    void resolveShouldResolveSimpleFactoryNameFromDefaultSaslPackage() throws 
Exception {
+        // GIVEN a configured built-in SASL factory simple name
+        GuiceSaslMechanismResolver testee = new GuiceSaslMechanismResolver(new 
ReflectionGuiceLoader());
+
+        // WHEN resolving it
+        ImmutableList<SaslMechanism> mechanisms = 
testee.resolve(ImmutableList.of("TestingDefaultPackageSaslMechanismFactory"),
+            ImmutableList.of(),
+            EMPTY_CONFIGURATION);
+
+        // THEN the factory is loaded from org.apache.james.protocols.sasl
+        assertThat(mechanisms)
+            .extracting(SaslMechanism::name)
+            .containsExactly("DEFAULT");
+    }
+
+    @Test
+    void resolveShouldResolveFullyQualifiedFactoryName() throws Exception {
+        // GIVEN a configured extension factory FQCN
+        GuiceSaslMechanismResolver testee = new GuiceSaslMechanismResolver(new 
ReflectionGuiceLoader());
+
+        // WHEN resolving it
+        ImmutableList<SaslMechanism> mechanisms = 
testee.resolve(ImmutableList.of(ExternalFakeSaslMechanismFactory.class.getCanonicalName()),
+            ImmutableList.of(),
+            EMPTY_CONFIGURATION);
+
+        // THEN the extension factory is loaded directly
+        assertThat(mechanisms)
+            .extracting(SaslMechanism::name)
+            .containsExactly("EXTERNAL-FAKE");
+    }
+
+    @Test
+    void resolveShouldUseConfiguredFactoriesInsteadOfDefaults() throws 
Exception {
+        // GIVEN both defaults and an explicit auth.saslMechanisms 
configuration
+        GuiceSaslMechanismResolver testee = new GuiceSaslMechanismResolver(new 
ReflectionGuiceLoader());
+
+        // WHEN resolving mechanisms
+        ImmutableList<SaslMechanism> mechanisms = 
testee.resolve(ImmutableList.of(ExternalFakeSaslMechanismFactory.class.getCanonicalName()),
+            ImmutableList.of(factory("DEFAULT")),
+            EMPTY_CONFIGURATION);
+
+        // THEN the configured list replaces defaults
+        assertThat(mechanisms)
+            .extracting(SaslMechanism::name)
+            .containsExactly("EXTERNAL-FAKE");
+    }
+
+    @Test
+    void 
resolveShouldCreateConfiguredFactoriesFromCurrentServerConfiguration() throws 
Exception {
+        // GIVEN two server configurations using the same configured SASL 
factory
+        BaseHierarchicalConfiguration firstConfiguration = new 
BaseHierarchicalConfiguration();
+        firstConfiguration.addProperty("auth.example.realm", "FIRST");
+        BaseHierarchicalConfiguration secondConfiguration = new 
BaseHierarchicalConfiguration();
+        secondConfiguration.addProperty("auth.example.realm", "SECOND");
+        GuiceSaslMechanismResolver testee = new GuiceSaslMechanismResolver(new 
ReflectionGuiceLoader());
+
+        // WHEN resolving the same configured factory for each server
+        SaslMechanism firstMechanism = 
testee.resolve(ImmutableList.of(ConfigurableFakeSaslMechanismFactory.class.getCanonicalName()),
+                ImmutableList.of(),
+                firstConfiguration)
+            .getFirst();
+        SaslMechanism secondMechanism = 
testee.resolve(ImmutableList.of(ConfigurableFakeSaslMechanismFactory.class.getCanonicalName()),
+                ImmutableList.of(),
+                secondConfiguration)
+            .getFirst();
+
+        // THEN each mechanism is created from that server's configuration, 
not from a global singleton
+        assertThat(firstMechanism.name()).isEqualTo("FIRST");
+        assertThat(secondMechanism.name()).isEqualTo("SECOND");
+    }
+
+    @Test
+    void resolveShouldPreserveConfiguredOrderForDistinctMechanisms() throws 
Exception {
+        // GIVEN several distinct SASL mechanism factories in a configured 
order
+        GuiceSaslMechanismResolver testee = new GuiceSaslMechanismResolver(new 
ReflectionGuiceLoader());
+
+        // WHEN resolving them
+        ImmutableList<SaslMechanism> mechanisms = 
testee.resolve(ImmutableList.of(),
+            ImmutableList.of(factory("FIRST"), factory("SECOND"), 
factory("THIRD")),
+            EMPTY_CONFIGURATION);
+
+        // THEN the resolved mechanisms keep the configured order
+        assertThat(mechanisms)
+            .extracting(SaslMechanism::name)
+            .containsExactly("FIRST", "SECOND", "THIRD");
+    }
+
+    @Test
+    void resolveShouldDeduplicateMechanismNamesCaseInsensitively() throws 
Exception {
+        // GIVEN two factories returning the same SASL mechanism name with 
different case
+        GuiceSaslMechanismResolver testee = new GuiceSaslMechanismResolver(new 
ReflectionGuiceLoader());
+
+        // WHEN resolving both factories
+        ImmutableList<SaslMechanism> mechanisms = 
testee.resolve(ImmutableList.of(),
+            ImmutableList.of(factory("DUPLICATE"), factory("duplicate")),
+            EMPTY_CONFIGURATION);
+
+        // THEN first occurrence wins and order remains stable
+        assertThat(mechanisms)
+            .extracting(SaslMechanism::name)
+            .containsExactly("DUPLICATE");
+    }
+
+    @Test
+    void resolveShouldFailWhenConfiguredFactoryClassDoesNotExist() {
+        // GIVEN a resolver used for configured SASL mechanism factory entries
+        GuiceSaslMechanismResolver testee = new GuiceSaslMechanismResolver(new 
ReflectionGuiceLoader());
+
+        // WHEN resolving an unknown factory class name
+        // THEN startup wiring can fail fast with the configured entry in the 
error
+        assertThatThrownBy(() -> 
testee.resolve(ImmutableList.of("MissingSaslMechanismFactory"),
+            ImmutableList.of(),
+            EMPTY_CONFIGURATION))
+            .isInstanceOf(ConfigurationException.class)
+            .hasMessageContaining("MissingSaslMechanismFactory");
+    }
+
+    private static SaslMechanismFactory factory(String mechanismName) {
+        return serverConfiguration -> new 
FixedNameSaslMechanism(mechanismName);
+    }
+
+    private static class ReflectionGuiceLoader implements GuiceLoader {
+        @Override
+        public <T> T instantiate(ClassName className) throws 
ClassNotFoundException {
+            return 
this.<T>withNamingSheme(NamingScheme.IDENTITY).instantiate(className);
+        }
+
+        @Override
+        public <T> InvocationPerformer<T> withNamingSheme(NamingScheme 
namingSheme) {
+            return new ReflectionInvocationPerformer<>(namingSheme);
+        }
+
+        @Override
+        public <T> InvocationPerformer<T> withChildModule(Module childModule) {
+            return new ReflectionInvocationPerformer<>(NamingScheme.IDENTITY);
+        }
+    }
+
+    private static class ReflectionInvocationPerformer<T> implements 
GuiceLoader.InvocationPerformer<T> {
+        private final NamingScheme namingScheme;
+
+        private ReflectionInvocationPerformer(NamingScheme namingScheme) {
+            this.namingScheme = namingScheme;
+        }
+
+        @Override
+        public T instantiate(ClassName className) throws 
ClassNotFoundException {
+            try {
+                return 
locateClass(className).getDeclaredConstructor().newInstance();
+            } catch (InstantiationException | IllegalAccessException | 
InvocationTargetException | NoSuchMethodException e) {
+                throw new ClassNotFoundException(className.getName(), e);
+            }
+        }
+
+        @Override
+        public Class<T> locateClass(ClassName className) throws 
ClassNotFoundException {
+            Optional<Class<T>> locatedClass = 
namingScheme.toFullyQualifiedClassNames(className)
+                .map(FullyQualifiedClassName::getName)
+                .map(this::tryLocateClass)
+                .flatMap(Optional::stream)
+                .findFirst();
+            return locatedClass.orElseThrow(() -> new 
ClassNotFoundException(className.getName()));
+        }
+
+        @Override
+        public GuiceLoader.InvocationPerformer<T> withChildModule(Module 
childModule) {
+            return new ReflectionInvocationPerformer<>(namingScheme);
+        }
+
+        @Override
+        public GuiceLoader.InvocationPerformer<T> withNamingSheme(NamingScheme 
namingSheme) {
+            return new ReflectionInvocationPerformer<>(namingSheme);
+        }
+
+        @SuppressWarnings("unchecked")
+        private Optional<Class<T>> tryLocateClass(String className) {
+            try {
+                return Optional.of((Class<T>) Class.forName(className));
+            } catch (ClassNotFoundException e) {
+                return Optional.empty();
+            }
+        }
+    }
+}


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

Reply via email to