diff --git a/spring-cloud-function-samples/function-sample-pojo/src/main/java/com/example/SampleApplication.java b/spring-cloud-function-samples/function-sample-pojo/src/main/java/com/example/SampleApplication.java index 73fc0e004..2cf66d468 100644 --- a/spring-cloud-function-samples/function-sample-pojo/src/main/java/com/example/SampleApplication.java +++ b/spring-cloud-function-samples/function-sample-pojo/src/main/java/com/example/SampleApplication.java @@ -1,29 +1,31 @@ /* * Copyright 2013-2016 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. -*/ + * + * 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 com.example; -import java.util.function.Function; -import java.util.function.Supplier; - import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.Bean; - +import org.springframework.util.MultiValueMap; import reactor.core.publisher.Flux; +import java.util.HashMap; +import java.util.Map; +import java.util.function.Function; +import java.util.function.Supplier; + @SpringBootApplication public class SampleApplication { @@ -32,13 +34,25 @@ public class SampleApplication { return value -> new Bar(value.uppercase()); } - @Bean - public Supplier> words() { - return () -> Flux.fromArray(new Foo[] { new Foo("foo"), new Foo("bar") }).log(); + public static void main(String[] args) { + SpringApplication.run(SampleApplication.class, args); } - public static void main(String[] args) throws Exception { - SpringApplication.run(SampleApplication.class, args); + @Bean + public Function, Map> sum() { + return multiValueMap -> { + + Map result = new HashMap<>(); + + multiValueMap.forEach((s, strings) -> result.put(s, strings.stream().mapToInt(Integer::parseInt).sum())); + + return result; + }; + } + + @Bean + public Supplier> words() { + return () -> Flux.fromArray(new Foo[]{new Foo("foo"), new Foo("bar")}).log(); } } diff --git a/spring-cloud-function-samples/function-sample-pojo/src/test/java/com/example/SampleApplicationTests.java b/spring-cloud-function-samples/function-sample-pojo/src/test/java/com/example/SampleApplicationTests.java index 86153f540..8d8664c0a 100644 --- a/spring-cloud-function-samples/function-sample-pojo/src/test/java/com/example/SampleApplicationTests.java +++ b/spring-cloud-function-samples/function-sample-pojo/src/test/java/com/example/SampleApplicationTests.java @@ -17,18 +17,22 @@ package com.example; import org.junit.Test; import org.junit.runner.RunWith; - import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; import org.springframework.boot.test.web.client.TestRestTemplate; import org.springframework.boot.web.server.LocalServerPort; +import org.springframework.http.MediaType; +import org.springframework.http.RequestEntity; import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.util.LinkedMultiValueMap; + +import java.net.URI; +import java.util.Arrays; import static org.assertj.core.api.Assertions.assertThat; /** * @author Dave Syer - * */ @RunWith(SpringRunner.class) @SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) @@ -41,7 +45,7 @@ public class SampleApplicationTests { public void words() { assertThat(new TestRestTemplate() .getForObject("http://localhost:" + port + "/words", String.class)) - .isEqualTo("[{\"value\":\"foo\"},{\"value\":\"bar\"}]"); + .isEqualTo("[{\"value\":\"foo\"},{\"value\":\"bar\"}]"); } @Test @@ -55,7 +59,7 @@ public class SampleApplicationTests { public void composite() { assertThat(new TestRestTemplate() .getForObject("http://localhost:" + port + "/words,uppercase", String.class)) - .isEqualTo("[{\"value\":\"FOO\"},{\"value\":\"BAR\"}]"); + .isEqualTo("[{\"value\":\"FOO\"},{\"value\":\"BAR\"}]"); } @Test @@ -72,4 +76,18 @@ public class SampleApplicationTests { String.class)).isEqualTo("[{\"value\":\"foo\"}]"); } + @Test + public void sum() throws Exception { + + LinkedMultiValueMap map = new LinkedMultiValueMap<>(); + + map.put("A", Arrays.asList("1", "2", "3")); + map.put("B", Arrays.asList("5", "6")); + + assertThat(new TestRestTemplate().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}]"); + } + } diff --git a/spring-cloud-function-web/src/main/java/org/springframework/cloud/function/web/flux/FunctionController.java b/spring-cloud-function-web/src/main/java/org/springframework/cloud/function/web/flux/FunctionController.java index 108f557eb..f495c649c 100644 --- a/spring-cloud-function-web/src/main/java/org/springframework/cloud/function/web/flux/FunctionController.java +++ b/spring-cloud-function-web/src/main/java/org/springframework/cloud/function/web/flux/FunctionController.java @@ -16,19 +16,13 @@ package org.springframework.cloud.function.web.flux; -import java.util.Optional; -import java.util.function.Consumer; -import java.util.function.Function; -import java.util.function.Supplier; -import java.util.stream.Stream; - import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.reactivestreams.Publisher; - import org.springframework.cloud.function.context.catalog.FunctionInspector; import org.springframework.cloud.function.context.message.MessageUtils; import org.springframework.cloud.function.web.flux.constants.WebRequestConstants; +import org.springframework.cloud.function.web.flux.request.FluxFormRequest; import org.springframework.cloud.function.web.flux.request.FluxRequest; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -38,10 +32,15 @@ import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.context.request.WebRequest; - import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +import java.util.Optional; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Supplier; +import java.util.stream.Stream; + /** * @author Dave Syer * @author Mark Fisher @@ -68,18 +67,21 @@ public class FunctionController { @PostMapping(path = "/**") @ResponseBody - public ResponseEntity> post(WebRequest request, - @RequestBody FluxRequest body) { + public ResponseEntity> post(WebRequest request, @RequestBody FluxRequest body) { + @SuppressWarnings("unchecked") Function, Flux> function = (Function, Flux>) request .getAttribute(WebRequestConstants.FUNCTION, WebRequest.SCOPE_REQUEST); @SuppressWarnings("unchecked") Consumer> consumer = (Consumer>) request .getAttribute(WebRequestConstants.CONSUMER, WebRequest.SCOPE_REQUEST); - Boolean single = (Boolean) request.getAttribute(WebRequestConstants.INPUT_SINGLE, - WebRequest.SCOPE_REQUEST); + Boolean single = (Boolean) request + .getAttribute(WebRequestConstants.INPUT_SINGLE, WebRequest.SCOPE_REQUEST); + + FluxFormRequest form = FluxFormRequest.from(request.getParameterMap()); + if (function != null) { - Flux flux = body.flux(); + Flux flux = body.body() == null ? form.flux() : body.flux(); if (debug) { flux = flux.log(); } @@ -90,11 +92,11 @@ public class FunctionController { if (logger.isDebugEnabled()) { logger.debug("Handled POST with function"); } - return ResponseEntity.ok().body( - debug ? result.log() : response(request, function, single, result)); + return ResponseEntity.ok().body(debug ? result.log() : response(request, function, single, result)); } + if (consumer != null) { - Flux flux = body.flux().cache(); // send a copy back to the caller + Flux flux = body.body() == null ? form.flux().cache() : body.flux().cache(); // send a copy back to the caller if (debug) { flux = flux.log(); } @@ -104,18 +106,19 @@ public class FunctionController { } return ResponseEntity.status(HttpStatus.ACCEPTED).body(flux); } + throw new IllegalArgumentException("no such function"); } - private Publisher response(WebRequest request, Object handler, Boolean single, - Flux result) { + private Publisher response(WebRequest request, Object handler, Boolean single, Flux result) { + if (single != null && single && isOutputSingle(handler)) { - request.setAttribute(WebRequestConstants.OUTPUT_SINGLE, true, - WebRequest.SCOPE_REQUEST); + request.setAttribute(WebRequestConstants.OUTPUT_SINGLE, true, WebRequest.SCOPE_REQUEST); return Mono.from(result); } - request.setAttribute(WebRequestConstants.OUTPUT_SINGLE, false, - WebRequest.SCOPE_REQUEST); + + request.setAttribute(WebRequestConstants.OUTPUT_SINGLE, false, WebRequest.SCOPE_REQUEST); + return result; } @@ -128,10 +131,7 @@ public class FunctionController { if (wrapper == type) { return true; } - if (Mono.class.equals(wrapper) || Optional.class.equals(wrapper)) { - return true; - } - return false; + return Mono.class.equals(wrapper) || Optional.class.equals(wrapper); } @GetMapping(path = "/**") @@ -143,8 +143,9 @@ public class FunctionController { @SuppressWarnings("unchecked") Supplier> supplier = (Supplier>) request .getAttribute(WebRequestConstants.SUPPLIER, WebRequest.SCOPE_REQUEST); - String argument = (String) request.getAttribute(WebRequestConstants.ARGUMENT, - WebRequest.SCOPE_REQUEST); + String argument = (String) request + .getAttribute(WebRequestConstants.ARGUMENT, WebRequest.SCOPE_REQUEST); + if (function != null) { return value(function, argument); } diff --git a/spring-cloud-function-web/src/main/java/org/springframework/cloud/function/web/flux/constants/WebRequestConstants.java b/spring-cloud-function-web/src/main/java/org/springframework/cloud/function/web/flux/constants/WebRequestConstants.java index a8fcdd1f1..4d636133a 100644 --- a/spring-cloud-function-web/src/main/java/org/springframework/cloud/function/web/flux/constants/WebRequestConstants.java +++ b/spring-cloud-function-web/src/main/java/org/springframework/cloud/function/web/flux/constants/WebRequestConstants.java @@ -18,9 +18,8 @@ package org.springframework.cloud.function.web.flux.constants; /** * Common storage for web request attribute names (in a separate package to avoid cycles). - * - * @author Dave Syer * + * @author Dave Syer */ public abstract class WebRequestConstants { @@ -32,7 +31,8 @@ public abstract class WebRequestConstants { + ".supplier"; public static final String ARGUMENT = WebRequestConstants.class.getName() + ".argument"; - public static final String HANDLER = WebRequestConstants.class.getName() + ".handler"; + public static final String HANDLER = WebRequestConstants.class.getName() + + ".handler"; public static final String INPUT_SINGLE = WebRequestConstants.class.getName() + ".input_single"; public static final String OUTPUT_SINGLE = WebRequestConstants.class.getName() diff --git a/spring-cloud-function-web/src/main/java/org/springframework/cloud/function/web/flux/request/FluxFormRequest.java b/spring-cloud-function-web/src/main/java/org/springframework/cloud/function/web/flux/request/FluxFormRequest.java new file mode 100644 index 000000000..daa7cb6ef --- /dev/null +++ b/spring-cloud-function-web/src/main/java/org/springframework/cloud/function/web/flux/request/FluxFormRequest.java @@ -0,0 +1,41 @@ +package org.springframework.cloud.function.web.flux.request; + +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import reactor.core.publisher.Flux; + +import java.util.Arrays; +import java.util.Map; + +public class FluxFormRequest { + + private Map map; + + public FluxFormRequest(Map map) { + this.map = map; + } + + public static FluxFormRequest from(Map map) { + return new FluxFormRequest<>(map); + } + + public Flux> flux() { + return Flux.just(buildMap()); + } + + public MultiValueMap body() { + return buildMap(); + } + + private MultiValueMap buildMap() { + + if (map == null) + return null; + + MultiValueMap result = new LinkedMultiValueMap<>(); + map.forEach((key, values) -> result.put(key, Arrays.asList(values))); + return result; + + } + +} diff --git a/spring-cloud-function-web/src/test/java/org/springframework/cloud/function/web/RestApplicationTests.java b/spring-cloud-function-web/src/test/java/org/springframework/cloud/function/web/RestApplicationTests.java index cba4952b0..524a0fa6d 100644 --- a/spring-cloud-function-web/src/test/java/org/springframework/cloud/function/web/RestApplicationTests.java +++ b/spring-cloud-function-web/src/test/java/org/springframework/cloud/function/web/RestApplicationTests.java @@ -15,23 +15,10 @@ */ package org.springframework.cloud.function.web; -import java.net.URI; -import java.time.Duration; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.function.Consumer; -import java.util.function.Function; -import java.util.function.Supplier; - import org.junit.Before; import org.junit.Ignore; import org.junit.Test; import org.junit.runner.RunWith; - import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; @@ -48,15 +35,23 @@ import org.springframework.http.ResponseEntity; import org.springframework.messaging.Message; import org.springframework.messaging.support.MessageBuilder; import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; import org.springframework.util.StringUtils; +import reactor.core.publisher.Flux; + +import java.net.URI; +import java.time.Duration; +import java.util.*; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Supplier; +import java.util.stream.Collectors; import static org.assertj.core.api.Assertions.assertThat; -import reactor.core.publisher.Flux; - /** * @author Dave Syer - * */ @RunWith(SpringRunner.class) @SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) @@ -76,7 +71,7 @@ public class RestApplicationTests { } @Test - public void staticResource() throws Exception { + public void staticResource() { assertThat(rest.getForObject("/test.html", String.class)).contains("Test"); } @@ -224,7 +219,7 @@ public class RestApplicationTests { assertThat(rest.exchange( RequestEntity.get(new URI("/sentences")).accept(MediaType.ALL).build(), String.class).getBody()) - .isEqualTo("[[\"go\",\"home\"],[\"come\",\"back\"]]"); + .isEqualTo("[[\"go\",\"home\"],[\"come\",\"back\"]]"); } @Test @@ -415,7 +410,7 @@ public class RestApplicationTests { // The new line in the middle is optional .body("[{\"value\":\"foo\"},\n{\"value\":\"bar\"}]"), String.class).getBody()) - .isEqualTo("[{\"value\":\"FOO\"},{\"value\":\"BAR\"}]"); + .isEqualTo("[{\"value\":\"FOO\"},{\"value\":\"BAR\"}]"); } @Test @@ -423,7 +418,21 @@ public class RestApplicationTests { assertThat(rest.exchange(RequestEntity.post(new URI("/uppercase")) .accept(EVENT_STREAM).contentType(MediaType.APPLICATION_JSON) .body("[\"foo\",\"bar\"]"), String.class).getBody()) - .isEqualTo(sse("(FOO)", "(BAR)")); + .isEqualTo(sse("(FOO)", "(BAR)")); + } + + @Test + public void sum() throws Exception { + + LinkedMultiValueMap 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("/sum")) + .accept(MediaType.APPLICATION_JSON).contentType(MediaType.MULTIPART_FORM_DATA) + .body(map), String.class).getBody()) + .isEqualTo("[{\"A\":6,\"B\":11}]"); } private String sse(String... values) { @@ -575,6 +584,20 @@ public class RestApplicationTests { Arrays.asList("come", "back")); } + @Bean + public Function, Map> sum() { + return valueMap -> valueMap + .entrySet() + .stream() + .collect( + Collectors + .toMap( + Map.Entry::getKey, + values -> values.getValue().stream().mapToInt(Integer::parseInt).sum() + ) + ); + } + } public static class Foo {