GH-1025 Add initial support for CRUD http mapping

This commit is contained in:
Oleg Zhurakousky
2023-05-26 16:16:08 +02:00
parent 38a30f05fc
commit f274325d77
6 changed files with 285 additions and 38 deletions

View File

@@ -0,0 +1,86 @@
/*
* Copyright 2023-2023 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;
import java.util.List;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.cloud.function.context.FunctionProperties;
/**
*
* @author Oleg Zhurakousky
* @since 4.0.4
*
*/
@ConfigurationProperties(prefix = FunctionProperties.PREFIX + ".http")
public class FunctionHttpProperties {
/**
* Blah.
*/
public List<String> get;
/**
* Blah.
*/
public List<String> post;
/**
* Blah.
*/
public List<String> put;
/**
* Blah.
*/
public List<String> delete;
public List<String> getGet() {
return this.get;
}
public void setGet(List<String> get) {
this.get = get;
}
public List<String> getPost() {
return post;
}
public void setPost(List<String> post) {
this.post = post;
}
public List<String> getPut() {
return put;
}
public void setPut(List<String> put) {
this.put = put;
}
public List<String> getDelete() {
return delete;
}
public void setDelete(List<String> delete) {
this.delete = delete;
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2012-2020 the original author or authors.
* Copyright 2016-2023 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.
@@ -20,7 +20,9 @@ import org.reactivestreams.Publisher;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cloud.function.context.catalog.SimpleFunctionRegistry.FunctionInvocationWrapper;
import org.springframework.cloud.function.web.FunctionHttpProperties;
import org.springframework.cloud.function.web.constants.WebRequestConstants;
import org.springframework.cloud.function.web.util.FunctionWebRequestProcessingHelper;
import org.springframework.cloud.function.web.util.FunctionWrapper;
@@ -32,8 +34,10 @@ import org.springframework.http.codec.multipart.Part;
import org.springframework.stereotype.Component;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
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.server.ServerWebExchange;
@@ -44,16 +48,29 @@ import org.springframework.web.server.ServerWebExchange;
* @author Oleg Zhurakousky
*/
@Component
@EnableConfigurationProperties(FunctionHttpProperties.class)
public class FunctionController {
private final FunctionHttpProperties functionHttpProperties;
public FunctionController(FunctionHttpProperties functionHttpProperties) {
this.functionHttpProperties = functionHttpProperties;
}
@SuppressWarnings("unchecked")
@PostMapping(path = "/**", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
@ResponseBody
public Mono<ResponseEntity<?>> form(ServerWebExchange request) {
FunctionWrapper wrapper = wrapper(request);
return request.getFormData().doOnSuccess(params -> wrapper.getParams().addAll(params))
.then(Mono.defer(() -> (Mono<ResponseEntity<?>>) FunctionWebRequestProcessingHelper
.processRequest(wrapper, wrapper.getParams(), false)));
if (FunctionWebRequestProcessingHelper.isValidFunction("POST", wrapper.getFunction().getFunctionDefinition(), this.functionHttpProperties)) {
return request.getFormData().doOnSuccess(params -> wrapper.getParams().addAll(params))
.then(Mono.defer(() -> (Mono<ResponseEntity<?>>) FunctionWebRequestProcessingHelper
.processRequest(wrapper, wrapper.getParams(), false)));
}
else {
throw new IllegalArgumentException(FunctionWebRequestProcessingHelper.buildBadMappingErrorMessage("POST", wrapper.getFunction().getFunctionDefinition()));
}
}
@SuppressWarnings("unchecked")
@@ -61,10 +78,15 @@ public class FunctionController {
@ResponseBody
public Mono<ResponseEntity<?>> multipart(ServerWebExchange request) {
FunctionWrapper wrapper = wrapper(request);
return request.getMultipartData()
.doOnSuccess(params -> wrapper.getParams().addAll(multi(params)))
.then(Mono.defer(() -> (Mono<ResponseEntity<?>>) FunctionWebRequestProcessingHelper
.processRequest(wrapper, wrapper.getParams(), false)));
if (FunctionWebRequestProcessingHelper.isValidFunction("POST", wrapper.getFunction().getFunctionDefinition(), this.functionHttpProperties)) {
return request.getMultipartData()
.doOnSuccess(params -> wrapper.getParams().addAll(multi(params)))
.then(Mono.defer(() -> (Mono<ResponseEntity<?>>) FunctionWebRequestProcessingHelper
.processRequest(wrapper, wrapper.getParams(), false)));
}
else {
throw new IllegalArgumentException(FunctionWebRequestProcessingHelper.buildBadMappingErrorMessage("POST", wrapper.getFunction().getFunctionDefinition()));
}
}
@SuppressWarnings("unchecked")
@@ -72,20 +94,66 @@ public class FunctionController {
@ResponseBody
public Mono<ResponseEntity<?>> post(ServerWebExchange request,
@RequestBody(required = false) String body) {
return (Mono<ResponseEntity<?>>) FunctionWebRequestProcessingHelper.processRequest(wrapper(request), body, false);
FunctionWrapper wrapper = wrapper(request);
if (FunctionWebRequestProcessingHelper.isValidFunction("POST", wrapper.getFunction().getFunctionDefinition(), this.functionHttpProperties)) {
return (Mono<ResponseEntity<?>>) FunctionWebRequestProcessingHelper.processRequest(wrapper, body, false);
}
else {
throw new IllegalArgumentException(FunctionWebRequestProcessingHelper.buildBadMappingErrorMessage("POST", wrapper.getFunction().getFunctionDefinition()));
}
}
@SuppressWarnings("unchecked")
@PutMapping(path = "/**")
@ResponseBody
public Mono<ResponseEntity<?>> put(ServerWebExchange request,
@RequestBody(required = false) String body) {
FunctionWrapper wrapper = wrapper(request);
if (FunctionWebRequestProcessingHelper.isValidFunction("PUT", wrapper.getFunction().getFunctionDefinition(), this.functionHttpProperties)) {
return (Mono<ResponseEntity<?>>) FunctionWebRequestProcessingHelper.processRequest(wrapper, body, false);
}
else {
throw new IllegalArgumentException(FunctionWebRequestProcessingHelper.buildBadMappingErrorMessage("PUT", wrapper.getFunction().getFunctionDefinition()));
}
}
@SuppressWarnings("unchecked")
@DeleteMapping(path = "/**")
@ResponseBody
public Mono<ResponseEntity<?>> delete(ServerWebExchange request,
@RequestBody(required = false) String body) {
FunctionWrapper wrapper = wrapper(request);
if (FunctionWebRequestProcessingHelper.isValidFunction("DELETE", wrapper.getFunction().getFunctionDefinition(), this.functionHttpProperties)) {
return (Mono<ResponseEntity<?>>) FunctionWebRequestProcessingHelper.processRequest(wrapper, body, false);
}
else {
throw new IllegalArgumentException(FunctionWebRequestProcessingHelper.buildBadMappingErrorMessage("DELETE", wrapper.getFunction().getFunctionDefinition()));
}
}
@PostMapping(path = "/**", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
@ResponseBody
public Publisher<?> postStream(ServerWebExchange request, @RequestBody(required = false) Flux<String> body) {
return FunctionWebRequestProcessingHelper.processRequest(wrapper(request), body, true);
FunctionWrapper wrapper = wrapper(request);
if (FunctionWebRequestProcessingHelper.isValidFunction("POST", wrapper.getFunction().getFunctionDefinition(), this.functionHttpProperties)) {
return FunctionWebRequestProcessingHelper.processRequest(wrapper, body, true);
}
else {
throw new IllegalArgumentException(FunctionWebRequestProcessingHelper.buildBadMappingErrorMessage("POST", wrapper.getFunction().getFunctionDefinition()));
}
}
@GetMapping(path = "/**", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
@ResponseBody
public Publisher<?> getStream(ServerWebExchange request) {
FunctionWrapper wrapper = wrapper(request);
return FunctionWebRequestProcessingHelper.processRequest(wrapper, wrapper.getArgument(), true);
if (FunctionWebRequestProcessingHelper.isValidFunction("GET", wrapper.getFunction().getFunctionDefinition(), this.functionHttpProperties)) {
return FunctionWebRequestProcessingHelper.processRequest(wrapper, wrapper.getArgument(), true);
}
else {
throw new IllegalArgumentException(FunctionWebRequestProcessingHelper.buildBadMappingErrorMessage("GET", wrapper.getFunction().getFunctionDefinition()));
}
}
@SuppressWarnings("unchecked")
@@ -93,7 +161,12 @@ public class FunctionController {
@ResponseBody
public Mono<ResponseEntity<?>> get(ServerWebExchange request) {
FunctionWrapper wrapper = wrapper(request);
return (Mono<ResponseEntity<?>>) FunctionWebRequestProcessingHelper.processRequest(wrapper, wrapper.getArgument(), false);
if (FunctionWebRequestProcessingHelper.isValidFunction("GET", wrapper.getFunction().getFunctionDefinition(), this.functionHttpProperties)) {
return (Mono<ResponseEntity<?>>) FunctionWebRequestProcessingHelper.processRequest(wrapper, wrapper.getArgument(), false);
}
else {
throw new IllegalArgumentException(FunctionWebRequestProcessingHelper.buildBadMappingErrorMessage("GET", wrapper.getFunction().getFunctionDefinition()));
}
}
private FunctionWrapper wrapper(ServerWebExchange request) {

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2012-2021 the original author or authors.
* Copyright 2016-2023 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.
@@ -25,7 +25,9 @@ import org.reactivestreams.Publisher;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cloud.function.context.catalog.SimpleFunctionRegistry.FunctionInvocationWrapper;
import org.springframework.cloud.function.web.FunctionHttpProperties;
import org.springframework.cloud.function.web.constants.WebRequestConstants;
import org.springframework.cloud.function.web.util.FunctionWebRequestProcessingHelper;
import org.springframework.cloud.function.web.util.FunctionWrapper;
@@ -39,8 +41,10 @@ import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import org.springframework.util.MultiValueMap;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
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.context.request.ServletWebRequest;
@@ -54,34 +58,45 @@ import org.springframework.web.multipart.support.StandardMultipartHttpServletReq
* @author Oleg Zhurakousky
*/
@Component
@EnableConfigurationProperties(FunctionHttpProperties.class)
public class FunctionController {
private final FunctionHttpProperties functionHttpProperties;
public FunctionController(FunctionHttpProperties functionHttpProperties) {
this.functionHttpProperties = functionHttpProperties;
}
@PostMapping(path = "/**", consumes = { MediaType.APPLICATION_FORM_URLENCODED_VALUE,
MediaType.MULTIPART_FORM_DATA_VALUE })
@ResponseBody
public Object form(WebRequest request) {
FunctionWrapper wrapper = wrapper(request);
if (FunctionWebRequestProcessingHelper.isValidFunction("POST", wrapper.getFunction().getFunctionDefinition(), this.functionHttpProperties)) {
if (((ServletWebRequest) request).getRequest() instanceof StandardMultipartHttpServletRequest) {
MultiValueMap<String, MultipartFile> multiFileMap = ((StandardMultipartHttpServletRequest) ((ServletWebRequest) request)
.getRequest()).getMultiFileMap();
if (!CollectionUtils.isEmpty(multiFileMap)) {
List<Message<MultipartFile>> files = multiFileMap.values().stream().flatMap(v -> v.stream())
.map(file -> MessageBuilder.withPayload(file).copyHeaders(wrapper.getHeaders()).build())
.collect(Collectors.toList());
FunctionInvocationWrapper function = wrapper.getFunction();
if (((ServletWebRequest) request).getRequest() instanceof StandardMultipartHttpServletRequest) {
MultiValueMap<String, MultipartFile> multiFileMap = ((StandardMultipartHttpServletRequest) ((ServletWebRequest) request)
.getRequest()).getMultiFileMap();
if (!CollectionUtils.isEmpty(multiFileMap)) {
List<Message<MultipartFile>> files = multiFileMap.values().stream().flatMap(v -> v.stream())
.map(file -> MessageBuilder.withPayload(file).copyHeaders(wrapper.getHeaders()).build())
.collect(Collectors.toList());
FunctionInvocationWrapper function = wrapper.getFunction();
Publisher<?> result = (Publisher<?>) function.apply(Flux.fromIterable(files));
BodyBuilder builder = ResponseEntity.ok();
if (result instanceof Flux) {
result = Flux.from(result).map(message -> {
return message instanceof Message ? ((Message<?>) message).getPayload() : message;
}).collectList();
Publisher<?> result = (Publisher<?>) function.apply(Flux.fromIterable(files));
BodyBuilder builder = ResponseEntity.ok();
if (result instanceof Flux) {
result = Flux.from(result).map(message -> {
return message instanceof Message ? ((Message<?>) message).getPayload() : message;
}).collectList();
}
return Mono.from(result).flatMap(body -> Mono.just(builder.body(body)));
}
return Mono.from(result).flatMap(body -> Mono.just(builder.body(body)));
}
return FunctionWebRequestProcessingHelper.processRequest(wrapper, wrapper.getParams(), false);
}
else {
throw new IllegalArgumentException(FunctionWebRequestProcessingHelper.buildBadMappingErrorMessage("POST", wrapper.getFunction().getFunctionDefinition()));
}
return FunctionWebRequestProcessingHelper.processRequest(wrapper, wrapper.getParams(), false);
}
@SuppressWarnings("unchecked")
@@ -90,29 +105,74 @@ public class FunctionController {
public Mono<ResponseEntity<Publisher<?>>> postStream(WebRequest request,
@RequestBody(required = false) String body) {
String argument = StringUtils.hasText(body) ? body : "";
return ((Mono<ResponseEntity<?>>) FunctionWebRequestProcessingHelper.processRequest(wrapper(request), argument, true)).map(response -> ResponseEntity.ok()
.headers(response.getHeaders()).body((Publisher<?>) response.getBody()));
FunctionWrapper wrapper = wrapper(request);
if (FunctionWebRequestProcessingHelper.isValidFunction("POST", wrapper.getFunction().getFunctionDefinition(), this.functionHttpProperties)) {
return ((Mono<ResponseEntity<?>>) FunctionWebRequestProcessingHelper.processRequest(wrapper, argument, true)).map(response -> ResponseEntity.ok()
.headers(response.getHeaders()).body((Publisher<?>) response.getBody()));
}
else {
throw new IllegalArgumentException(FunctionWebRequestProcessingHelper.buildBadMappingErrorMessage("POST", wrapper.getFunction().getFunctionDefinition()));
}
}
@GetMapping(path = "/**", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
@ResponseBody
public Publisher<?> getStream(WebRequest request) {
FunctionWrapper wrapper = wrapper(request);
return FunctionWebRequestProcessingHelper
.processRequest(wrapper, wrapper.getArgument(), true);
if (FunctionWebRequestProcessingHelper.isValidFunction("GET", wrapper.getFunction().getFunctionDefinition(), this.functionHttpProperties)) {
return FunctionWebRequestProcessingHelper.processRequest(wrapper, wrapper.getArgument(), true);
}
else {
throw new IllegalArgumentException(FunctionWebRequestProcessingHelper.buildBadMappingErrorMessage("GET", wrapper.getFunction().getFunctionDefinition()));
}
}
@PostMapping(path = "/**")
@ResponseBody
public Object post(WebRequest request, @RequestBody(required = false) String body) {
return FunctionWebRequestProcessingHelper.processRequest(wrapper(request), body, false);
FunctionWrapper wrapper = wrapper(request);
if (FunctionWebRequestProcessingHelper.isValidFunction("POST", wrapper.getFunction().getFunctionDefinition(), this.functionHttpProperties)) {
return FunctionWebRequestProcessingHelper.processRequest(wrapper, body, false);
}
else {
throw new IllegalArgumentException(FunctionWebRequestProcessingHelper.buildBadMappingErrorMessage("POST", wrapper.getFunction().getFunctionDefinition()));
}
}
@PutMapping(path = "/**")
@ResponseBody
public Object put(WebRequest request, @RequestBody(required = false) String body) {
FunctionWrapper wrapper = wrapper(request);
if (FunctionWebRequestProcessingHelper.isValidFunction("PUT", wrapper.getFunction().getFunctionDefinition(), this.functionHttpProperties)) {
return FunctionWebRequestProcessingHelper.processRequest(wrapper, body, false);
}
else {
throw new IllegalArgumentException(FunctionWebRequestProcessingHelper.buildBadMappingErrorMessage("PUT", wrapper.getFunction().getFunctionDefinition()));
}
}
@DeleteMapping(path = "/**")
@ResponseBody
public Object 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);
}
else {
throw new IllegalArgumentException(FunctionWebRequestProcessingHelper.buildBadMappingErrorMessage("DELETE", wrapper.getFunction().getFunctionDefinition()));
}
}
@GetMapping(path = "/**")
@ResponseBody
public Object get(WebRequest request) {
FunctionWrapper wrapper = wrapper(request);
return FunctionWebRequestProcessingHelper.processRequest(wrapper, wrapper.getArgument(), false);
if (FunctionWebRequestProcessingHelper.isValidFunction("GET", wrapper.getFunction().getFunctionDefinition(), this.functionHttpProperties)) {
return FunctionWebRequestProcessingHelper.processRequest(wrapper, wrapper.getArgument(), false);
}
else {
throw new IllegalArgumentException(FunctionWebRequestProcessingHelper.buildBadMappingErrorMessage("GET", wrapper.getFunction().getFunctionDefinition()));
}
}
private FunctionWrapper wrapper(WebRequest request) {

View File

@@ -30,6 +30,7 @@ import reactor.core.publisher.Mono;
import org.springframework.cloud.function.context.FunctionCatalog;
import org.springframework.cloud.function.context.FunctionProperties;
import org.springframework.cloud.function.context.catalog.SimpleFunctionRegistry.FunctionInvocationWrapper;
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;
@@ -71,6 +72,35 @@ public final class FunctionWebRequestProcessingHelper {
return postProcessResult(result, isMessage);
}
public static boolean isValidFunction(String httpMethod, String functionDefinition, FunctionHttpProperties functionHttpProperties) {
List<String> functionDefinitions = null;
switch (httpMethod) {
case "GET":
functionDefinitions = functionHttpProperties.getGet();
break;
case "POST":
functionDefinitions = functionHttpProperties.getPost();
break;
case "PUT":
functionDefinitions = functionHttpProperties.getPut();
break;
case "DELETE":
functionDefinitions = functionHttpProperties.getDelete();
break;
default:
return false;
}
return CollectionUtils.isEmpty(functionDefinitions) || functionDefinitions.contains(functionDefinition);
}
public static String buildBadMappingErrorMessage(String httpMethod, String functionDefinition) {
return "Function '" + functionDefinition + "' is not eligible to be invoked "
+ "via " + httpMethod + " method. This is due to the fact that explicit mappings for " + httpMethod
+ " are provided via 'spring.cloud.function.http." + httpMethod + "' property "
+ "and this function is not listed there. Either remove all explicit mappings for " + httpMethod + " or add this function to the list of functions "
+ "specified in 'spring.cloud.function.http." + httpMethod + "' property.";
}
@SuppressWarnings({ "rawtypes", "unchecked" })
public static Publisher<?> processRequest(FunctionWrapper wrapper, Object argument, boolean eventStream) {
if (argument == null) {