diff --git a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/annotation/RequestBodyParameterProcessor.java b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/annotation/RequestBodyParameterProcessor.java new file mode 100644 index 000000000..0af61f3fc --- /dev/null +++ b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/annotation/RequestBodyParameterProcessor.java @@ -0,0 +1,64 @@ +/* + * Copyright 2013-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.openfeign.annotation; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; + +import feign.MethodMetadata; +import feign.Request; + +import org.springframework.cloud.openfeign.AnnotatedParameterProcessor; +import org.springframework.web.bind.annotation.RequestBody; + +import static feign.Util.checkState; +import static org.springframework.cloud.openfeign.support.SpringEncoder.OPTIONAL_REQUEST_BODY; + +/** + * {@Link RequestBody} annotation processor. + * + * @author Matt King + * @see AnnotatedParameterProcessor + */ +public class RequestBodyParameterProcessor implements AnnotatedParameterProcessor { + + private static final Class ANNOTATION = RequestBody.class; + + @Override + public Class getAnnotationType() { + return ANNOTATION; + } + + @Override + public boolean processArgument(AnnotatedParameterContext context, + Annotation annotation, Method method) { + int parameterIndex = context.getParameterIndex(); + MethodMetadata data = context.getMethodMetadata(); + boolean required = ANNOTATION.cast(annotation).required(); + + checkState(data.bodyIndex() == null, "Only one request body is allowed."); + + data.bodyIndex(parameterIndex); + + if (!required) { + data.template().body(Request.Body.bodyTemplate(OPTIONAL_REQUEST_BODY, null)); + } + + return true; + } + +} diff --git a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/support/SpringEncoder.java b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/support/SpringEncoder.java index ed99a2b9b..5c9a78860 100644 --- a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/support/SpringEncoder.java +++ b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/support/SpringEncoder.java @@ -58,6 +58,9 @@ */ public class SpringEncoder implements Encoder { + /** Constant to identify request body as optional. */ + public static final String OPTIONAL_REQUEST_BODY = "OPTIONAL_REQUEST_BODY"; + private static final Log log = LogFactory.getLog(SpringEncoder.class); private final SpringFormEncoder springFormEncoder; @@ -150,6 +153,11 @@ else if (messageConverter instanceof ProtobufHttpMessageConverter } throw new EncodeException(message); } + else { + if (OPTIONAL_REQUEST_BODY.equals(request.bodyTemplate())) { + request.body(Request.Body.empty()); + } + } } @SuppressWarnings("unchecked") diff --git a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/support/SpringMvcContract.java b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/support/SpringMvcContract.java index 433c93358..e384863af 100644 --- a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/support/SpringMvcContract.java +++ b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/support/SpringMvcContract.java @@ -40,6 +40,7 @@ import org.springframework.cloud.openfeign.annotation.MatrixVariableParameterProcessor; import org.springframework.cloud.openfeign.annotation.PathVariableParameterProcessor; import org.springframework.cloud.openfeign.annotation.QueryMapParameterProcessor; +import org.springframework.cloud.openfeign.annotation.RequestBodyParameterProcessor; import org.springframework.cloud.openfeign.annotation.RequestHeaderParameterProcessor; import org.springframework.cloud.openfeign.annotation.RequestParamParameterProcessor; import org.springframework.cloud.openfeign.annotation.RequestPartParameterProcessor; @@ -359,6 +360,7 @@ private List getDefaultAnnotatedArgumentsProcessors annotatedArgumentResolvers.add(new MatrixVariableParameterProcessor()); annotatedArgumentResolvers.add(new PathVariableParameterProcessor()); + annotatedArgumentResolvers.add(new RequestBodyParameterProcessor()); annotatedArgumentResolvers.add(new RequestParamParameterProcessor()); annotatedArgumentResolvers.add(new RequestHeaderParameterProcessor()); annotatedArgumentResolvers.add(new QueryMapParameterProcessor()); diff --git a/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/support/SpringEncoderTests.java b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/support/SpringEncoderTests.java index 91c595a81..5544530fe 100644 --- a/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/support/SpringEncoderTests.java +++ b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/support/SpringEncoderTests.java @@ -18,11 +18,13 @@ import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; +import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.util.Collection; import java.util.Collections; import java.util.List; +import feign.Request; import feign.RequestTemplate; import feign.codec.EncodeException; import feign.codec.Encoder; @@ -54,6 +56,7 @@ import org.springframework.web.multipart.MultipartFile; import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.cloud.openfeign.support.SpringEncoder.OPTIONAL_REQUEST_BODY; import static org.springframework.http.HttpHeaders.ACCEPT; import static org.springframework.http.HttpHeaders.CONTENT_LENGTH; import static org.springframework.http.HttpHeaders.CONTENT_TYPE; @@ -185,6 +188,20 @@ public void testMultipartFile2() { .as("Body content cannot be decoded").contains("hi"); } + @Test + public void testOptionalBody() { + Encoder encoder = this.context.getInstance("foo", Encoder.class); + assertThat(encoder).isNotNull(); + + RequestTemplate request = new RequestTemplate(); + request.body(Request.Body.bodyTemplate(OPTIONAL_REQUEST_BODY, (Charset) null)); + + encoder.encode(null, Object.class, request); + + assertThat(request.requestBody()) + .isEqualToComparingFieldByField(Request.Body.empty()); + } + protected interface TestClient { } diff --git a/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/support/SpringMvcContractTests.java b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/support/SpringMvcContractTests.java index b22bcc588..fa5babc6e 100644 --- a/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/support/SpringMvcContractTests.java +++ b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/support/SpringMvcContractTests.java @@ -64,6 +64,8 @@ import static com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility.NONE; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.Assume.assumeTrue; +import static org.springframework.cloud.openfeign.support.SpringEncoder.OPTIONAL_REQUEST_BODY; +import static org.springframework.web.util.UriUtils.encode; /** * @author chadjaros @@ -530,6 +532,26 @@ public void testProcessQueryMapMoreThanOnce() throws Exception { this.contract.parseAndValidateMetadata(method.getDeclaringClass(), method); } + @Test + public void testRequestBody_optional() throws Exception { + Method method = TestTemplate_RequestBody.class.getDeclaredMethod("requestBody", + TestObject.class); + MethodMetadata data = contract + .parseAndValidateMetadata(method.getDeclaringClass(), method); + + assertThat(data.template().bodyTemplate()).isEqualTo(OPTIONAL_REQUEST_BODY); + } + + @Test + public void testRequestBody_required() throws Exception { + Method method = TestTemplate_RequestBody.class + .getDeclaredMethod("requestBodyRequired", TestObject.class); + MethodMetadata data = contract + .parseAndValidateMetadata(method.getDeclaringClass(), method); + + assertThat(data.template().bodyTemplate()).isNotEqualTo(OPTIONAL_REQUEST_BODY); + } + @Test public void testMatrixVariable_MapParam() throws Exception { Method method = TestTemplate_MatrixVariable.class @@ -770,6 +792,16 @@ public interface TestTemplate_MatrixVariable { } + public interface TestTemplate_RequestBody { + + @RequestMapping(path = "/requestBody") + String requestBody(@RequestBody(required = false) TestObject body); + + @RequestMapping(path = "/requestBody_required") + String requestBodyRequired(@RequestBody TestObject body); + + } + @JsonAutoDetect @RequestMapping("/advanced") public interface TestTemplate_Advanced {