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

lukaszlenart pushed a commit to branch feature/WW-5512-optional-inject-s7
in repository https://gitbox.apache.org/repos/asf/struts.git

commit f64053c1974cda2855fb4be79b7205a5f60ad3b6
Author: Lukasz Lenart <lukaszlen...@apache.org>
AuthorDate: Tue Jan 21 07:39:34 2025 +0100

    WW-5512 Extends the container to support injecting optional parameters into 
constructor
---
 .../org/apache/struts2/inject/ContainerImpl.java   |  54 +++++++-
 .../apache/struts2/inject/ContainerImplTest.java   | 142 +++++++++++++++++----
 2 files changed, 173 insertions(+), 23 deletions(-)

diff --git a/core/src/main/java/org/apache/struts2/inject/ContainerImpl.java 
b/core/src/main/java/org/apache/struts2/inject/ContainerImpl.java
index f6bb10280..a7f626b0b 100644
--- a/core/src/main/java/org/apache/struts2/inject/ContainerImpl.java
+++ b/core/src/main/java/org/apache/struts2/inject/ContainerImpl.java
@@ -242,6 +242,41 @@ class ContainerImpl implements Container {
         return toArray(parameterInjectors);
     }
 
+    /**
+     * Gets parameter injectors with nulls for optional dependencies.
+     *
+     * @param member         to which the parameters belong
+     * @param annotations    on the parameters
+     * @param parameterTypes parameter types
+     * @return injections
+     */
+    <M extends AccessibleObject & Member> ParameterInjector<?>[] 
getParametersInjectorsWithNulls(
+        M member,
+        Annotation[][] annotations,
+        Class<?>[] parameterTypes,
+        String defaultName
+    ) throws MissingDependencyException {
+        final List<ParameterInjector<?>> parameterInjectors = new 
ArrayList<>();
+
+        final Iterator<Annotation[]> annotationsIterator = 
Arrays.asList(annotations).iterator();
+        for (Class<?> parameterType : parameterTypes) {
+            Inject annotation = findInject(annotationsIterator.next());
+            String name = annotation == null ? defaultName : 
annotation.value();
+            Key<?> key = Key.newInstance(parameterType, name);
+            try {
+                parameterInjectors.add(createParameterInjector(key, member));
+            } catch (MissingDependencyException e) {
+                if (annotation != null && annotation.required()) {
+                    throw e;
+                } else {
+                    parameterInjectors.add(createNullParameterInjector(key, 
member));
+                }
+            }
+        }
+
+        return toArray(parameterInjectors);
+    }
+
     <T> ParameterInjector<T> createParameterInjector(Key<T> key, Member 
member) throws MissingDependencyException {
         final InternalFactory<? extends T> factory = getFactory(key);
         if (factory == null) {
@@ -252,6 +287,23 @@ class ContainerImpl implements Container {
         return new ParameterInjector<>(externalContext, factory);
     }
 
+    <T> ParameterInjector<T> createNullParameterInjector(Key<T> key, Member 
member) throws MissingDependencyException {
+        final InternalFactory<? extends T> factory = new InternalFactory<T>() {
+            @Override
+            public T create(InternalContext context) {
+                return null;
+            }
+
+            @Override
+            public Class<? extends T> type() {
+                return key.getType();
+            }
+        };
+
+        final ExternalContext<T> externalContext = 
ExternalContext.newInstance(member, key, this);
+        return new ParameterInjector<>(externalContext, factory);
+    }
+
     private ParameterInjector<?>[] toArray(List<ParameterInjector<?>> 
parameterInjections) {
         return parameterInjections.toArray(new ParameterInjector[0]);
     }
@@ -361,7 +413,7 @@ class ContainerImpl implements Container {
             Inject inject, ContainerImpl container, Constructor<T> 
constructor) throws MissingDependencyException {
             return constructor.getParameterTypes().length == 0
                 ? null // default constructor.
-                : container.getParametersInjectors(
+                : container.getParametersInjectorsWithNulls(
                 constructor,
                 constructor.getParameterAnnotations(),
                 constructor.getParameterTypes(),
diff --git 
a/core/src/test/java/org/apache/struts2/inject/ContainerImplTest.java 
b/core/src/test/java/org/apache/struts2/inject/ContainerImplTest.java
index a98c6f1da..adb6b8276 100644
--- a/core/src/test/java/org/apache/struts2/inject/ContainerImplTest.java
+++ b/core/src/test/java/org/apache/struts2/inject/ContainerImplTest.java
@@ -28,7 +28,9 @@ import java.util.concurrent.Callable;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNotSame;
+import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertThrows;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assume.assumeTrue;
@@ -49,6 +51,7 @@ public class ContainerImplTest {
         ContainerBuilder cb = new ContainerBuilder();
         cb.constant("methodCheck.name", "Lukasz");
         cb.constant("fieldCheck.name", "Lukasz");
+        cb.constant("constructorCheck.name", "Lukasz");
         cb.factory(EarlyInitializable.class, EarlyInitializableBean.class, 
Scope.SINGLETON);
         cb.factory(Initializable.class, InitializableBean.class, 
Scope.SINGLETON);
         cb.factory(EarlyInitializable.class, "prototypeEarlyInitializable", 
EarlyInitializableBean.class, Scope.PROTOTYPE);
@@ -65,15 +68,43 @@ public class ContainerImplTest {
     }
 
     @Test
-    public void fieldInjector() throws Exception {
+    public void fieldInjector() {
         FieldCheck fieldCheck = new FieldCheck();
         c.inject(fieldCheck);
-        assertEquals(fieldCheck.getName(), "Lukasz");
+        assertEquals("Lukasz", fieldCheck.getName());
     }
 
     @Test
-    public void methodInjector() throws Exception {
-        c.inject(new MethodCheck());
+    public void methodInjector() {
+        MethodCheck methodCheck = new MethodCheck();
+        c.inject(methodCheck);
+        assertEquals("Lukasz", methodCheck.getName());
+    }
+
+    @Test
+    public void constructorInjector() {
+        ConstructorCheck constructorCheck = c.inject(ConstructorCheck.class);
+        assertEquals("Lukasz", constructorCheck.getName());
+    }
+
+    @Test
+    public void optionalConstructorInjector() {
+        OptionalConstructorCheck constructorCheck = 
c.inject(OptionalConstructorCheck.class);
+        assertNull(constructorCheck.getName());
+    }
+
+    @Test
+    public void requiredOptionalConstructorInjector() {
+        RequiredOptionalConstructorCheck constructorCheck = 
c.inject(RequiredOptionalConstructorCheck.class);
+        assertNotNull(constructorCheck.getExistingName());
+        assertNull(constructorCheck.getNonExitingName());
+    }
+
+    @Test
+    public void optionalRequiredConstructorInjector() {
+        OptionalRequiredConstructorCheck constructorCheck = 
c.inject(OptionalRequiredConstructorCheck.class);
+        assertNull(constructorCheck.getNonExitingName());
+        assertNotNull(constructorCheck.getExistingName());
     }
 
     /**
@@ -92,7 +123,7 @@ public class ContainerImplTest {
      * Inject values into method under SecurityManager
      */
     @Test
-    public void testMethodInjectorWithSecurityEnabled() throws Exception {
+    public void testMethodInjectorWithSecurityEnabled() {
         assumeTrue(SystemUtils.isJavaVersionAtMost(JavaVersion.JAVA_20));
         System.setSecurityManager(new TestSecurityManager());
         assertThrows(DependencyException.class, () -> c.inject(new 
MethodCheck()));
@@ -101,7 +132,7 @@ public class ContainerImplTest {
     }
 
     @Test
-    public void testEarlyInitializable() throws Exception {
+    public void testEarlyInitializable() {
         assertTrue("should being initialized already", 
EarlyInitializableBean.initializedEarly);
 
         EarlyInitializableCheck earlyInitializableCheck = new 
EarlyInitializableCheck();
@@ -148,22 +179,19 @@ public class ContainerImplTest {
 
         final InitializableCheck initializableCheck3 = new 
InitializableCheck();
         final TestScopeStrategy testScopeStrategy = new TestScopeStrategy();
-        Thread thread = new Thread(new Runnable() {
-            @Override
-            public void run() {
-                ContainerBuilder cb2 = new ContainerBuilder();
-                cb2.factory(EarlyInitializable.class, 
EarlyInitializableBean.class, Scope.SINGLETON);
-                cb2.factory(Initializable.class, InitializableBean.class, 
Scope.SINGLETON);
-                cb2.factory(EarlyInitializable.class, 
"prototypeEarlyInitializable", EarlyInitializableBean.class, Scope.PROTOTYPE);
-                cb2.factory(Initializable.class, "prototypeInitializable", 
InitializableBean.class, Scope.PROTOTYPE);
-                cb2.factory(Initializable.class, "requestInitializable", 
InitializableBean.class, Scope.REQUEST);
-                cb2.factory(Initializable.class, "sessionInitializable", 
InitializableBean.class, Scope.SESSION);
-                cb2.factory(Initializable.class, "threadInitializable", 
InitializableBean.class, Scope.THREAD);
-                cb2.factory(Initializable.class, "wizardInitializable", 
InitializableBean.class, Scope.WIZARD);
-                Container c2 = cb2.create(false);
-                c2.setScopeStrategy(testScopeStrategy);
-                c2.inject(initializableCheck3);
-            }
+        Thread thread = new Thread(() -> {
+            ContainerBuilder cb2 = new ContainerBuilder();
+            cb2.factory(EarlyInitializable.class, 
EarlyInitializableBean.class, Scope.SINGLETON);
+            cb2.factory(Initializable.class, InitializableBean.class, 
Scope.SINGLETON);
+            cb2.factory(EarlyInitializable.class, 
"prototypeEarlyInitializable", EarlyInitializableBean.class, Scope.PROTOTYPE);
+            cb2.factory(Initializable.class, "prototypeInitializable", 
InitializableBean.class, Scope.PROTOTYPE);
+            cb2.factory(Initializable.class, "requestInitializable", 
InitializableBean.class, Scope.REQUEST);
+            cb2.factory(Initializable.class, "sessionInitializable", 
InitializableBean.class, Scope.SESSION);
+            cb2.factory(Initializable.class, "threadInitializable", 
InitializableBean.class, Scope.THREAD);
+            cb2.factory(Initializable.class, "wizardInitializable", 
InitializableBean.class, Scope.WIZARD);
+            Container c2 = cb2.create(false);
+            c2.setScopeStrategy(testScopeStrategy);
+            c2.inject(initializableCheck3);
         });
         thread.run();
         thread.join();
@@ -205,6 +233,76 @@ public class ContainerImplTest {
 
     }
 
+    public static class ConstructorCheck {
+        private String name;
+
+        @Inject("constructorCheck.name")
+        public ConstructorCheck(String name) {
+            this.name = name;
+        }
+
+        public String getName() {
+            return name;
+        }
+    }
+
+    public static class OptionalConstructorCheck {
+        private String name;
+
+        @Inject(value = "nonExistingConstant", required = false)
+        public OptionalConstructorCheck(String name) {
+            this.name = name;
+        }
+
+        public String getName() {
+            return name;
+        }
+    }
+
+    public static class RequiredOptionalConstructorCheck {
+        private final String existingName;
+        private final String nonExitingName;
+
+        @Inject(required = false)
+        public RequiredOptionalConstructorCheck(
+                @Inject("constructorCheck.name") String existingName,
+                @Inject(value = "nonExistingConstant", required = false) 
String nonExitingName
+        ) {
+            this.existingName = existingName;
+            this.nonExitingName = nonExitingName;
+        }
+
+        public String getExistingName() {
+            return existingName;
+        }
+
+        public String getNonExitingName() {
+            return nonExitingName;
+        }
+    }
+
+    public static class OptionalRequiredConstructorCheck {
+        private final String existingName;
+        private final String nonExitingName;
+
+        @Inject(required = false)
+        public OptionalRequiredConstructorCheck(
+                @Inject(value = "nonExistingConstant", required = false) 
String nonExitingName,
+                @Inject("constructorCheck.name") String existingName
+        ) {
+            this.existingName = existingName;
+            this.nonExitingName = nonExitingName;
+        }
+
+        public String getExistingName() {
+            return existingName;
+        }
+
+        public String getNonExitingName() {
+            return nonExitingName;
+        }
+    }
+
     class InitializableCheck {
 
         private Initializable initializable;

Reply via email to