Skip to content

Commit

Permalink
Use generic type when binding constructor parameters
Browse files Browse the repository at this point in the history
Fixes gh-19156
  • Loading branch information
mbhave committed Dec 3, 2019
1 parent 2e763be commit f4db8c8
Show file tree
Hide file tree
Showing 3 changed files with 70 additions and 17 deletions.
Expand Up @@ -31,6 +31,7 @@
import org.springframework.boot.context.properties.source.ConfigurationPropertyName;
import org.springframework.core.DefaultParameterNameDiscoverer;
import org.springframework.core.KotlinDetector;
import org.springframework.core.MethodParameter;
import org.springframework.core.ParameterNameDiscoverer;
import org.springframework.core.ResolvableType;
import org.springframework.util.Assert;
Expand Down Expand Up @@ -118,9 +119,9 @@ static <T> ValueObject<T> get(Bindable<T> bindable, BindConstructorProvider cons
return null;
}
if (KotlinDetector.isKotlinType(type)) {
return KotlinValueObject.get((Constructor<T>) bindConstructor);
return KotlinValueObject.get((Constructor<T>) bindConstructor, bindable.getType());
}
return DefaultValueObject.get(bindConstructor);
return DefaultValueObject.get(bindConstructor, bindable.getType());
}

}
Expand All @@ -132,19 +133,22 @@ private static final class KotlinValueObject<T> extends ValueObject<T> {

private final List<ConstructorParameter> constructorParameters;

private KotlinValueObject(Constructor<T> primaryConstructor, KFunction<T> kotlinConstructor) {
private KotlinValueObject(Constructor<T> primaryConstructor, KFunction<T> kotlinConstructor,
ResolvableType type) {
super(primaryConstructor);
this.constructorParameters = parseConstructorParameters(kotlinConstructor);
this.constructorParameters = parseConstructorParameters(kotlinConstructor, type);
}

private List<ConstructorParameter> parseConstructorParameters(KFunction<T> kotlinConstructor) {
private List<ConstructorParameter> parseConstructorParameters(KFunction<T> kotlinConstructor,
ResolvableType type) {
List<KParameter> parameters = kotlinConstructor.getParameters();
List<ConstructorParameter> result = new ArrayList<>(parameters.size());
for (KParameter parameter : parameters) {
String name = parameter.getName();
ResolvableType type = ResolvableType.forType(ReflectJvmMapping.getJavaType(parameter.getType()));
ResolvableType parameterType = ResolvableType
.forType(ReflectJvmMapping.getJavaType(parameter.getType()), type);
Annotation[] annotations = parameter.getAnnotations().toArray(new Annotation[0]);
result.add(new ConstructorParameter(name, type, annotations));
result.add(new ConstructorParameter(name, parameterType, annotations));
}
return Collections.unmodifiableList(result);
}
Expand All @@ -154,12 +158,12 @@ List<ConstructorParameter> getConstructorParameters() {
return this.constructorParameters;
}

static <T> ValueObject<T> get(Constructor<T> bindConstructor) {
static <T> ValueObject<T> get(Constructor<T> bindConstructor, ResolvableType type) {
KFunction<T> kotlinConstructor = ReflectJvmMapping.getKotlinFunction(bindConstructor);
if (kotlinConstructor != null) {
return new KotlinValueObject<>(bindConstructor, kotlinConstructor);
return new KotlinValueObject<>(bindConstructor, kotlinConstructor, type);
}
return DefaultValueObject.get(bindConstructor);
return DefaultValueObject.get(bindConstructor, type);
}

}
Expand All @@ -174,21 +178,23 @@ private static final class DefaultValueObject<T> extends ValueObject<T> {

private final List<ConstructorParameter> constructorParameters;

private DefaultValueObject(Constructor<T> constructor) {
private DefaultValueObject(Constructor<T> constructor, ResolvableType type) {
super(constructor);
this.constructorParameters = parseConstructorParameters(constructor);
this.constructorParameters = parseConstructorParameters(constructor, type);
}

private static List<ConstructorParameter> parseConstructorParameters(Constructor<?> constructor) {
private static List<ConstructorParameter> parseConstructorParameters(Constructor<?> constructor,
ResolvableType type) {
String[] names = PARAMETER_NAME_DISCOVERER.getParameterNames(constructor);
Assert.state(names != null, () -> "Failed to extract parameter names for " + constructor);
Parameter[] parameters = constructor.getParameters();
List<ConstructorParameter> result = new ArrayList<>(parameters.length);
for (int i = 0; i < parameters.length; i++) {
String name = names[i];
ResolvableType type = ResolvableType.forConstructorParameter(constructor, i);
ResolvableType parameterType = ResolvableType.forMethodParameter(new MethodParameter(constructor, i),
type);
Annotation[] annotations = parameters[i].getDeclaredAnnotations();
result.add(new ConstructorParameter(name, type, annotations));
result.add(new ConstructorParameter(name, parameterType, annotations));
}
return Collections.unmodifiableList(result);
}
Expand All @@ -199,8 +205,8 @@ List<ConstructorParameter> getConstructorParameters() {
}

@SuppressWarnings("unchecked")
static <T> ValueObject<T> get(Constructor<?> bindConstructor) {
return new DefaultValueObject<>((Constructor<T>) bindConstructor);
static <T> ValueObject<T> get(Constructor<?> bindConstructor, ResolvableType type) {
return new DefaultValueObject<>((Constructor<T>) bindConstructor, type);
}

}
Expand Down
Expand Up @@ -19,13 +19,15 @@
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;

import org.junit.jupiter.api.Test;

import org.springframework.boot.context.properties.source.ConfigurationPropertyName;
import org.springframework.boot.context.properties.source.ConfigurationPropertySource;
import org.springframework.boot.context.properties.source.MockConfigurationPropertySource;
import org.springframework.core.ResolvableType;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.util.Assert;

Expand Down Expand Up @@ -233,6 +235,19 @@ void bindWhenAllPropertiesBoundShouldClearConfigurationProperty() { // gh-18704
.satisfies(this::noConfigurationProperty);
}

@Test
void bindToClassShouldBindWithGenerics() {
// gh-19156
ResolvableType type = ResolvableType.forClassWithGenerics(Map.class, String.class, String.class);
MockConfigurationPropertySource source = new MockConfigurationPropertySource();
source.put("foo.value.bar", "baz");
this.sources.add(source);
GenericValue<Map<String, String>> bean = this.binder.bind("foo", Bindable
.<GenericValue<Map<String, String>>>of(ResolvableType.forClassWithGenerics(GenericValue.class, type)))
.get();
assertThat(bean.getValue().get("bar")).isEqualTo("baz");
}

private void noConfigurationProperty(BindException ex) {
assertThat(ex.getProperty()).isNull();
}
Expand Down Expand Up @@ -452,4 +467,18 @@ String getBar() {

}

static class GenericValue<T> {

private final T value;

GenericValue(T value) {
this.value = value;
}

T getValue() {
return this.value;
}

}

}
Expand Up @@ -4,6 +4,7 @@ import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
import org.springframework.boot.context.properties.source.ConfigurationPropertyName
import org.springframework.boot.context.properties.source.MockConfigurationPropertySource
import org.springframework.core.ResolvableType

/**
* Tests for `ConstructorParametersBinder`.
Expand Down Expand Up @@ -173,6 +174,19 @@ class KotlinConstructorParametersBinderTests {
assertThat(bean.enumValue).isEqualTo(ExampleEnum.FOO_BAR)
}

@Test
fun `Bind to data class with generics`() {
val source = MockConfigurationPropertySource()
source.put("foo.value.bar", "baz")
val binder = Binder(source)
val type = ResolvableType.forClassWithGenerics(Map::class.java, String::class.java,
String::class.java)
val bean = binder.bind("foo", Bindable
.of<GenericValue<Map<String, String>>>(ResolvableType.forClassWithGenerics(GenericValue::class.java, type)))
.get()
assertThat(bean.value.get("bar")).isEqualTo("baz");
}

class ExampleValueBean(val intValue: Int?, val longValue: Long?,
val booleanValue: Boolean?, val stringValue: String?,
val enumValue: ExampleEnum?)
Expand Down Expand Up @@ -214,4 +228,8 @@ class KotlinConstructorParametersBinderTests {
val stringValue: String = "my data",
val enumValue: ExampleEnum = ExampleEnum.BAR_BAZ)

data class GenericValue<T>(
val value: T
)

}

0 comments on commit f4db8c8

Please sign in to comment.