Skip to content

Commit

Permalink
WW-5512 Extends the container to support injecting optional parameter…
Browse files Browse the repository at this point in the history
…s into constructor
  • Loading branch information
lukaszlenart committed Jan 23, 2025
1 parent b054b51 commit 358f808
Show file tree
Hide file tree
Showing 2 changed files with 176 additions and 26 deletions.
60 changes: 56 additions & 4 deletions core/src/main/java/org/apache/struts2/inject/ContainerImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,41 @@ <M extends AccessibleObject & Member> 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
*/
<M extends AccessibleObject & Member> List<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 parameterInjectors;
}

<T> ParameterInjector<T> createParameterInjector(Key<T> key, Member member) throws MissingDependencyException {
final InternalFactory<? extends T> factory = getFactory(key);
if (factory == null) {
Expand All @@ -252,6 +287,23 @@ <T> ParameterInjector<T> createParameterInjector(Key<T> key, Member member) thro
return new ParameterInjector<>(externalContext, factory);
}

<T> ParameterInjector<T> createNullParameterInjector(Key<T> key, Member member) {
final InternalFactory<? extends T> factory = new InternalFactory<>() {
@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]);
}
Expand Down Expand Up @@ -339,15 +391,15 @@ static class ConstructorInjector<T> {

MissingDependencyException exception = null;
Inject inject = null;
ParameterInjector<?>[] parameters = null;
List<ParameterInjector<?>> parameters = null;

try {
inject = constructor.getAnnotation(Inject.class);
parameters = constructParameterInjector(inject, container, constructor);
} catch (MissingDependencyException e) {
exception = e;
}
parameterInjectors = parameters;
parameterInjectors = parameters != null ? container.toArray(parameters) : null;

if (exception != null) {
if (inject != null && inject.required()) {
Expand All @@ -357,11 +409,11 @@ static class ConstructorInjector<T> {
injectors = container.injectors.get(implementation);
}

ParameterInjector<?>[] constructParameterInjector(
List<ParameterInjector<?>> constructParameterInjector(
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(),
Expand Down
142 changes: 120 additions & 22 deletions core/src/test/java/org/apache/struts2/inject/ContainerImplTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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);
Expand All @@ -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());
}

/**
Expand All @@ -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()));
Expand All @@ -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();
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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;
Expand Down

0 comments on commit 358f808

Please sign in to comment.