Inspect FunctionType to extract collection item type

Resurrects some code from #222 (the test didn't go far enough
when that issue was closed).
This commit is contained in:
Dave Syer
2018-10-26 08:00:56 +01:00
parent c1bce87771
commit a735f50daa
9 changed files with 122 additions and 37 deletions

View File

@@ -14,7 +14,7 @@
<parent> <parent>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId> <artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.0.BUILD-SNAPSHOT</version> <version>2.1.0.RC1</version>
<relativePath /> <!-- lookup parent from repository --> <relativePath /> <!-- lookup parent from repository -->
</parent> </parent>

View File

@@ -13,7 +13,7 @@
<parent> <parent>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId> <artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.0.M4</version> <version>2.1.0.RC1</version>
<relativePath /> <!-- lookup parent from repository --> <relativePath /> <!-- lookup parent from repository -->
</parent> </parent>

View File

@@ -13,7 +13,7 @@
<parent> <parent>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId> <artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.0.M1</version> <version>2.1.0.RC1</version>
<relativePath/> <relativePath/>
</parent> </parent>
@@ -22,7 +22,7 @@
<spring-cloud-function.version>2.0.0.BUILD-SNAPSHOT</spring-cloud-function.version> <spring-cloud-function.version>2.0.0.BUILD-SNAPSHOT</spring-cloud-function.version>
<spring-cloud-stream.version>Fishtown.BUILD-SNAPSHOT</spring-cloud-stream.version> <spring-cloud-stream.version>Fishtown.BUILD-SNAPSHOT</spring-cloud-stream.version>
<reactor.version>3.1.2.RELEASE</reactor.version> <reactor.version>3.1.2.RELEASE</reactor.version>
<wrapper.version>1.0.10.RELEASE</wrapper.version> <wrapper.version>1.0.17.RELEASE</wrapper.version>
</properties> </properties>
<dependencies> <dependencies>

View File

@@ -12,7 +12,7 @@
<parent> <parent>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId> <artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.0.M1</version> <version>2.1.0.RC1</version>
<relativePath /> <relativePath />
</parent> </parent>

View File

@@ -18,6 +18,7 @@ package com.example;
import java.net.URI; import java.net.URI;
import java.util.Arrays; import java.util.Arrays;
import org.junit.Ignore;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
@@ -41,40 +42,38 @@ public class SampleApplicationTests {
@LocalServerPort @LocalServerPort
private int port; private int port;
private TestRestTemplate rest = new TestRestTemplate();
@Test @Test
public void words() { public void words() {
assertThat(new TestRestTemplate() assertThat(rest.getForObject("http://localhost:" + port + "/words", String.class))
.getForObject("http://localhost:" + port + "/words", String.class))
.isEqualTo("[{\"value\":\"foo\"},{\"value\":\"bar\"}]"); .isEqualTo("[{\"value\":\"foo\"},{\"value\":\"bar\"}]");
} }
@Test @Test
public void uppercase() { public void uppercase() {
assertThat(new TestRestTemplate().postForObject( assertThat(rest.postForObject("http://localhost:" + port + "/uppercase",
"http://localhost:" + port + "/uppercase", "[{\"value\":\"foo\"}]", "[{\"value\":\"foo\"}]", String.class))
String.class)).isEqualTo("[{\"value\":\"FOO\"}]"); .isEqualTo("[{\"value\":\"FOO\"}]");
} }
@Test @Test
public void composite() { public void composite() {
assertThat(new TestRestTemplate() assertThat(rest.getForObject("http://localhost:" + port + "/words,uppercase",
.getForObject("http://localhost:" + port + "/words,uppercase", String.class)) String.class)).isEqualTo("[{\"value\":\"FOO\"},{\"value\":\"BAR\"}]");
.isEqualTo("[{\"value\":\"FOO\"},{\"value\":\"BAR\"}]");
} }
@Test @Test
public void single() { public void single() {
assertThat(new TestRestTemplate().postForObject( assertThat(rest.postForObject("http://localhost:" + port + "/uppercase",
"http://localhost:" + port + "/uppercase", "{\"value\":\"foo\"}", "{\"value\":\"foo\"}", String.class)).isEqualTo("{\"value\":\"FOO\"}");
String.class)).isEqualTo("{\"value\":\"FOO\"}");
} }
@Test @Test
public void lowercase() { public void lowercase() {
assertThat(new TestRestTemplate().postForObject( assertThat(rest.postForObject("http://localhost:" + port + "/lowercase",
"http://localhost:" + port + "/lowercase", "[{\"value\":\"Foo\"}]", "[{\"value\":\"Foo\"}]", String.class))
String.class)).isEqualTo("[{\"value\":\"foo\"}]"); .isEqualTo("[{\"value\":\"foo\"}]");
} }
@Test @Test
@@ -85,10 +84,25 @@ public class SampleApplicationTests {
map.put("A", Arrays.asList("1", "2", "3")); map.put("A", Arrays.asList("1", "2", "3"));
map.put("B", Arrays.asList("5", "6")); map.put("B", Arrays.asList("5", "6"));
assertThat(new TestRestTemplate().exchange(RequestEntity.post(new URI("http://localhost:" + port + "/sum")) assertThat(rest.exchange(
.accept(MediaType.APPLICATION_JSON).contentType(MediaType.APPLICATION_FORM_URLENCODED) RequestEntity.post(new URI("http://localhost:" + port + "/sum"))
.body(map), String.class).getBody()) .accept(MediaType.APPLICATION_JSON)
.isEqualTo("[{\"A\":6,\"B\":11}]"); .contentType(MediaType.APPLICATION_FORM_URLENCODED).body(map),
String.class).getBody()).isEqualTo("[{\"A\":6,\"B\":11}]");
} }
@Test
@Ignore
public void multipart() throws Exception {
LinkedMultiValueMap<String, String> map = new LinkedMultiValueMap<>();
map.put("A", Arrays.asList("1", "2", "3"));
map.put("B", Arrays.asList("5", "6"));
assertThat(rest.exchange(
RequestEntity.post(new URI("http://localhost:" + port + "/sum")).accept(MediaType.APPLICATION_JSON)
.contentType(MediaType.MULTIPART_FORM_DATA).body(map),
String.class).getBody()).isEqualTo("[{\"A\":6,\"B\":11}]");
}
} }

View File

@@ -13,14 +13,14 @@
<parent> <parent>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId> <artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.0.M1</version> <version>2.1.0.RC1</version>
<relativePath/> <relativePath/>
</parent> </parent>
<properties> <properties>
<java.version>1.8</java.version> <java.version>1.8</java.version>
<spring-cloud-function.version>2.0.0.BUILD-SNAPSHOT</spring-cloud-function.version> <spring-cloud-function.version>2.0.0.BUILD-SNAPSHOT</spring-cloud-function.version>
<wrapper.version>1.0.13.RELEASE</wrapper.version> <wrapper.version>1.0.17.RELEASE</wrapper.version>
</properties> </properties>
<dependencies> <dependencies>

View File

@@ -60,6 +60,11 @@
<artifactId>spring-boot-configuration-processor</artifactId> <artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional> <optional>true</optional>
</dependency> </dependency>
<dependency>
<groupId>org.synchronoss.cloud</groupId>
<artifactId>nio-multipart-parser</artifactId>
<scope>test</scope>
</dependency>
</dependencies> </dependencies>
<build> <build>

View File

@@ -16,6 +16,8 @@
package org.springframework.cloud.function.web; package org.springframework.cloud.function.web;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; import java.util.Collections;
@@ -97,15 +99,14 @@ public class RequestProcessor {
boolean stream) { boolean stream) {
Object function = wrapper.handler(); Object function = wrapper.handler();
Class<?> inputType = inspector.getInputType(function); Class<?> inputType = inspector.getInputType(function);
Type itemType = getItemType(function);
Object input = null; Object input = null;
if (StringUtils.hasText(body)) { if (StringUtils.hasText(body)) {
if (body.startsWith("[")) { if (body.startsWith("[")) {
input = Collection.class.isAssignableFrom(inputType) input = mapper.toObject(body, ResolvableType
? mapper.toObject(body, inputType) .forClassWithGenerics(ArrayList.class, (Class<?>) itemType)
: mapper.toObject(body, .getType());
ResolvableType.forClassWithGenerics(ArrayList.class,
(Class<?>) inputType).getType());
} }
else { else {
if (inputType == String.class) { if (inputType == String.class) {
@@ -146,8 +147,8 @@ public class RequestProcessor {
form.putAll(params); form.putAll(params);
} }
boolean inputIsCollection = boolean inputIsCollection = Collection.class
Collection.class.isAssignableFrom(inspector.getInputType(wrapper.handler())); .isAssignableFrom(inspector.getInputType(wrapper.handler()));
Flux<?> flux = body == null ? Flux.just(form) Flux<?> flux = body == null ? Flux.just(form)
: inputIsCollection ? Flux.just(body) : Flux.fromIterable(iterable); : inputIsCollection ? Flux.just(body) : Flux.fromIterable(iterable);
if (inspector.isMessage(function)) { if (inspector.isMessage(function)) {
@@ -254,6 +255,42 @@ public class RequestProcessor {
return Mono.from(function.apply(input)); return Mono.from(function.apply(input));
} }
private Object getTargetFunction(Object function) {
// we need to get the actual un-fluxed function so we can interrogate for types
Object target = inspector.getRegistration(function).getTarget();
if (target instanceof FluxWrapper) {
target = ((FluxWrapper<?>) target).getTarget();
}
return target;
}
private Type getItemType(Object function) {
Class<?> inputType = inspector.getInputType(function);
if (!Collection.class.isAssignableFrom(inputType)) {
return inputType;
}
Type type = inspector.getRegistration(this.getTargetFunction(function)).getType()
.getType();
if (type instanceof ParameterizedType) {
type = ((ParameterizedType) type).getActualTypeArguments()[0];
}
else {
for (Type iface : ((Class<?>) type).getGenericInterfaces()) {
if (iface.getTypeName().startsWith("java.util.function")) {
type = ((ParameterizedType) iface).getActualTypeArguments()[0];
break;
}
}
}
if (type instanceof ParameterizedType) {
type = ((ParameterizedType) type).getActualTypeArguments()[0];
}
else {
type = inputType;
}
return type;
}
public static class FunctionWrapper { public static class FunctionWrapper {
private final Function<Publisher<?>, Publisher<?>> function; private final Function<Publisher<?>, Publisher<?>> function;

View File

@@ -18,6 +18,7 @@ package org.springframework.cloud.function.test;
import java.util.List; import java.util.List;
import java.util.function.Function; import java.util.function.Function;
import java.util.stream.Collectors;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
@@ -45,15 +46,43 @@ public class FunctionalWithInputCollectionTests {
@Test @Test
public void words() throws Exception { public void words() throws Exception {
client.post().uri("/").body(Mono.just("[\"foo\", \"bar\"]"), String.class).exchange() client.post().uri("/").body(Mono.just("[{\"value\":\"foo\"}, {\"value\":\"bar\"}]"), String.class)
.expectStatus().isOk().expectBody(String.class).isEqualTo("[FOO, BAR]"); .exchange().expectStatus().isOk().expectBody(String.class)
.isEqualTo("{\"value\":\"FOOBAR\"}");
} }
@SpringBootConfiguration @SpringBootConfiguration
protected static class TestConfiguration implements Function<List<String>, String> { protected static class TestConfiguration implements Function<List<Foo>, Foo> {
@Override @Override
public String apply(List<String> value) { public Foo apply(List<Foo> value) {
return value.toString().toUpperCase(); return new Foo(value.stream().map(foo -> foo.getValue().toUpperCase())
.collect(Collectors.joining()));
} }
} }
public static class Foo {
private String value;
public Foo() {
}
public Foo(String value) {
this.value = value;
}
public String getValue() {
return this.value;
}
public void setValue(String value) {
this.value = value;
}
@Override
public String toString() {
return "Foo [value=" + this.value + "]";
}
}
} }