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 f6bb102803..a690b3c384 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 @@ ParameterInjector[] getParametersInject 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 + */ + List> getParametersInjectorsWithNulls( + M member, + Annotation[][] annotations, + Class[] parameterTypes, + String defaultName + ) throws MissingDependencyException { + final List> parameterInjectors = new ArrayList<>(); + + final Iterator 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 parameterInjectors; + } + ParameterInjector createParameterInjector(Key key, Member member) throws MissingDependencyException { final InternalFactory factory = getFactory(key); if (factory == null) { @@ -252,6 +287,23 @@ ParameterInjector createParameterInjector(Key key, Member member) thro return new ParameterInjector<>(externalContext, factory); } + ParameterInjector createNullParameterInjector(Key key, Member member) { + final InternalFactory factory = new InternalFactory<>() { + @Override + public T create(InternalContext context) { + return null; + } + + @Override + public Class type() { + return key.getType(); + } + }; + + final ExternalContext externalContext = ExternalContext.newInstance(member, key, this); + return new ParameterInjector<>(externalContext, factory); + } + private ParameterInjector[] toArray(List> parameterInjections) { return parameterInjections.toArray(new ParameterInjector[0]); } @@ -339,7 +391,7 @@ static class ConstructorInjector { MissingDependencyException exception = null; Inject inject = null; - ParameterInjector[] parameters = null; + List> parameters = null; try { inject = constructor.getAnnotation(Inject.class); @@ -347,7 +399,7 @@ static class ConstructorInjector { } catch (MissingDependencyException e) { exception = e; } - parameterInjectors = parameters; + parameterInjectors = parameters != null ? container.toArray(parameters) : null; if (exception != null) { if (inject != null && inject.required()) { @@ -357,11 +409,11 @@ static class ConstructorInjector { injectors = container.injectors.get(implementation); } - ParameterInjector[] constructParameterInjector( + List> constructParameterInjector( Inject inject, ContainerImpl container, Constructor 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 a98c6f1dae..adb6b82769 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 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 void setUp() throws Exception { 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 void setUp() throws Exception { } @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 void testFieldInjectorWithSecurityEnabled() throws Exception { * 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 void testMethodInjectorWithSecurityEnabled() throws Exception { } @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 void testInitializable() throws Exception { 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 String getName() { } + 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;