From f6410af8e1031b4a3dc80b741918022b83809e39 Mon Sep 17 00:00:00 2001 From: Oleg Zhurakousky Date: Tue, 30 May 2023 14:04:14 +0200 Subject: [PATCH] GH-1025 Polish and test DELETE feature of s-c-f-web --- .../main/asciidoc/spring-cloud-function.adoc | 4 +- .../function/web/mvc/FunctionController.java | 13 +- .../FunctionWebRequestProcessingHelper.java | 6 +- .../function/web/util/FunctionWrapper.java | 17 ++ .../web/mvc/HttpDeleteIntegrationTests.java | 152 ++++++++++++++++++ 5 files changed, 184 insertions(+), 8 deletions(-) create mode 100644 spring-cloud-function-web/src/test/java/org/springframework/cloud/function/web/mvc/HttpDeleteIntegrationTests.java diff --git a/docs/src/main/asciidoc/spring-cloud-function.adoc b/docs/src/main/asciidoc/spring-cloud-function.adoc index 17bd81495..886768415 100644 --- a/docs/src/main/asciidoc/spring-cloud-function.adoc +++ b/docs/src/main/asciidoc/spring-cloud-function.adoc @@ -698,9 +698,9 @@ plain text and JSON. | GET | /{supplier} | - | Items from the named supplier | 200 OK | POST | /{consumer} | JSON object or text | Mirrors input and pushes request body into consumer | 202 Accepted -| POST | /{consumer} | JSON array or text with new lines | Mirrors input and pushes body into consumer one by one | 202 Accepted +| PUT | /{consumer} | JSON object or text | Mirrors input and pushes request body into consumer | 202 Accepted | POST | /{function} | JSON object or text | The result of applying the named function | 200 OK -| POST | /{function} | JSON array or text with new lines | The result of applying the named function | 200 OK +| PUT | /{function} | JSON object or text | The result of applying the named function | 200 OK | GET | /{function}/{item} | - | Convert the item into an object and return the result of applying the function | 200 OK |=== diff --git a/spring-cloud-function-web/src/main/java/org/springframework/cloud/function/web/mvc/FunctionController.java b/spring-cloud-function-web/src/main/java/org/springframework/cloud/function/web/mvc/FunctionController.java index 669ee2abd..bf390d11f 100644 --- a/spring-cloud-function-web/src/main/java/org/springframework/cloud/function/web/mvc/FunctionController.java +++ b/spring-cloud-function-web/src/main/java/org/springframework/cloud/function/web/mvc/FunctionController.java @@ -24,6 +24,7 @@ import java.util.stream.Collectors; import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +import reactor.netty.http.server.HttpServerRequest; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.cloud.function.context.catalog.SimpleFunctionRegistry.FunctionInvocationWrapper; @@ -32,6 +33,7 @@ import org.springframework.cloud.function.web.constants.WebRequestConstants; import org.springframework.cloud.function.web.util.FunctionWebRequestProcessingHelper; import org.springframework.cloud.function.web.util.FunctionWrapper; import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity.BodyBuilder; @@ -47,11 +49,14 @@ import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.context.request.ServletWebRequest; import org.springframework.web.context.request.WebRequest; import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.support.StandardMultipartHttpServletRequest; +import jakarta.servlet.http.HttpServletRequest; + /** * @author Dave Syer * @author Mark Fisher @@ -152,11 +157,11 @@ public class FunctionController { } @DeleteMapping(path = "/**") - @ResponseBody - public Object delete(WebRequest request, @RequestBody(required = false) String body) { + @ResponseStatus(value = HttpStatus.NO_CONTENT) + public void delete(WebRequest request, @RequestBody(required = false) String body) { FunctionWrapper wrapper = wrapper(request); if (FunctionWebRequestProcessingHelper.isValidFunction("DELETE", wrapper.getFunction().getFunctionDefinition(), this.functionHttpProperties)) { - return FunctionWebRequestProcessingHelper.processRequest(wrapper, body, false); + FunctionWebRequestProcessingHelper.processRequest(wrapper, wrapper.getArgument(), false); } else { throw new IllegalArgumentException(FunctionWebRequestProcessingHelper.buildBadMappingErrorMessage("DELETE", wrapper.getFunction().getFunctionDefinition())); @@ -178,7 +183,7 @@ public class FunctionController { private FunctionWrapper wrapper(WebRequest request) { FunctionInvocationWrapper function = (FunctionInvocationWrapper) request .getAttribute(WebRequestConstants.HANDLER, WebRequest.SCOPE_REQUEST); - FunctionWrapper wrapper = new FunctionWrapper(function); + FunctionWrapper wrapper = new FunctionWrapper(function, (((ServletWebRequest) request).getRequest()).getMethod()); for (String key : request.getParameterMap().keySet()) { wrapper.getParams().addAll(key, Arrays.asList(request.getParameterValues(key))); } diff --git a/spring-cloud-function-web/src/main/java/org/springframework/cloud/function/web/util/FunctionWebRequestProcessingHelper.java b/spring-cloud-function-web/src/main/java/org/springframework/cloud/function/web/util/FunctionWebRequestProcessingHelper.java index 8a7d37d6e..cbcfe8588 100644 --- a/spring-cloud-function-web/src/main/java/org/springframework/cloud/function/web/util/FunctionWebRequestProcessingHelper.java +++ b/spring-cloud-function-web/src/main/java/org/springframework/cloud/function/web/util/FunctionWebRequestProcessingHelper.java @@ -34,6 +34,7 @@ import org.springframework.cloud.function.web.FunctionHttpProperties; import org.springframework.cloud.function.web.constants.WebRequestConstants; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; +import org.springframework.http.RequestEntity.HeadersBuilder; import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity.BodyBuilder; import org.springframework.messaging.Message; @@ -59,7 +60,7 @@ public final class FunctionWebRequestProcessingHelper { public static FunctionInvocationWrapper findFunction(FunctionProperties functionProperties, HttpMethod method, FunctionCatalog functionCatalog, Map attributes, String path) { - if (method.equals(HttpMethod.GET) || method.equals(HttpMethod.POST)) { + if (method.equals(HttpMethod.GET) || method.equals(HttpMethod.POST) || method.equals(HttpMethod.PUT) || method.equals(HttpMethod.DELETE)) { return doFindFunction(functionProperties.getDefinition(), method, functionCatalog, attributes, path); } else { @@ -132,7 +133,8 @@ public final class FunctionWebRequestProcessingHelper { if (result instanceof Publisher) { Mono.from((Publisher) result).subscribe(); } - return Mono.just(ResponseEntity.accepted().headers(HeaderUtils.sanitize(headers)).build()); + return "DELETE".equals(wrapper.getMethod()) ? + Mono.empty() : Mono.just(ResponseEntity.ok().headers(HeaderUtils.sanitize(headers)).build()); } BodyBuilder responseOkBuilder = ResponseEntity.ok().headers(HeaderUtils.sanitize(headers)); diff --git a/spring-cloud-function-web/src/main/java/org/springframework/cloud/function/web/util/FunctionWrapper.java b/spring-cloud-function-web/src/main/java/org/springframework/cloud/function/web/util/FunctionWrapper.java index 47594778a..d9b6af26d 100644 --- a/spring-cloud-function-web/src/main/java/org/springframework/cloud/function/web/util/FunctionWrapper.java +++ b/spring-cloud-function-web/src/main/java/org/springframework/cloud/function/web/util/FunctionWrapper.java @@ -37,8 +37,21 @@ public class FunctionWrapper { private Object argument; + private final String method; + + /** + * + * @param function instance of {@link FunctionInvocationWrapper} + * @deprecated since 4.0.4 in favor of the constructor that takes Http method as second argument. + */ + @Deprecated public FunctionWrapper(FunctionInvocationWrapper function) { + this(function, null); + } + + public FunctionWrapper(FunctionInvocationWrapper function, String method) { this.function = function; + this.method = method; } public HttpHeaders getHeaders() { @@ -64,4 +77,8 @@ public class FunctionWrapper { public MultiValueMap getParams() { return params; } + + public String getMethod() { + return method; + } } diff --git a/spring-cloud-function-web/src/test/java/org/springframework/cloud/function/web/mvc/HttpDeleteIntegrationTests.java b/spring-cloud-function-web/src/test/java/org/springframework/cloud/function/web/mvc/HttpDeleteIntegrationTests.java new file mode 100644 index 000000000..e34cbc200 --- /dev/null +++ b/spring-cloud-function-web/src/test/java/org/springframework/cloud/function/web/mvc/HttpDeleteIntegrationTests.java @@ -0,0 +1,152 @@ +/* + * Copyright 2012-2019 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 + * + * https://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.web.mvc; + +import java.net.URI; +import java.util.ArrayList; +import java.util.Arrays; +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.stream.Collectors; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.boot.test.web.server.LocalServerPort; +import org.springframework.cloud.function.web.RestApplication; +import org.springframework.cloud.function.web.mvc.HttpDeleteIntegrationTests.ApplicationConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.RequestEntity; +import org.springframework.http.ResponseEntity; +import org.springframework.messaging.Message; +import org.springframework.messaging.support.MessageBuilder; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.util.StringUtils; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Oleg Zhurakousky + */ +@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT, properties = "spring.main.web-application-type=servlet") +@ContextConfiguration(classes = { RestApplication.class, ApplicationConfiguration.class }) +public class HttpDeleteIntegrationTests { + + private static final MediaType EVENT_STREAM = MediaType.TEXT_EVENT_STREAM; + + @LocalServerPort + private int port; + + @Autowired + private TestRestTemplate rest; + + @Autowired + private ApplicationConfiguration test; + + @BeforeEach + public void init() { + this.test.list.clear(); + } + + @Test + public void testDeleteConsumer() throws Exception { + ResponseEntity result = this.rest.exchange( + RequestEntity.delete(new URI("/deleteConsumer/123")) + .build(), Void.class); + + assertThat(result.getStatusCode()).isEqualTo(HttpStatus.NO_CONTENT); + } + + @Test + public void testDeleteConsumerWithParameters() throws Exception { + ResponseEntity result = this.rest.exchange( + RequestEntity.delete(new URI("/deleteConsumerAsMessage/123?foo=bar")) + .build(), Void.class); + + assertThat(result.getStatusCode()).isEqualTo(HttpStatus.NO_CONTENT); + } + + @EnableAutoConfiguration + @TestConfiguration + public static class ApplicationConfiguration { + + private List list = new ArrayList<>(); + + public static void main(String[] args) throws Exception { + SpringApplication.run(HttpDeleteIntegrationTests.ApplicationConfiguration.class, + args); + } + + @Bean + public Consumer deleteConsumer() { + return v -> { + assertThat(v).isEqualTo("123"); + System.out.println("Deleting: " + v); + }; + } + + @Bean + public Consumer> deleteConsumerAsMessage() { + return v -> { + assertThat(v.getPayload()).isEqualTo("123"); + assertThat(((Map) v.getHeaders().get("http_request_param")).get("foo")).isEqualTo("bar"); + System.out.println("Deleting: " + v); + }; + } + + } + + public static class Foo { + + private String value; + + public Foo(String value) { + this.value = value; + } + + Foo() { + } + + public String getValue() { + return this.value; + } + + public void setValue(String value) { + this.value = value; + } + + } + +}