Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WW-5512 Extends the container to support injecting optional parameters into constructor #1191

Merged
merged 1 commit into from
Jan 26, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading