diff --git a/jaxrs/src/main/java/feign/jaxrs/JAXRSContract.java b/jaxrs/src/main/java/feign/jaxrs/JAXRSContract.java index c660627e..95556ac0 100644 --- a/jaxrs/src/main/java/feign/jaxrs/JAXRSContract.java +++ b/jaxrs/src/main/java/feign/jaxrs/JAXRSContract.java @@ -151,7 +151,8 @@ public class JAXRSContract extends DeclarativeContract { } // Not using override as the super-type's method is deprecated and will be removed. - private String addTemplatedParam(String name) { + // Protected so JAXRS2Contract can make use of this + protected String addTemplatedParam(String name) { return String.format("{%s}", name); } } diff --git a/jaxrs2/README.md b/jaxrs2/README.md index cbdd4be7..fa340bd2 100644 --- a/jaxrs2/README.md +++ b/jaxrs2/README.md @@ -35,3 +35,5 @@ Links the value of the corresponding parameter to a query parameter. When invok Links the value of the corresponding parameter to a header. #### `@FormParam` Links the value of the corresponding parameter to a key passed to `Encoder.Text>.encode()`. +#### `@BeanParm` +Aggregates the above supported parameter annotations under a single value object. diff --git a/jaxrs2/pom.xml b/jaxrs2/pom.xml index 2cca77be..db954f01 100644 --- a/jaxrs2/pom.xml +++ b/jaxrs2/pom.xml @@ -42,6 +42,12 @@ feign-core + + javax.ws.rs + javax.ws.rs-api + 2.1.1 + + ${project.groupId} feign-jaxrs @@ -53,12 +59,6 @@ - - javax.ws.rs - javax.ws.rs-api - 2.1.1 - - ${project.groupId} @@ -77,6 +77,12 @@ feign-jaxrs test-jar test + + + javax.ws.rs + jsr311-api + + diff --git a/jaxrs2/src/main/java/feign/jaxrs2/JAXRS2Contract.java b/jaxrs2/src/main/java/feign/jaxrs2/JAXRS2Contract.java index 736a9276..094d3f4f 100644 --- a/jaxrs2/src/main/java/feign/jaxrs2/JAXRS2Contract.java +++ b/jaxrs2/src/main/java/feign/jaxrs2/JAXRS2Contract.java @@ -13,10 +13,13 @@ */ package feign.jaxrs2; -import javax.ws.rs.BeanParam; +import feign.jaxrs.JAXRSContract; +import javax.ws.rs.*; import javax.ws.rs.container.Suspended; import javax.ws.rs.core.Context; -import feign.jaxrs.JAXRSContract; +import java.lang.reflect.Field; +import static feign.Util.checkState; +import static feign.Util.emptyToNull; /** * Please refer to the Feign @@ -31,7 +34,67 @@ public final class JAXRS2Contract extends JAXRSContract { // https://github.com/OpenFeign/feign/issues/669 super.registerParameterAnnotation(Suspended.class, (ann, data, i) -> data.ignoreParamater(i)); super.registerParameterAnnotation(Context.class, (ann, data, i) -> data.ignoreParamater(i)); - super.registerParameterAnnotation(BeanParam.class, (ann, data, i) -> data.ignoreParamater(i)); + } + @Override + protected void registerParamAnnotations() { + super.registerParamAnnotations(); + + registerParameterAnnotation(BeanParam.class, (param, data, paramIndex) -> { + final Field[] aggregatedParams = data.method() + .getParameters()[paramIndex] + .getType() + .getDeclaredFields(); + + for (Field aggregatedParam : aggregatedParams) { + + if (aggregatedParam.isAnnotationPresent(PathParam.class)) { + final String name = aggregatedParam.getAnnotation(PathParam.class).value(); + checkState( + emptyToNull(name) != null, + "BeanParam parameter %s contains PathParam with empty .value() on field %s", + paramIndex, + aggregatedParam.getName()); + nameParam(data, name, paramIndex); + } + + if (aggregatedParam.isAnnotationPresent(QueryParam.class)) { + final String name = aggregatedParam.getAnnotation(QueryParam.class).value(); + checkState( + emptyToNull(name) != null, + "BeanParam parameter %s contains QueryParam with empty .value() on field %s", + paramIndex, + aggregatedParam.getName()); + final String query = addTemplatedParam(name); + data.template().query(name, query); + nameParam(data, name, paramIndex); + } + + if (aggregatedParam.isAnnotationPresent(HeaderParam.class)) { + final String name = aggregatedParam.getAnnotation(HeaderParam.class).value(); + checkState( + emptyToNull(name) != null, + "BeanParam parameter %s contains HeaderParam with empty .value() on field %s", + paramIndex, + aggregatedParam.getName()); + final String header = addTemplatedParam(name); + data.template().header(name, header); + nameParam(data, name, paramIndex); + } + + if (aggregatedParam.isAnnotationPresent(FormParam.class)) { + final String name = aggregatedParam.getAnnotation(FormParam.class).value(); + checkState( + emptyToNull(name) != null, + "BeanParam parameter %s contains FormParam with empty .value() on field %s", + paramIndex, + aggregatedParam.getName()); + data.formParams().add(name); + nameParam(data, name, paramIndex); + } + } + }); + + } } diff --git a/jaxrs2/src/test/java/feign/jaxrs2/JAXRS2ContractWithBeanParamSupportTest.java b/jaxrs2/src/test/java/feign/jaxrs2/JAXRS2ContractWithBeanParamSupportTest.java new file mode 100644 index 00000000..53cd84aa --- /dev/null +++ b/jaxrs2/src/test/java/feign/jaxrs2/JAXRS2ContractWithBeanParamSupportTest.java @@ -0,0 +1,92 @@ +/* + * Copyright 2012-2023 The Feign 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 feign.jaxrs2; + +import feign.MethodMetadata; +import feign.jaxrs.JAXRSContract; +import feign.jaxrs.JAXRSContractTest; +import feign.jaxrs2.JAXRS2ContractWithBeanParamSupportTest.Jaxrs2Internals.BeanParamInput; +import org.junit.Test; +import javax.ws.rs.*; +import javax.ws.rs.container.AsyncResponse; +import javax.ws.rs.container.Suspended; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.UriInfo; +import static feign.assertj.FeignAssertions.assertThat; +import static java.util.Arrays.asList; +import static org.assertj.core.data.MapEntry.entry; + +/** + * Tests interfaces defined per {@link JAXRS2Contract} are interpreted into expected + * {@link feign .RequestTemplate template} instances. + */ +public class JAXRS2ContractWithBeanParamSupportTest extends JAXRSContractTest { + + @Override + protected JAXRSContract createContract() { + return new JAXRS2Contract(); + } + + @Test + public void injectJaxrsInternals() throws Exception { + final MethodMetadata methodMetadata = + parseAndValidateMetadata(Jaxrs2Internals.class, "inject", AsyncResponse.class, + UriInfo.class); + assertThat(methodMetadata.template()) + .noRequestBody(); + } + + @Test + public void injectBeanParam() throws Exception { + final MethodMetadata methodMetadata = + parseAndValidateMetadata(Jaxrs2Internals.class, "beanParameters", BeanParamInput.class); + assertThat(methodMetadata.template()) + .noRequestBody(); + + assertThat(methodMetadata.template()) + .hasHeaders(entry("X-Custom-Header", asList("{X-Custom-Header}"))); + assertThat(methodMetadata.template()) + .hasQueries(entry("query", asList("{query}"))); + assertThat(methodMetadata.formParams()) + .isNotEmpty() + .containsExactly("form"); + + } + + public interface Jaxrs2Internals { + @GET + @Path("/") + void inject(@Suspended AsyncResponse ar, @Context UriInfo info); + + @Path("/{path}") + @POST + void beanParameters(@BeanParam BeanParamInput beanParam); + + public class BeanParamInput { + + @PathParam("path") + String path; + + @QueryParam("query") + String query; + + @FormParam("form") + String form; + + @HeaderParam("X-Custom-Header") + String header; + } + } + +}