diff --git a/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/json/GsonMapper.java b/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/json/GsonMapper.java index 988742094..c46a5df82 100644 --- a/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/json/GsonMapper.java +++ b/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/json/GsonMapper.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2017 the original author or authors. + * Copyright 2016-2018 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,16 +15,13 @@ */ package org.springframework.cloud.function.json; -import java.util.ArrayList; -import java.util.List; +import java.lang.reflect.Type; import com.google.gson.Gson; -import org.springframework.cloud.function.json.JsonMapper; -import org.springframework.core.ResolvableType; - /** * @author Dave Syer + * @author Oleg Zhurakousky * */ public class GsonMapper implements JsonMapper { @@ -36,13 +33,7 @@ public class GsonMapper implements JsonMapper { } @Override - public List toList(String json, Class type) { - return gson.fromJson(json, - ResolvableType.forClassWithGenerics(ArrayList.class, type).getType()); - } - - @Override - public T toSingle(String json, Class type) { + public T toObject(String json, Type type) { return gson.fromJson(json, type); } diff --git a/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/json/JacksonMapper.java b/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/json/JacksonMapper.java index 476b99719..4bcff9987 100644 --- a/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/json/JacksonMapper.java +++ b/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/json/JacksonMapper.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2017 the original author or authors. + * Copyright 2016-2018 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,17 +15,15 @@ */ package org.springframework.cloud.function.json; -import java.util.ArrayList; -import java.util.List; +import java.lang.reflect.Type; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; - -import org.springframework.cloud.function.json.JsonMapper; +import com.fasterxml.jackson.databind.type.TypeFactory; /** * @author Dave Syer - * + * @author Oleg Zhurakousky */ public class JacksonMapper implements JsonMapper { @@ -36,23 +34,12 @@ public class JacksonMapper implements JsonMapper { } @Override - public List toList(String json, Class type) { + public T toObject(String json, Type type) { try { - return mapper.readValue(json, mapper.getTypeFactory() - .constructCollectionLikeType(ArrayList.class, type)); + return mapper.readValue(json, TypeFactory.defaultInstance().constructType(type)); } catch (Exception e) { - throw new IllegalArgumentException("Cannot convert JSON", e); - } - } - - @Override - public T toSingle(String json, Class type) { - try { - return mapper.readValue(json, type); - } - catch (Exception e) { - throw new IllegalArgumentException("Cannot convert JSON", e); + throw new IllegalArgumentException("Cannot convert JSON " + json, e); } } diff --git a/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/json/JsonMapper.java b/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/json/JsonMapper.java index 41b465e7c..db15eaf40 100644 --- a/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/json/JsonMapper.java +++ b/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/json/JsonMapper.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2017 the original author or authors. + * Copyright 2016-2018 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,17 +15,41 @@ */ package org.springframework.cloud.function.json; +import java.lang.reflect.Type; +import java.util.ArrayList; import java.util.List; +import org.springframework.core.ResolvableType; + /** * @author Dave Syer - * + * @author Oleg Zhurakousky */ public interface JsonMapper { - List toList(String json, Class type); + /** + * @deprecated in favor of {@link #toObject(String, Type)} + */ + @Deprecated + default List toList(String json, Class type) { + Type actualType = (json.startsWith("[") && !List.class.isAssignableFrom(type)) + ? ResolvableType.forClassWithGenerics(ArrayList.class, (Class) type) + .getType() + : type; + return toObject(json, actualType); + } - T toSingle(String json, Class type); + /** + * @since 2.0 + */ + T toObject(String json, Type type); + + /** + * @deprecated in favor of {@link #toObject(String, Type)} + */ + default T toSingle(String json, Class type) { + return toObject(json, type); + } String toString(Object value); diff --git a/spring-cloud-function-context/src/test/java/org/springframework/cloud/function/util/JsonMapperTests.java b/spring-cloud-function-context/src/test/java/org/springframework/cloud/function/util/JsonMapperTests.java index 64a7bba6e..bfd83bbdb 100644 --- a/spring-cloud-function-context/src/test/java/org/springframework/cloud/function/util/JsonMapperTests.java +++ b/spring-cloud-function-context/src/test/java/org/springframework/cloud/function/util/JsonMapperTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2017 the original author or authors. + * Copyright 2016-2018 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -29,11 +29,13 @@ import org.junit.runners.Parameterized.Parameters; import org.springframework.cloud.function.json.GsonMapper; import org.springframework.cloud.function.json.JacksonMapper; import org.springframework.cloud.function.json.JsonMapper; +import org.springframework.core.ResolvableType; import static org.assertj.core.api.Assertions.assertThat; /** * @author Dave Syer + * @author Oleg Zhurakousky * */ @RunWith(Parameterized.class) @@ -54,8 +56,8 @@ public class JsonMapperTests { @Test public void vanillaArray() { String json = "[{\"value\":\"foo\"},{\"value\":\"foo\"}]"; - List list = mapper.toList(json, - Foo.class); + List list = mapper.toObject(json, + ResolvableType.forClassWithGenerics(List.class, Foo.class).getType()); assertThat(list).hasSize(2); assertThat(list.get(0).getValue()).isEqualTo("foo"); assertThat(mapper.toString(list)).isEqualTo(json); @@ -63,34 +65,36 @@ public class JsonMapperTests { @Test public void intArray() { - List list = mapper.toList("[123,456]", Integer.class); + List list = mapper.toObject("[123,456]", + ResolvableType.forClassWithGenerics(List.class, Integer.class).getType()); assertThat(list).hasSize(2); assertThat(list.get(0)).isEqualTo(123); } @Test public void emptyArray() { - List list = mapper.toList("[]", Foo.class); + List list = mapper.toObject("[]", + ResolvableType.forClassWithGenerics(List.class, Foo.class).getType()); assertThat(list).hasSize(0); } @Test public void vanillaObject() { String json = "{\"value\":\"foo\"}"; - Foo foo = mapper.toSingle(json, Foo.class); + Foo foo = mapper.toObject(json, Foo.class); assertThat(foo.getValue()).isEqualTo("foo"); assertThat(mapper.toString(foo)).isEqualTo(json); } @Test public void intValue() { - int foo = mapper.toSingle("123", Integer.class); + int foo = mapper.toObject("123", Integer.class); assertThat(foo).isEqualTo(123); } @Test public void empty() { - Foo foo = mapper.toSingle("{}", Foo.class); + Foo foo = mapper.toObject("{}", Foo.class); assertThat(foo.getValue()).isNull(); } diff --git a/spring-cloud-function-web/src/main/java/org/springframework/cloud/function/web/RequestProcessor.java b/spring-cloud-function-web/src/main/java/org/springframework/cloud/function/web/RequestProcessor.java index 58a34fc1e..edb5a89f9 100644 --- a/spring-cloud-function-web/src/main/java/org/springframework/cloud/function/web/RequestProcessor.java +++ b/spring-cloud-function-web/src/main/java/org/springframework/cloud/function/web/RequestProcessor.java @@ -16,6 +16,7 @@ package org.springframework.cloud.function.web; +import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.List; @@ -36,6 +37,7 @@ import org.springframework.cloud.function.context.message.MessageUtils; import org.springframework.cloud.function.core.FluxWrapper; import org.springframework.cloud.function.json.JsonMapper; import org.springframework.cloud.function.web.util.HeaderUtils; +import org.springframework.core.ResolvableType; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -72,12 +74,15 @@ public class RequestProcessor { this.converter = converter; } - public static FunctionWrapper wrapper(Function, ? extends Publisher> function, - Consumer> consumer, Supplier> supplier) { + public static FunctionWrapper wrapper( + Function, ? extends Publisher> function, + Consumer> consumer, + Supplier> supplier) { return new FunctionWrapper(function, consumer, supplier); } - public static FunctionWrapper wrapper(Function, ? extends Publisher> function) { + public static FunctionWrapper wrapper( + Function, ? extends Publisher> function) { return new FunctionWrapper(function, null, null); } @@ -94,21 +99,24 @@ public class RequestProcessor { public Mono> post(FunctionWrapper wrapper, String body, boolean stream) { - Mono> responseEntityMono; Object function = wrapper.handler(); + Class inputType = inspector.getInputType(function); Object input = null; if (StringUtils.hasText(body)) { - Class inputType = inspector.getInputType(function); if (body.startsWith("[")) { - input = mapper.toList(body, inputType); + input = Collection.class.isAssignableFrom(inputType) + ? mapper.toObject(body, inputType) + : mapper.toObject(body, ResolvableType + .forClassWithGenerics(ArrayList.class, (Class) inputType) + .getType()); } else { if (inputType == String.class) { input = body; } else if (body.startsWith("{")) { - input = mapper.toSingle(body, inputType); + input = mapper.toObject(body, inputType); } else if (body.startsWith("\"")) { input = body.substring(1, body.length() - 2); @@ -118,8 +126,8 @@ public class RequestProcessor { } } } - responseEntityMono = post(wrapper, input, null, stream); - return responseEntityMono; + return post(wrapper, input, null, stream, + !Collection.class.isAssignableFrom(inputType)); } public Mono> stream(FunctionWrapper request) { @@ -130,7 +138,8 @@ public class RequestProcessor { } private Mono> post(FunctionWrapper wrapper, Object body, - MultiValueMap params, boolean stream) { + MultiValueMap params, boolean stream, + boolean shouldFluxAsIterable) { Iterable iterable = body instanceof Collection ? (List) body : Collections.singletonList(body); @@ -143,7 +152,8 @@ public class RequestProcessor { form.putAll(params); } - Flux flux = body == null ? Flux.just(form) : Flux.fromIterable(iterable); + Flux flux = body == null ? Flux.just(form) + : shouldFluxAsIterable ? Flux.fromIterable(iterable) : Flux.just(body); if (inspector.isMessage(function)) { flux = messages(wrapper, function == null ? consumer : function, flux); } @@ -239,7 +249,8 @@ public class RequestProcessor { } } - private Publisher value(Function, Publisher> function, Publisher value) { + private Publisher value(Function, Publisher> function, + Publisher value) { Flux input = Flux.from(value).map(body -> converter.convert(function, body)); return Mono.from(function.apply(input)); } @@ -259,8 +270,10 @@ public class RequestProcessor { private Publisher argument; @SuppressWarnings("unchecked") - public FunctionWrapper(Function, ? extends Publisher> function, - Consumer> consumer, Supplier> supplier) { + public FunctionWrapper( + Function, ? extends Publisher> function, + Consumer> consumer, + Supplier> supplier) { this.function = (Function, Publisher>) function; this.consumer = (Consumer>) consumer; this.supplier = (Supplier>) supplier; diff --git a/spring-cloud-function-web/src/test/java/org/springframework/cloud/function/test/FunctionalWithInputCollectionTests.java b/spring-cloud-function-web/src/test/java/org/springframework/cloud/function/test/FunctionalWithInputCollectionTests.java new file mode 100644 index 000000000..3cf0eddf4 --- /dev/null +++ b/spring-cloud-function-web/src/test/java/org/springframework/cloud/function/test/FunctionalWithInputCollectionTests.java @@ -0,0 +1,59 @@ +/* + * Copyright 2012-2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.function.test; + +import java.util.List; +import java.util.function.Function; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.SpringBootConfiguration; +import org.springframework.boot.test.autoconfigure.web.reactive.AutoConfigureWebTestClient; +import org.springframework.cloud.function.context.test.FunctionalSpringBootTest; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.web.reactive.server.WebTestClient; + +import reactor.core.publisher.Mono; + +/** + * @author Dave Syer + * + */ +@RunWith(SpringRunner.class) +@FunctionalSpringBootTest("spring.main.web-application-type=reactive") +@AutoConfigureWebTestClient +public class FunctionalWithInputCollectionTests { + + @Autowired + private WebTestClient client; + + @Test + public void words() throws Exception { + client.post().uri("/").body(Mono.just("[\"foo\", \"bar\"]"), String.class).exchange() + .expectStatus().isOk().expectBody(String.class).isEqualTo("[FOO, BAR]"); + } + + @SpringBootConfiguration + protected static class TestConfiguration implements Function, String> { + @Override + public String apply(List value) { + return value.toString().toUpperCase(); + } + } +}