From f4be32f37f38b6d98e16a11a9373c5dcebf16c0c Mon Sep 17 00:00:00 2001 From: Ralph Jennings Date: Fri, 23 Mar 2018 14:55:52 -0700 Subject: [PATCH 01/13] Added support for custom param encoding --- README.md | 28 +++++ core/src/main/java/feign/Contract.java | 4 + core/src/main/java/feign/CustomParam.java | 50 ++++++++ core/src/main/java/feign/MethodMetadata.java | 12 ++ core/src/main/java/feign/ReflectiveFeign.java | 13 ++- .../test/java/feign/DefaultContractTest.java | 34 ++++++ core/src/test/java/feign/FeignTest.java | 107 +++++++++++++++++- 7 files changed, 246 insertions(+), 2 deletions(-) create mode 100644 core/src/main/java/feign/CustomParam.java diff --git a/README.md b/README.md index e38c6e73aa..3df1019514 100644 --- a/README.md +++ b/README.md @@ -443,6 +443,34 @@ A Map parameter can be annotated with `QueryMap` to construct a query that uses V find(@QueryMap Map queryMap); ``` +#### Custom Parameter Encoding +Parameters annotated with `CustomParam` will be encoded using the specified `CustomParam.ParamEncoder`. + +```java +@RequestLine("GET /") +V find(@CustomParam(encoder = CustomObjectEncoder.class) CustomObject customObject); +``` + +Your custom param encoder just needs a public 0-arg constructor and to implement the CustomParam.ParamEncoder interface. Following is an example: +```java +public class CustomObjectEncoder implements CustomParam.ParamEncoder { + + public void encode (Object object, RequestTemplate template) { + + CustomObject customObject = (CustomObject)object; + if (customObject.getName() != null) { + template.query("name", customObject.getName()); + } + if (customObject.getAddress() != null) { + template.query("address", customObject.getAddress()); + } + if (customObject.getCustomHeaderValue() != null) { + template.header("X-Custom-Header", customObject.getCustomHeaderValue()); + } + } +} +``` + #### Static and Default Methods Interfaces targeted by Feign may have static or default methods (if using Java 8+). These allows Feign clients to contain logic that is not expressly defined by the underlying API. diff --git a/core/src/main/java/feign/Contract.java b/core/src/main/java/feign/Contract.java index c5a3a106fe..e1f7cead36 100644 --- a/core/src/main/java/feign/Contract.java +++ b/core/src/main/java/feign/Contract.java @@ -273,6 +273,10 @@ protected boolean processAnnotationsOnParameter(MethodMetadata data, Annotation[ !searchMapValuesContainsSubstring(data.template().headers(), varName)) { data.formParams().add(name); } + } else if (annotationType == CustomParam.class) { + Class encoderClass = ((CustomParam)annotation).encoder(); + data.indexToCustomEncoderClass().put(paramIndex, encoderClass); + isHttpAnnotation = true; } else if (annotationType == QueryMap.class) { checkState(data.queryMapIndex() == null, "QueryMap annotation was present on multiple parameters."); data.queryMapIndex(paramIndex); diff --git a/core/src/main/java/feign/CustomParam.java b/core/src/main/java/feign/CustomParam.java new file mode 100644 index 0000000000..725beb38d3 --- /dev/null +++ b/core/src/main/java/feign/CustomParam.java @@ -0,0 +1,50 @@ +/** + * Copyright 2012-2018 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; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.ANNOTATION_TYPE; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +/** + * A custom object with parameters that will be applied to the {@link RequestTemplate} + * via a custom {@link ParamEncoder} class. + */ +@Retention(RUNTIME) +@Target({PARAMETER, ANNOTATION_TYPE}) +public @interface CustomParam { + + /** + * How to encode this parameter (must be supplied) + */ + Class encoder(); + + /** + * An encoder for a {@link CustomParam} annotated parameter. + * Must have a public 0-arg constructor. + */ + interface ParamEncoder { + + /** + * Converts objects to appropriate representations in the template. + * + * @param object what to encode. + * @param template the request template to populate. + */ + void encode (Object object, RequestTemplate template); + } +} diff --git a/core/src/main/java/feign/MethodMetadata.java b/core/src/main/java/feign/MethodMetadata.java index 431b3f86c6..792f75c2ef 100644 --- a/core/src/main/java/feign/MethodMetadata.java +++ b/core/src/main/java/feign/MethodMetadata.java @@ -41,6 +41,8 @@ public final class MethodMetadata implements Serializable { private Map> indexToExpanderClass = new LinkedHashMap>(); private Map indexToEncoded = new LinkedHashMap(); + private Map> indexToCustomEncoderClass = + new LinkedHashMap>(); private transient Map indexToExpander; MethodMetadata() { @@ -106,6 +108,16 @@ public MethodMetadata queryMapIndex(Integer queryMapIndex) { return this; } + public Map> indexToCustomEncoderClass () { + return indexToCustomEncoderClass; + + } + + public MethodMetadata indexToCustomEncoderClass (Map> indexToCustomEncoderClass) { + this.indexToCustomEncoderClass = indexToCustomEncoderClass; + return this; + } + public boolean queryMapEncoded() { return queryMapEncoded; } diff --git a/core/src/main/java/feign/ReflectiveFeign.java b/core/src/main/java/feign/ReflectiveFeign.java index fc3ea9273e..69c11c0b9c 100644 --- a/core/src/main/java/feign/ReflectiveFeign.java +++ b/core/src/main/java/feign/ReflectiveFeign.java @@ -29,7 +29,6 @@ import static feign.Util.checkArgument; import static feign.Util.checkNotNull; -import static feign.Util.checkState; public class ReflectiveFeign extends Feign { @@ -219,6 +218,18 @@ public RequestTemplate create(Object[] argv) { template = addHeaderMapHeaders((Map) argv[metadata.headerMapIndex()], template); } + if (metadata.indexToCustomEncoderClass() != null) { + for (Map.Entry> entry : metadata.indexToCustomEncoderClass().entrySet()) { + try { + entry.getValue().newInstance().encode(argv[entry.getKey()], template); + } catch (InstantiationException e) { + throw new IllegalStateException(e); + } catch (IllegalAccessException e) { + throw new IllegalStateException(e); + } + } + } + return template; } diff --git a/core/src/test/java/feign/DefaultContractTest.java b/core/src/test/java/feign/DefaultContractTest.java index 5f6b01359a..0e47848abd 100644 --- a/core/src/test/java/feign/DefaultContractTest.java +++ b/core/src/test/java/feign/DefaultContractTest.java @@ -15,6 +15,7 @@ import com.google.gson.reflect.TypeToken; +import feign.CustomParam.ParamEncoder; import org.assertj.core.api.Fail; import org.junit.Rule; import org.junit.Test; @@ -306,6 +307,13 @@ public void queryMapMapSubclass() throws Exception { assertThat(md.queryMapIndex()).isEqualTo(0); } + @Test + public void customParamObject() throws Exception { + MethodMetadata md = parseAndValidateMetadata(CustomParamObjectInterface.class, "customObject", CustomObject.class); + + assertThat(md.indexToCustomEncoderClass()).containsOnly(entry(0, CustomObjectParamEncoder.class)); + } + @Test public void onlyOneQueryMapAnnotationPermitted() throws Exception { try { @@ -473,6 +481,32 @@ interface HeaderParamsNotAtStart { void logout(@Param("authToken") String token); } + interface CustomParamObjectInterface { + + @RequestLine("POST /") + void customObject(@CustomParam(encoder = CustomObjectParamEncoder.class) CustomObject object); + } + + class CustomObject { + + String name; + String address; + } + + class CustomObjectParamEncoder implements ParamEncoder { + + @Override + public void encode (Object object, RequestTemplate template) { + CustomObject customObject = (CustomObject)object; + if (customObject.name != null) { + template.query("name", customObject.name); + } + if (customObject.address != null) { + template.query("address", ((CustomObject) object).address); + } + } + } + interface CustomExpander { @RequestLine("POST /?date={date}") diff --git a/core/src/test/java/feign/FeignTest.java b/core/src/test/java/feign/FeignTest.java index 245f8092dc..7b80df166b 100644 --- a/core/src/test/java/feign/FeignTest.java +++ b/core/src/test/java/feign/FeignTest.java @@ -384,6 +384,72 @@ public void queryMapValueStartingWithBrace() throws Exception { .hasPath("/?%7Bname=%7Balice"); } + @Test + public void customParamObjectWithExplicitParams() throws Exception { + TestInterface api = new TestInterfaceBuilder().target("http://localhost:" + server.getPort()); + + CustomObject customObject = new CustomObject(); + customObject.name = "Name"; + customObject.number = 3; + + server.enqueue(new MockResponse()); + api.customObjectWithExplicitParams(customObject); + assertThat(server.takeRequest()) + .hasPath("/?name=Name&number=3"); + } + + @Test + public void customParamObjectWithImplicitParams() throws Exception { + TestInterface api = new TestInterfaceBuilder().target("http://localhost:" + server.getPort()); + + CustomObject customObject = new CustomObject(); + customObject.name = "Name"; + customObject.number = 3; + + server.enqueue(new MockResponse()); + api.customObjectWithImpliedParams(customObject); + assertThat(server.takeRequest()) + .hasPath("/?name=Name&number=3"); + } + + @Test + public void customParamObjectWithPartialParams() throws Exception { + TestInterface api = new TestInterfaceBuilder().target("http://localhost:" + server.getPort()); + + CustomObject customObject = new CustomObject(); + customObject.name = "Name"; + customObject.number = null; + + server.enqueue(new MockResponse()); + api.customObjectWithImpliedParams(customObject); + assertThat(server.takeRequest()) + .hasPath("/?name=Name"); + } + + @Test + public void customParamObjectWithEmptyParams() throws Exception { + TestInterface api = new TestInterfaceBuilder().target("http://localhost:" + server.getPort()); + + CustomObject customObject = new CustomObject(); + customObject.name = null; + customObject.number = null; + + server.enqueue(new MockResponse()); + api.customObjectWithImpliedParams(customObject); + assertThat(server.takeRequest()) + .hasPath("/"); + } + + @Test + public void customParamWithCustomEncoder() throws Exception { + TestInterface api = new TestInterfaceBuilder().target("http://localhost:" + server.getPort()); + + server.enqueue(new MockResponse()); + api.customObjectWithHeaderParamEncoder("header value"); + assertThat(server.takeRequest()) + .hasHeaders(MapEntry.entry(HeaderGeneratingEncoder.HEADER_NAME, Arrays.asList("header value"))); + } + @Test public void configKeyFormatsAsExpected() throws Exception { assertEquals("TestInterface#post()", @@ -590,7 +656,7 @@ public void decodingDoesNotSwallow404ErrorsInDecode404Mode() throws Exception { .decode404() .errorDecoder(new IllegalArgumentExceptionOn404()) .target("http://localhost:" + server.getPort()); - api.queryMap(Collections.emptyMap()); + api.queryMap(Collections.emptyMap()); } @Test @@ -796,6 +862,15 @@ void form( @RequestLine("GET /?trim={trim}") void encodedQueryParam(@Param(value = "trim", encoded = true) String trim); + @RequestLine("GET /?name={name}&number={number}") + void customObjectWithExplicitParams(@CustomParam(encoder = CustomObjectParamEncoder.class) CustomObject object); + + @RequestLine("GET /") + void customObjectWithImpliedParams(@CustomParam(encoder = CustomObjectParamEncoder.class) CustomObject object); + + @RequestLine("GET /") + void customObjectWithHeaderParamEncoder(@CustomParam(encoder = HeaderGeneratingEncoder.class) String value); + class DateToMillis implements Param.Expander { @Override @@ -805,6 +880,36 @@ public String expand(Object value) { } } + static class CustomObject { + + String name; + Integer number; + } + + static class CustomObjectParamEncoder implements CustomParam.ParamEncoder { + + @Override + public void encode (Object object, RequestTemplate template) { + CustomObject customObject = (CustomObject)object; + if (customObject.name != null) { + template.query("name", customObject.name); + } + if (customObject.number != null) { + template.query("number", customObject.number.toString()); + } + } + } + + static class HeaderGeneratingEncoder implements CustomParam.ParamEncoder { + + final static String HEADER_NAME = "X-Generated-Header"; + + @Override + public void encode (Object object, RequestTemplate template) { + template.header(HEADER_NAME, (String)object); + } + } + interface OtherTestInterface { @RequestLine("POST /") From 6b8ac825a13cc9e2fa6aad5d8d44be08f59e2cdb Mon Sep 17 00:00:00 2001 From: Ralph Jennings Date: Fri, 23 Mar 2018 19:57:39 -0700 Subject: [PATCH 02/13] Added ability to inherit @CustomParam annotation --- core/src/main/java/feign/Contract.java | 19 +++++++-- .../test/java/feign/DefaultContractTest.java | 39 +++++++++++++++++++ 2 files changed, 54 insertions(+), 4 deletions(-) diff --git a/core/src/main/java/feign/Contract.java b/core/src/main/java/feign/Contract.java index e1f7cead36..8d44202625 100644 --- a/core/src/main/java/feign/Contract.java +++ b/core/src/main/java/feign/Contract.java @@ -273,10 +273,6 @@ protected boolean processAnnotationsOnParameter(MethodMetadata data, Annotation[ !searchMapValuesContainsSubstring(data.template().headers(), varName)) { data.formParams().add(name); } - } else if (annotationType == CustomParam.class) { - Class encoderClass = ((CustomParam)annotation).encoder(); - data.indexToCustomEncoderClass().put(paramIndex, encoderClass); - isHttpAnnotation = true; } else if (annotationType == QueryMap.class) { checkState(data.queryMapIndex() == null, "QueryMap annotation was present on multiple parameters."); data.queryMapIndex(paramIndex); @@ -286,11 +282,26 @@ protected boolean processAnnotationsOnParameter(MethodMetadata data, Annotation[ checkState(data.headerMapIndex() == null, "HeaderMap annotation was present on multiple parameters."); data.headerMapIndex(paramIndex); isHttpAnnotation = true; + } else { + CustomParam customParam = findCustomParam(annotation); + if (customParam != null) { + Class encoderClass = customParam.encoder(); + data.indexToCustomEncoderClass().put(paramIndex, encoderClass); + isHttpAnnotation = true; + } } } return isHttpAnnotation; } + private static CustomParam findCustomParam (Annotation annotation) { + if (annotation instanceof CustomParam) { + return (CustomParam)annotation; + } else { + return annotation.annotationType().getAnnotation(CustomParam.class); + } + } + private static boolean searchMapValuesContainsSubstring(Map> map, String search) { Collection> values = map.values(); diff --git a/core/src/test/java/feign/DefaultContractTest.java b/core/src/test/java/feign/DefaultContractTest.java index 0e47848abd..a896bf67b4 100644 --- a/core/src/test/java/feign/DefaultContractTest.java +++ b/core/src/test/java/feign/DefaultContractTest.java @@ -16,6 +16,9 @@ import com.google.gson.reflect.TypeToken; import feign.CustomParam.ParamEncoder; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; import org.assertj.core.api.Fail; import org.junit.Rule; import org.junit.Test; @@ -314,6 +317,22 @@ public void customParamObject() throws Exception { assertThat(md.indexToCustomEncoderClass()).containsOnly(entry(0, CustomObjectParamEncoder.class)); } + @Test + public void customParamObjectMultiple() throws Exception { + MethodMetadata md = parseAndValidateMetadata(MultipleCustomParamObjectInterface.class, "customObjects", CustomObject.class, CustomObject.class); + + assertThat(md.indexToCustomEncoderClass()).contains( + entry(0, CustomObjectParamEncoder.class), + entry(1, CustomObjectParamEncoder.class)); + } + + @Test + public void customParamObjectInheritedAnnotation() throws Exception { + MethodMetadata md = parseAndValidateMetadata(InheritedCustomParamObjectInterface.class, "customObject", CustomObject.class); + + assertThat(md.indexToCustomEncoderClass()).containsOnly(entry(0, CustomObjectParamEncoder.class)); + } + @Test public void onlyOneQueryMapAnnotationPermitted() throws Exception { try { @@ -487,6 +506,26 @@ interface CustomParamObjectInterface { void customObject(@CustomParam(encoder = CustomObjectParamEncoder.class) CustomObject object); } + @Retention(RetentionPolicy.RUNTIME) + @java.lang.annotation.Target(ElementType.PARAMETER) + @CustomParam(encoder = CustomObjectParamEncoder.class) + @interface InheritedCustomParam { + } + + interface InheritedCustomParamObjectInterface { + + @RequestLine("POST /") + void customObject(@InheritedCustomParam CustomObject object); + } + + interface MultipleCustomParamObjectInterface { + + @RequestLine("POST /") + void customObjects( + @CustomParam(encoder = CustomObjectParamEncoder.class) CustomObject object1, + @CustomParam(encoder = CustomObjectParamEncoder.class) CustomObject object2); + } + class CustomObject { String name; From 0eea796e7ab98a4628e08d59ea961be560b58b74 Mon Sep 17 00:00:00 2001 From: Ralph Jennings Date: Mon, 26 Mar 2018 12:58:32 -0700 Subject: [PATCH 03/13] Updated class cast style to match rest of code --- core/src/main/java/feign/Contract.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/java/feign/Contract.java b/core/src/main/java/feign/Contract.java index 8d44202625..588b6394ef 100644 --- a/core/src/main/java/feign/Contract.java +++ b/core/src/main/java/feign/Contract.java @@ -296,7 +296,7 @@ protected boolean processAnnotationsOnParameter(MethodMetadata data, Annotation[ private static CustomParam findCustomParam (Annotation annotation) { if (annotation instanceof CustomParam) { - return (CustomParam)annotation; + return CustomParam.class.cast(annotation); } else { return annotation.annotationType().getAnnotation(CustomParam.class); } From 186f66c31e0c58ebefed8b197f221f45d227b81e Mon Sep 17 00:00:00 2001 From: Ralph Jennings Date: Mon, 2 Apr 2018 18:10:45 -0700 Subject: [PATCH 04/13] Updated to use QueryMap for custom pojo query parameters --- README.md | 29 ++--- core/src/main/java/feign/Contract.java | 23 ++-- core/src/main/java/feign/CustomParam.java | 50 -------- core/src/main/java/feign/MethodMetadata.java | 12 -- .../main/java/feign/ObjectParamMetadata.java | 64 ++++++++++ core/src/main/java/feign/ReflectiveFeign.java | 30 ++--- .../test/java/feign/DefaultContractTest.java | 115 +++++------------- core/src/test/java/feign/FeignTest.java | 89 +++----------- 8 files changed, 147 insertions(+), 265 deletions(-) delete mode 100644 core/src/main/java/feign/CustomParam.java create mode 100644 core/src/main/java/feign/ObjectParamMetadata.java diff --git a/README.md b/README.md index 3df1019514..255088817b 100644 --- a/README.md +++ b/README.md @@ -443,30 +443,23 @@ A Map parameter can be annotated with `QueryMap` to construct a query that uses V find(@QueryMap Map queryMap); ``` -#### Custom Parameter Encoding -Parameters annotated with `CustomParam` will be encoded using the specified `CustomParam.ParamEncoder`. +This may also be used to generate the query parameters from a POJO object. ```java -@RequestLine("GET /") -V find(@CustomParam(encoder = CustomObjectEncoder.class) CustomObject customObject); +@RequestLine("GET /find") +V find(@QueryMap CustomPojo customPojo); ``` -Your custom param encoder just needs a public 0-arg constructor and to implement the CustomParam.ParamEncoder interface. Following is an example: -```java -public class CustomObjectEncoder implements CustomParam.ParamEncoder { +When used in this manner, the query map will be generated using member variable names as query parameter names. The following POJO will generate query params of "name={name}&number={number}" - public void encode (Object object, RequestTemplate template) { +```java +public class CustomPojo { + private final String name; + private final int number; - CustomObject customObject = (CustomObject)object; - if (customObject.getName() != null) { - template.query("name", customObject.getName()); - } - if (customObject.getAddress() != null) { - template.query("address", customObject.getAddress()); - } - if (customObject.getCustomHeaderValue() != null) { - template.header("X-Custom-Header", customObject.getCustomHeaderValue()); - } + public CustomPojo (String name, int number) { + this.name = name; + this.number = number; } } ``` diff --git a/core/src/main/java/feign/Contract.java b/core/src/main/java/feign/Contract.java index 588b6394ef..f14e6f1d0d 100644 --- a/core/src/main/java/feign/Contract.java +++ b/core/src/main/java/feign/Contract.java @@ -123,7 +123,9 @@ protected MethodMetadata parseAndValidateMetadata(Class targetType, Method me } if (data.queryMapIndex() != null) { - checkMapString("QueryMap", parameterTypes[data.queryMapIndex()], genericParameterTypes[data.queryMapIndex()]); + if (Map.class.isAssignableFrom(parameterTypes[data.queryMapIndex()])) { + checkMapKeys("QueryMap", genericParameterTypes[data.queryMapIndex()]); + } } return data; @@ -132,6 +134,10 @@ protected MethodMetadata parseAndValidateMetadata(Class targetType, Method me private static void checkMapString(String name, Class type, Type genericType) { checkState(Map.class.isAssignableFrom(type), "%s parameter must be a Map: %s", name, type); + checkMapKeys(name, genericType); + } + + private static void checkMapKeys(String name, Type genericType) { Type[] parameterTypes = ((ParameterizedType) genericType).getActualTypeArguments(); Class keyClass = (Class) parameterTypes[0]; checkState(String.class.equals(keyClass), @@ -282,26 +288,11 @@ protected boolean processAnnotationsOnParameter(MethodMetadata data, Annotation[ checkState(data.headerMapIndex() == null, "HeaderMap annotation was present on multiple parameters."); data.headerMapIndex(paramIndex); isHttpAnnotation = true; - } else { - CustomParam customParam = findCustomParam(annotation); - if (customParam != null) { - Class encoderClass = customParam.encoder(); - data.indexToCustomEncoderClass().put(paramIndex, encoderClass); - isHttpAnnotation = true; - } } } return isHttpAnnotation; } - private static CustomParam findCustomParam (Annotation annotation) { - if (annotation instanceof CustomParam) { - return CustomParam.class.cast(annotation); - } else { - return annotation.annotationType().getAnnotation(CustomParam.class); - } - } - private static boolean searchMapValuesContainsSubstring(Map> map, String search) { Collection> values = map.values(); diff --git a/core/src/main/java/feign/CustomParam.java b/core/src/main/java/feign/CustomParam.java deleted file mode 100644 index 725beb38d3..0000000000 --- a/core/src/main/java/feign/CustomParam.java +++ /dev/null @@ -1,50 +0,0 @@ -/** - * Copyright 2012-2018 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; - -import java.lang.annotation.Retention; -import java.lang.annotation.Target; - -import static java.lang.annotation.ElementType.ANNOTATION_TYPE; -import static java.lang.annotation.ElementType.PARAMETER; -import static java.lang.annotation.RetentionPolicy.RUNTIME; - -/** - * A custom object with parameters that will be applied to the {@link RequestTemplate} - * via a custom {@link ParamEncoder} class. - */ -@Retention(RUNTIME) -@Target({PARAMETER, ANNOTATION_TYPE}) -public @interface CustomParam { - - /** - * How to encode this parameter (must be supplied) - */ - Class encoder(); - - /** - * An encoder for a {@link CustomParam} annotated parameter. - * Must have a public 0-arg constructor. - */ - interface ParamEncoder { - - /** - * Converts objects to appropriate representations in the template. - * - * @param object what to encode. - * @param template the request template to populate. - */ - void encode (Object object, RequestTemplate template); - } -} diff --git a/core/src/main/java/feign/MethodMetadata.java b/core/src/main/java/feign/MethodMetadata.java index 792f75c2ef..431b3f86c6 100644 --- a/core/src/main/java/feign/MethodMetadata.java +++ b/core/src/main/java/feign/MethodMetadata.java @@ -41,8 +41,6 @@ public final class MethodMetadata implements Serializable { private Map> indexToExpanderClass = new LinkedHashMap>(); private Map indexToEncoded = new LinkedHashMap(); - private Map> indexToCustomEncoderClass = - new LinkedHashMap>(); private transient Map indexToExpander; MethodMetadata() { @@ -108,16 +106,6 @@ public MethodMetadata queryMapIndex(Integer queryMapIndex) { return this; } - public Map> indexToCustomEncoderClass () { - return indexToCustomEncoderClass; - - } - - public MethodMetadata indexToCustomEncoderClass (Map> indexToCustomEncoderClass) { - this.indexToCustomEncoderClass = indexToCustomEncoderClass; - return this; - } - public boolean queryMapEncoded() { return queryMapEncoded; } diff --git a/core/src/main/java/feign/ObjectParamMetadata.java b/core/src/main/java/feign/ObjectParamMetadata.java new file mode 100644 index 0000000000..62252ac569 --- /dev/null +++ b/core/src/main/java/feign/ObjectParamMetadata.java @@ -0,0 +1,64 @@ +/** + * Copyright 2012-2018 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; + +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +class ObjectParamMetadata { + + private final static ConcurrentMap, ObjectParamMetadata> classToMetadata = + new ConcurrentHashMap, ObjectParamMetadata>(); + + private final List objectFields; + + private ObjectParamMetadata (List objectFields) { + this.objectFields = Collections.unmodifiableList(objectFields); + } + + Map toQueryMap(Object object) throws IllegalAccessException { + Map fieldNameToValue = new LinkedHashMap(); + for (Field field : objectFields) { + Object value = field.get(object); + fieldNameToValue.put(field.getName(), value); + } + return fieldNameToValue; + } + + static ObjectParamMetadata getMetadata(Class objectType) { + ObjectParamMetadata metadata = classToMetadata.get(objectType); + if (metadata == null) { + metadata = parseObjectType(objectType); + classToMetadata.putIfAbsent(objectType, metadata); + } + return metadata; + } + + private static ObjectParamMetadata parseObjectType(Class type) { + List fields = new ArrayList(); + for (Field field : type.getDeclaredFields()) { + if (!field.isAccessible()) { + field.setAccessible(true); + } + fields.add(field); + } + return new ObjectParamMetadata(fields); + } +} diff --git a/core/src/main/java/feign/ReflectiveFeign.java b/core/src/main/java/feign/ReflectiveFeign.java index 69c11c0b9c..f101206f15 100644 --- a/core/src/main/java/feign/ReflectiveFeign.java +++ b/core/src/main/java/feign/ReflectiveFeign.java @@ -211,28 +211,30 @@ public RequestTemplate create(Object[] argv) { if (metadata.queryMapIndex() != null) { // add query map parameters after initial resolve so that they take // precedence over any predefined values - template = addQueryMapQueryParameters((Map) argv[metadata.queryMapIndex()], template); + boolean encoded = metadata.queryMapEncoded(); + Object value = argv[metadata.queryMapIndex()]; + Map queryMap = toQueryMap(value); + template = addQueryMapQueryParameters(queryMap, template); } if (metadata.headerMapIndex() != null) { template = addHeaderMapHeaders((Map) argv[metadata.headerMapIndex()], template); } - if (metadata.indexToCustomEncoderClass() != null) { - for (Map.Entry> entry : metadata.indexToCustomEncoderClass().entrySet()) { - try { - entry.getValue().newInstance().encode(argv[entry.getKey()], template); - } catch (InstantiationException e) { - throw new IllegalStateException(e); - } catch (IllegalAccessException e) { - throw new IllegalStateException(e); - } - } - } - return template; } + private Map toQueryMap (Object value) { + if (value instanceof Map) { + return (Map)value; + } + try { + return ObjectParamMetadata.getMetadata(value.getClass()).toQueryMap(value); + } catch (IllegalAccessException e) { + throw new IllegalStateException(e); + } + } + private Object expandElements(Expander expander, Object value) { if (value instanceof Iterable) { return expandIterable(expander, (Iterable) value); @@ -242,7 +244,7 @@ private Object expandElements(Expander expander, Object value) { private List expandIterable(Expander expander, Iterable value) { List values = new ArrayList(); - for (Object element : (Iterable) value) { + for (Object element : value) { if (element!=null) { values.add(expander.expand(element)); } diff --git a/core/src/test/java/feign/DefaultContractTest.java b/core/src/test/java/feign/DefaultContractTest.java index a896bf67b4..bb53ec90a0 100644 --- a/core/src/test/java/feign/DefaultContractTest.java +++ b/core/src/test/java/feign/DefaultContractTest.java @@ -15,10 +15,6 @@ import com.google.gson.reflect.TypeToken; -import feign.CustomParam.ParamEncoder; -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; import org.assertj.core.api.Fail; import org.junit.Rule; import org.junit.Test; @@ -310,29 +306,6 @@ public void queryMapMapSubclass() throws Exception { assertThat(md.queryMapIndex()).isEqualTo(0); } - @Test - public void customParamObject() throws Exception { - MethodMetadata md = parseAndValidateMetadata(CustomParamObjectInterface.class, "customObject", CustomObject.class); - - assertThat(md.indexToCustomEncoderClass()).containsOnly(entry(0, CustomObjectParamEncoder.class)); - } - - @Test - public void customParamObjectMultiple() throws Exception { - MethodMetadata md = parseAndValidateMetadata(MultipleCustomParamObjectInterface.class, "customObjects", CustomObject.class, CustomObject.class); - - assertThat(md.indexToCustomEncoderClass()).contains( - entry(0, CustomObjectParamEncoder.class), - entry(1, CustomObjectParamEncoder.class)); - } - - @Test - public void customParamObjectInheritedAnnotation() throws Exception { - MethodMetadata md = parseAndValidateMetadata(InheritedCustomParamObjectInterface.class, "customObject", CustomObject.class); - - assertThat(md.indexToCustomEncoderClass()).containsOnly(entry(0, CustomObjectParamEncoder.class)); - } - @Test public void onlyOneQueryMapAnnotationPermitted() throws Exception { try { @@ -343,16 +316,6 @@ public void onlyOneQueryMapAnnotationPermitted() throws Exception { } } - @Test - public void queryMapMustBeInstanceOfMap() throws Exception { - try { - parseAndValidateMetadata(QueryMapTestInterface.class, "nonMapQueryMap", String.class); - Fail.failBecauseExceptionWasNotThrown(IllegalStateException.class); - } catch (IllegalStateException ex) { - assertThat(ex).hasMessage("QueryMap parameter must be a Map: class java.lang.String"); - } - } - @Test public void queryMapKeysMustBeStrings() throws Exception { try { @@ -363,6 +326,29 @@ public void queryMapKeysMustBeStrings() throws Exception { } } + @Test + public void queryMapPojoObject() throws Exception { + MethodMetadata md = parseAndValidateMetadata(QueryMapTestInterface.class, "pojoObject", Object.class); + + assertThat(md.queryMapIndex()).isEqualTo(0); + } + + @Test + public void queryMapPojoObjectEncoded() throws Exception { + MethodMetadata md = parseAndValidateMetadata(QueryMapTestInterface.class, "pojoObjectEncoded", Object.class); + + assertThat(md.queryMapIndex()).isEqualTo(0); + assertThat(md.queryMapEncoded()).isTrue(); + } + + @Test + public void queryMapPojoObjectNotEncoded() throws Exception { + MethodMetadata md = parseAndValidateMetadata(QueryMapTestInterface.class, "pojoObjectNotEncoded", Object.class); + + assertThat(md.queryMapIndex()).isEqualTo(0); + assertThat(md.queryMapEncoded()).isFalse(); + } + @Test public void slashAreEncodedWhenNeeded() throws Exception { MethodMetadata md = parseAndValidateMetadata(SlashNeedToBeEncoded.class, @@ -500,52 +486,6 @@ interface HeaderParamsNotAtStart { void logout(@Param("authToken") String token); } - interface CustomParamObjectInterface { - - @RequestLine("POST /") - void customObject(@CustomParam(encoder = CustomObjectParamEncoder.class) CustomObject object); - } - - @Retention(RetentionPolicy.RUNTIME) - @java.lang.annotation.Target(ElementType.PARAMETER) - @CustomParam(encoder = CustomObjectParamEncoder.class) - @interface InheritedCustomParam { - } - - interface InheritedCustomParamObjectInterface { - - @RequestLine("POST /") - void customObject(@InheritedCustomParam CustomObject object); - } - - interface MultipleCustomParamObjectInterface { - - @RequestLine("POST /") - void customObjects( - @CustomParam(encoder = CustomObjectParamEncoder.class) CustomObject object1, - @CustomParam(encoder = CustomObjectParamEncoder.class) CustomObject object2); - } - - class CustomObject { - - String name; - String address; - } - - class CustomObjectParamEncoder implements ParamEncoder { - - @Override - public void encode (Object object, RequestTemplate template) { - CustomObject customObject = (CustomObject)object; - if (customObject.name != null) { - template.query("name", customObject.name); - } - if (customObject.address != null) { - template.query("address", ((CustomObject) object).address); - } - } - } - interface CustomExpander { @RequestLine("POST /?date={date}") @@ -574,6 +514,15 @@ interface QueryMapTestInterface { @RequestLine("POST /") void queryMapNotEncoded(@QueryMap(encoded = false) Map queryMap); + @RequestLine("POST /") + void pojoObject(@QueryMap Object object); + + @RequestLine("POST /") + void pojoObjectEncoded(@QueryMap(encoded = true) Object object); + + @RequestLine("POST /") + void pojoObjectNotEncoded(@QueryMap(encoded = false) Object object); + // invalid @RequestLine("POST /") void multipleQueryMap(@QueryMap Map mapOne, @QueryMap Map mapTwo); diff --git a/core/src/test/java/feign/FeignTest.java b/core/src/test/java/feign/FeignTest.java index 7b80df166b..0ee89317ef 100644 --- a/core/src/test/java/feign/FeignTest.java +++ b/core/src/test/java/feign/FeignTest.java @@ -25,7 +25,6 @@ import java.util.HashMap; import java.util.LinkedHashMap; import okio.Buffer; -import org.assertj.core.api.Fail; import org.assertj.core.data.MapEntry; import org.junit.Rule; import org.junit.Test; @@ -385,71 +384,47 @@ public void queryMapValueStartingWithBrace() throws Exception { } @Test - public void customParamObjectWithExplicitParams() throws Exception { + public void customParamPojoWithImplicitParams() throws Exception { TestInterface api = new TestInterfaceBuilder().target("http://localhost:" + server.getPort()); - CustomObject customObject = new CustomObject(); - customObject.name = "Name"; - customObject.number = 3; + CustomPojo customPojo = new CustomPojo(); + customPojo.name = "Name"; + customPojo.number = 3; server.enqueue(new MockResponse()); - api.customObjectWithExplicitParams(customObject); - assertThat(server.takeRequest()) - .hasPath("/?name=Name&number=3"); - } - - @Test - public void customParamObjectWithImplicitParams() throws Exception { - TestInterface api = new TestInterfaceBuilder().target("http://localhost:" + server.getPort()); - - CustomObject customObject = new CustomObject(); - customObject.name = "Name"; - customObject.number = 3; - - server.enqueue(new MockResponse()); - api.customObjectWithImpliedParams(customObject); + api.customPojoWithImpliedParams(customPojo); assertThat(server.takeRequest()) .hasPath("/?name=Name&number=3"); } @Test - public void customParamObjectWithPartialParams() throws Exception { + public void customParamPojoWithPartialParams() throws Exception { TestInterface api = new TestInterfaceBuilder().target("http://localhost:" + server.getPort()); - CustomObject customObject = new CustomObject(); - customObject.name = "Name"; - customObject.number = null; + CustomPojo customPojo = new CustomPojo(); + customPojo.name = "Name"; + customPojo.number = null; server.enqueue(new MockResponse()); - api.customObjectWithImpliedParams(customObject); + api.customPojoWithImpliedParams(customPojo); assertThat(server.takeRequest()) .hasPath("/?name=Name"); } @Test - public void customParamObjectWithEmptyParams() throws Exception { + public void customParamPojoWithEmptyParams() throws Exception { TestInterface api = new TestInterfaceBuilder().target("http://localhost:" + server.getPort()); - CustomObject customObject = new CustomObject(); - customObject.name = null; - customObject.number = null; + CustomPojo customPojo = new CustomPojo(); + customPojo.name = null; + customPojo.number = null; server.enqueue(new MockResponse()); - api.customObjectWithImpliedParams(customObject); + api.customPojoWithImpliedParams(customPojo); assertThat(server.takeRequest()) .hasPath("/"); } - @Test - public void customParamWithCustomEncoder() throws Exception { - TestInterface api = new TestInterfaceBuilder().target("http://localhost:" + server.getPort()); - - server.enqueue(new MockResponse()); - api.customObjectWithHeaderParamEncoder("header value"); - assertThat(server.takeRequest()) - .hasHeaders(MapEntry.entry(HeaderGeneratingEncoder.HEADER_NAME, Arrays.asList("header value"))); - } - @Test public void configKeyFormatsAsExpected() throws Exception { assertEquals("TestInterface#post()", @@ -862,14 +837,8 @@ void form( @RequestLine("GET /?trim={trim}") void encodedQueryParam(@Param(value = "trim", encoded = true) String trim); - @RequestLine("GET /?name={name}&number={number}") - void customObjectWithExplicitParams(@CustomParam(encoder = CustomObjectParamEncoder.class) CustomObject object); - @RequestLine("GET /") - void customObjectWithImpliedParams(@CustomParam(encoder = CustomObjectParamEncoder.class) CustomObject object); - - @RequestLine("GET /") - void customObjectWithHeaderParamEncoder(@CustomParam(encoder = HeaderGeneratingEncoder.class) String value); + void customPojoWithImpliedParams(@QueryMap CustomPojo object); class DateToMillis implements Param.Expander { @@ -880,36 +849,12 @@ public String expand(Object value) { } } - static class CustomObject { + static class CustomPojo { String name; Integer number; } - static class CustomObjectParamEncoder implements CustomParam.ParamEncoder { - - @Override - public void encode (Object object, RequestTemplate template) { - CustomObject customObject = (CustomObject)object; - if (customObject.name != null) { - template.query("name", customObject.name); - } - if (customObject.number != null) { - template.query("number", customObject.number.toString()); - } - } - } - - static class HeaderGeneratingEncoder implements CustomParam.ParamEncoder { - - final static String HEADER_NAME = "X-Generated-Header"; - - @Override - public void encode (Object object, RequestTemplate template) { - template.header(HEADER_NAME, (String)object); - } - } - interface OtherTestInterface { @RequestLine("POST /") From 4867c11f2ff10053a11e6add43eafc86e3b77a01 Mon Sep 17 00:00:00 2001 From: Ralph Jennings Date: Mon, 2 Apr 2018 18:12:48 -0700 Subject: [PATCH 05/13] Clarification in README of QueryMap POJO usage --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 255088817b..11c7b6fd77 100644 --- a/README.md +++ b/README.md @@ -450,7 +450,7 @@ This may also be used to generate the query parameters from a POJO object. V find(@QueryMap CustomPojo customPojo); ``` -When used in this manner, the query map will be generated using member variable names as query parameter names. The following POJO will generate query params of "name={name}&number={number}" +When used in this manner, the query map will be generated using member variable names as query parameter names. The following POJO will generate query params of "/find?name={name}&number={number}" (as usual, if any value is null, it will be left out). ```java public class CustomPojo { From 54a0e9e33fbcff0edca9eac0f7126cac1d7c3a6b Mon Sep 17 00:00:00 2001 From: Ralph Jennings Date: Mon, 2 Apr 2018 18:16:20 -0700 Subject: [PATCH 06/13] Removed unused line --- core/src/main/java/feign/ReflectiveFeign.java | 1 - 1 file changed, 1 deletion(-) diff --git a/core/src/main/java/feign/ReflectiveFeign.java b/core/src/main/java/feign/ReflectiveFeign.java index f101206f15..7ceaabb624 100644 --- a/core/src/main/java/feign/ReflectiveFeign.java +++ b/core/src/main/java/feign/ReflectiveFeign.java @@ -211,7 +211,6 @@ public RequestTemplate create(Object[] argv) { if (metadata.queryMapIndex() != null) { // add query map parameters after initial resolve so that they take // precedence over any predefined values - boolean encoded = metadata.queryMapEncoded(); Object value = argv[metadata.queryMapIndex()]; Map queryMap = toQueryMap(value); template = addQueryMapQueryParameters(queryMap, template); From dde5e10201bb165799f21db471a553e4f91468fb Mon Sep 17 00:00:00 2001 From: Ralph Jennings Date: Mon, 2 Apr 2018 18:29:08 -0700 Subject: [PATCH 07/13] Updated custom POJO QueryMap test to prove that private fields can be used --- core/src/test/java/feign/CustomPojo.java | 25 ++++++++++++++++++++++++ core/src/test/java/feign/FeignTest.java | 18 +++-------------- 2 files changed, 28 insertions(+), 15 deletions(-) create mode 100644 core/src/test/java/feign/CustomPojo.java diff --git a/core/src/test/java/feign/CustomPojo.java b/core/src/test/java/feign/CustomPojo.java new file mode 100644 index 0000000000..8a162afbeb --- /dev/null +++ b/core/src/test/java/feign/CustomPojo.java @@ -0,0 +1,25 @@ +/** + * Copyright 2012-2018 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; + +public class CustomPojo { + + private final String name; + private final Integer number; + + CustomPojo(String name, Integer number) { + this.name = name; + this.number = number; + } +} diff --git a/core/src/test/java/feign/FeignTest.java b/core/src/test/java/feign/FeignTest.java index 0ee89317ef..a201069640 100644 --- a/core/src/test/java/feign/FeignTest.java +++ b/core/src/test/java/feign/FeignTest.java @@ -387,9 +387,7 @@ public void queryMapValueStartingWithBrace() throws Exception { public void customParamPojoWithImplicitParams() throws Exception { TestInterface api = new TestInterfaceBuilder().target("http://localhost:" + server.getPort()); - CustomPojo customPojo = new CustomPojo(); - customPojo.name = "Name"; - customPojo.number = 3; + CustomPojo customPojo = new CustomPojo("Name", 3); server.enqueue(new MockResponse()); api.customPojoWithImpliedParams(customPojo); @@ -401,9 +399,7 @@ public void customParamPojoWithImplicitParams() throws Exception { public void customParamPojoWithPartialParams() throws Exception { TestInterface api = new TestInterfaceBuilder().target("http://localhost:" + server.getPort()); - CustomPojo customPojo = new CustomPojo(); - customPojo.name = "Name"; - customPojo.number = null; + CustomPojo customPojo = new CustomPojo("Name", null); server.enqueue(new MockResponse()); api.customPojoWithImpliedParams(customPojo); @@ -415,9 +411,7 @@ public void customParamPojoWithPartialParams() throws Exception { public void customParamPojoWithEmptyParams() throws Exception { TestInterface api = new TestInterfaceBuilder().target("http://localhost:" + server.getPort()); - CustomPojo customPojo = new CustomPojo(); - customPojo.name = null; - customPojo.number = null; + CustomPojo customPojo = new CustomPojo(null, null); server.enqueue(new MockResponse()); api.customPojoWithImpliedParams(customPojo); @@ -849,12 +843,6 @@ public String expand(Object value) { } } - static class CustomPojo { - - String name; - Integer number; - } - interface OtherTestInterface { @RequestLine("POST /") From 7d72d228e858af0491e87e8735b2783a24e2b1f9 Mon Sep 17 00:00:00 2001 From: Ralph Jennings Date: Mon, 2 Apr 2018 18:58:38 -0700 Subject: [PATCH 08/13] Removed no-longer-valid test endpoint --- core/src/test/java/feign/DefaultContractTest.java | 4 ---- 1 file changed, 4 deletions(-) diff --git a/core/src/test/java/feign/DefaultContractTest.java b/core/src/test/java/feign/DefaultContractTest.java index bb53ec90a0..556c9edfe6 100644 --- a/core/src/test/java/feign/DefaultContractTest.java +++ b/core/src/test/java/feign/DefaultContractTest.java @@ -527,10 +527,6 @@ interface QueryMapTestInterface { @RequestLine("POST /") void multipleQueryMap(@QueryMap Map mapOne, @QueryMap Map mapTwo); - // invalid - @RequestLine("POST /") - void nonMapQueryMap(@QueryMap String notAMap); - // invalid @RequestLine("POST /") void nonStringKeyQueryMap(@QueryMap Map queryMap); From a44c472b2502a8760554ea43e14c7bd6dba64a90 Mon Sep 17 00:00:00 2001 From: Ralph Jennings Date: Mon, 2 Apr 2018 19:04:19 -0700 Subject: [PATCH 09/13] Renamed tests to more accurately reflect their contents --- core/src/test/java/feign/FeignTest.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/core/src/test/java/feign/FeignTest.java b/core/src/test/java/feign/FeignTest.java index a201069640..309066c191 100644 --- a/core/src/test/java/feign/FeignTest.java +++ b/core/src/test/java/feign/FeignTest.java @@ -384,7 +384,7 @@ public void queryMapValueStartingWithBrace() throws Exception { } @Test - public void customParamPojoWithImplicitParams() throws Exception { + public void queryMapPojoWithFullParams() throws Exception { TestInterface api = new TestInterfaceBuilder().target("http://localhost:" + server.getPort()); CustomPojo customPojo = new CustomPojo("Name", 3); @@ -396,7 +396,7 @@ public void customParamPojoWithImplicitParams() throws Exception { } @Test - public void customParamPojoWithPartialParams() throws Exception { + public void queryMapPojoWithPartialParams() throws Exception { TestInterface api = new TestInterfaceBuilder().target("http://localhost:" + server.getPort()); CustomPojo customPojo = new CustomPojo("Name", null); @@ -408,7 +408,7 @@ public void customParamPojoWithPartialParams() throws Exception { } @Test - public void customParamPojoWithEmptyParams() throws Exception { + public void queryMapPojoWithEmptyParams() throws Exception { TestInterface api = new TestInterfaceBuilder().target("http://localhost:" + server.getPort()); CustomPojo customPojo = new CustomPojo(null, null); From 336f6bff6bb2ab512cdc1b03ae4999a05b6b49dc Mon Sep 17 00:00:00 2001 From: Ralph Jennings Date: Mon, 2 Apr 2018 19:16:46 -0700 Subject: [PATCH 10/13] More test cleanup --- core/src/test/java/feign/FeignTest.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/core/src/test/java/feign/FeignTest.java b/core/src/test/java/feign/FeignTest.java index 309066c191..2ab91b8ddd 100644 --- a/core/src/test/java/feign/FeignTest.java +++ b/core/src/test/java/feign/FeignTest.java @@ -390,7 +390,7 @@ public void queryMapPojoWithFullParams() throws Exception { CustomPojo customPojo = new CustomPojo("Name", 3); server.enqueue(new MockResponse()); - api.customPojoWithImpliedParams(customPojo); + api.queryMapPojo(customPojo); assertThat(server.takeRequest()) .hasPath("/?name=Name&number=3"); } @@ -402,7 +402,7 @@ public void queryMapPojoWithPartialParams() throws Exception { CustomPojo customPojo = new CustomPojo("Name", null); server.enqueue(new MockResponse()); - api.customPojoWithImpliedParams(customPojo); + api.queryMapPojo(customPojo); assertThat(server.takeRequest()) .hasPath("/?name=Name"); } @@ -414,7 +414,7 @@ public void queryMapPojoWithEmptyParams() throws Exception { CustomPojo customPojo = new CustomPojo(null, null); server.enqueue(new MockResponse()); - api.customPojoWithImpliedParams(customPojo); + api.queryMapPojo(customPojo); assertThat(server.takeRequest()) .hasPath("/"); } @@ -832,7 +832,7 @@ void form( void encodedQueryParam(@Param(value = "trim", encoded = true) String trim); @RequestLine("GET /") - void customPojoWithImpliedParams(@QueryMap CustomPojo object); + void queryMapPojo(@QueryMap CustomPojo object); class DateToMillis implements Param.Expander { From 1c3f822459db1da1bcc46148c46d755f72e9d6a7 Mon Sep 17 00:00:00 2001 From: Ralph Jennings Date: Thu, 19 Apr 2018 14:58:22 -0700 Subject: [PATCH 11/13] Modified QueryMap POJO encoding to use specified QueryMapEncoder (default implementation provided) --- README.md | 12 ++- core/src/main/java/feign/Feign.java | 10 ++- .../main/java/feign/ObjectParamMetadata.java | 64 -------------- core/src/main/java/feign/QueryMapEncoder.java | 88 +++++++++++++++++++ core/src/main/java/feign/ReflectiveFeign.java | 39 +++++--- .../feign/DefaultQueryMapEncoderTest.java | 78 ++++++++++++++++ .../src/test/java/feign/FeignBuilderTest.java | 26 ++++++ core/src/test/java/feign/FeignTest.java | 2 +- .../java/feign/QueryMapEncoderObject.java | 24 +++++ .../feign/assertj/RecordedRequestAssert.java | 26 ++++++ 10 files changed, 287 insertions(+), 82 deletions(-) delete mode 100644 core/src/main/java/feign/ObjectParamMetadata.java create mode 100644 core/src/main/java/feign/QueryMapEncoder.java create mode 100644 core/src/test/java/feign/DefaultQueryMapEncoderTest.java create mode 100644 core/src/test/java/feign/QueryMapEncoderObject.java diff --git a/README.md b/README.md index 11c7b6fd77..e1884e83ca 100644 --- a/README.md +++ b/README.md @@ -443,14 +443,14 @@ A Map parameter can be annotated with `QueryMap` to construct a query that uses V find(@QueryMap Map queryMap); ``` -This may also be used to generate the query parameters from a POJO object. +This may also be used to generate the query parameters from a POJO object using a `QueryParamEncoder`. ```java @RequestLine("GET /find") V find(@QueryMap CustomPojo customPojo); ``` -When used in this manner, the query map will be generated using member variable names as query parameter names. The following POJO will generate query params of "/find?name={name}&number={number}" (as usual, if any value is null, it will be left out). +When used in this manner, without specifying a custom `QueryParamEncoder`, the query map will be generated using member variable names as query parameter names. The following POJO will generate query params of "/find?name={name}&number={number}" (order of included query parameters not guaranteed, and as usual, if any value is null, it will be left out). ```java public class CustomPojo { @@ -464,6 +464,14 @@ public class CustomPojo { } ``` +To setup a custom `QueryParamEncoder`: + +```java +MyApi myApi = Feign.builder() + .queryParamEncoder(new MyCustomQueryParamEncoder()) + .target(MyApi.class, "https://api.hostname.com"); +``` + #### Static and Default Methods Interfaces targeted by Feign may have static or default methods (if using Java 8+). These allows Feign clients to contain logic that is not expressly defined by the underlying API. diff --git a/core/src/main/java/feign/Feign.java b/core/src/main/java/feign/Feign.java index b07369d31f..6d60f1b617 100644 --- a/core/src/main/java/feign/Feign.java +++ b/core/src/main/java/feign/Feign.java @@ -101,6 +101,7 @@ public static class Builder { private Logger logger = new NoOpLogger(); private Encoder encoder = new Encoder.Default(); private Decoder decoder = new Decoder.Default(); + private QueryMapEncoder queryMapEncoder = new QueryMapEncoder.Default(); private ErrorDecoder errorDecoder = new ErrorDecoder.Default(); private Options options = new Options(); private InvocationHandlerFactory invocationHandlerFactory = @@ -143,6 +144,11 @@ public Builder decoder(Decoder decoder) { return this; } + public Builder queryMapEncoder(QueryMapEncoder queryMapEncoder) { + this.queryMapEncoder = queryMapEncoder; + return this; + } + /** * Allows to map the response before passing it to the decoder. */ @@ -241,9 +247,9 @@ public Feign build() { new SynchronousMethodHandler.Factory(client, retryer, requestInterceptors, logger, logLevel, decode404, closeAfterDecode); ParseHandlersByName handlersByName = - new ParseHandlersByName(contract, options, encoder, decoder, + new ParseHandlersByName(contract, options, encoder, decoder, queryMapEncoder, errorDecoder, synchronousMethodHandlerFactory); - return new ReflectiveFeign(handlersByName, invocationHandlerFactory); + return new ReflectiveFeign(handlersByName, invocationHandlerFactory, queryMapEncoder); } } diff --git a/core/src/main/java/feign/ObjectParamMetadata.java b/core/src/main/java/feign/ObjectParamMetadata.java deleted file mode 100644 index 62252ac569..0000000000 --- a/core/src/main/java/feign/ObjectParamMetadata.java +++ /dev/null @@ -1,64 +0,0 @@ -/** - * Copyright 2012-2018 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; - -import java.lang.reflect.Field; -import java.util.ArrayList; -import java.util.Collections; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentMap; - -class ObjectParamMetadata { - - private final static ConcurrentMap, ObjectParamMetadata> classToMetadata = - new ConcurrentHashMap, ObjectParamMetadata>(); - - private final List objectFields; - - private ObjectParamMetadata (List objectFields) { - this.objectFields = Collections.unmodifiableList(objectFields); - } - - Map toQueryMap(Object object) throws IllegalAccessException { - Map fieldNameToValue = new LinkedHashMap(); - for (Field field : objectFields) { - Object value = field.get(object); - fieldNameToValue.put(field.getName(), value); - } - return fieldNameToValue; - } - - static ObjectParamMetadata getMetadata(Class objectType) { - ObjectParamMetadata metadata = classToMetadata.get(objectType); - if (metadata == null) { - metadata = parseObjectType(objectType); - classToMetadata.putIfAbsent(objectType, metadata); - } - return metadata; - } - - private static ObjectParamMetadata parseObjectType(Class type) { - List fields = new ArrayList(); - for (Field field : type.getDeclaredFields()) { - if (!field.isAccessible()) { - field.setAccessible(true); - } - fields.add(field); - } - return new ObjectParamMetadata(fields); - } -} diff --git a/core/src/main/java/feign/QueryMapEncoder.java b/core/src/main/java/feign/QueryMapEncoder.java new file mode 100644 index 0000000000..b6909823b4 --- /dev/null +++ b/core/src/main/java/feign/QueryMapEncoder.java @@ -0,0 +1,88 @@ +/** + * Copyright 2012-2018 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; + +import feign.codec.EncodeException; +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * A QueryMapEncoder encodes Objects into maps of query parameter names to values. + */ +public interface QueryMapEncoder { + + /** + * Encodes the given object into a query map. + * + * @param object the object to encode + * @return the map represented by the object + */ + Map encode (Object object); + + class Default implements QueryMapEncoder { + + private final Map, ObjectParamMetadata> classToMetadata = + new HashMap, ObjectParamMetadata>(); + + @Override + public Map encode (Object object) throws EncodeException { + try { + ObjectParamMetadata metadata = getMetadata(object.getClass()); + Map fieldNameToValue = new HashMap(); + for (Field field : metadata.objectFields) { + Object value = field.get(object); + if (value != null && value != object) { + fieldNameToValue.put(field.getName(), value); + } + } + return fieldNameToValue; + } catch (IllegalAccessException e) { + throw new EncodeException("Failure encoding object into query map", e); + } + } + + private ObjectParamMetadata getMetadata(Class objectType) { + ObjectParamMetadata metadata = classToMetadata.get(objectType); + if (metadata == null) { + metadata = ObjectParamMetadata.parseObjectType(objectType); + classToMetadata.put(objectType, metadata); + } + return metadata; + } + + private static class ObjectParamMetadata { + + private final List objectFields; + + private ObjectParamMetadata (List objectFields) { + this.objectFields = Collections.unmodifiableList(objectFields); + } + + private static ObjectParamMetadata parseObjectType(Class type) { + List fields = new ArrayList(); + for (Field field : type.getDeclaredFields()) { + if (!field.isAccessible()) { + field.setAccessible(true); + } + fields.add(field); + } + return new ObjectParamMetadata(fields); + } + } + } +} diff --git a/core/src/main/java/feign/ReflectiveFeign.java b/core/src/main/java/feign/ReflectiveFeign.java index 7ceaabb624..91332e8671 100644 --- a/core/src/main/java/feign/ReflectiveFeign.java +++ b/core/src/main/java/feign/ReflectiveFeign.java @@ -34,10 +34,12 @@ public class ReflectiveFeign extends Feign { private final ParseHandlersByName targetToHandlersByName; private final InvocationHandlerFactory factory; + private final QueryMapEncoder queryMapEncoder; - ReflectiveFeign(ParseHandlersByName targetToHandlersByName, InvocationHandlerFactory factory) { + ReflectiveFeign(ParseHandlersByName targetToHandlersByName, InvocationHandlerFactory factory, QueryMapEncoder queryMapEncoder) { this.targetToHandlersByName = targetToHandlersByName; this.factory = factory; + this.queryMapEncoder = queryMapEncoder; } /** @@ -127,14 +129,22 @@ static final class ParseHandlersByName { private final Encoder encoder; private final Decoder decoder; private final ErrorDecoder errorDecoder; + private final QueryMapEncoder queryMapEncoder; private final SynchronousMethodHandler.Factory factory; - ParseHandlersByName(Contract contract, Options options, Encoder encoder, Decoder decoder, - ErrorDecoder errorDecoder, SynchronousMethodHandler.Factory factory) { + ParseHandlersByName( + Contract contract, + Options options, + Encoder encoder, + Decoder decoder, + QueryMapEncoder queryMapEncoder, + ErrorDecoder errorDecoder, + SynchronousMethodHandler.Factory factory) { this.contract = contract; this.options = options; this.factory = factory; this.errorDecoder = errorDecoder; + this.queryMapEncoder = queryMapEncoder; this.encoder = checkNotNull(encoder, "encoder"); this.decoder = checkNotNull(decoder, "decoder"); } @@ -145,11 +155,11 @@ public Map apply(Target key) { for (MethodMetadata md : metadata) { BuildTemplateByResolvingArgs buildTemplate; if (!md.formParams().isEmpty() && md.template().bodyTemplate() == null) { - buildTemplate = new BuildFormEncodedTemplateFromArgs(md, encoder); + buildTemplate = new BuildFormEncodedTemplateFromArgs(md, encoder, queryMapEncoder); } else if (md.bodyIndex() != null) { - buildTemplate = new BuildEncodedTemplateFromArgs(md, encoder); + buildTemplate = new BuildEncodedTemplateFromArgs(md, encoder, queryMapEncoder); } else { - buildTemplate = new BuildTemplateByResolvingArgs(md); + buildTemplate = new BuildTemplateByResolvingArgs(md, queryMapEncoder); } result.put(md.configKey(), factory.create(key, md, buildTemplate, options, decoder, errorDecoder)); @@ -160,11 +170,14 @@ public Map apply(Target key) { private static class BuildTemplateByResolvingArgs implements RequestTemplate.Factory { + private final QueryMapEncoder queryMapEncoder; + protected final MethodMetadata metadata; private final Map indexToExpander = new LinkedHashMap(); - private BuildTemplateByResolvingArgs(MethodMetadata metadata) { + private BuildTemplateByResolvingArgs(MethodMetadata metadata, QueryMapEncoder queryMapEncoder) { this.metadata = metadata; + this.queryMapEncoder = queryMapEncoder; if (metadata.indexToExpander() != null) { indexToExpander.putAll(metadata.indexToExpander()); return; @@ -228,8 +241,8 @@ private Map toQueryMap (Object value) { return (Map)value; } try { - return ObjectParamMetadata.getMetadata(value.getClass()).toQueryMap(value); - } catch (IllegalAccessException e) { + return queryMapEncoder.encode(value); + } catch (EncodeException e) { throw new IllegalStateException(e); } } @@ -312,8 +325,8 @@ private static class BuildFormEncodedTemplateFromArgs extends BuildTemplateByRes private final Encoder encoder; - private BuildFormEncodedTemplateFromArgs(MethodMetadata metadata, Encoder encoder) { - super(metadata); + private BuildFormEncodedTemplateFromArgs(MethodMetadata metadata, Encoder encoder, QueryMapEncoder queryMapEncoder) { + super(metadata, queryMapEncoder); this.encoder = encoder; } @@ -341,8 +354,8 @@ private static class BuildEncodedTemplateFromArgs extends BuildTemplateByResolvi private final Encoder encoder; - private BuildEncodedTemplateFromArgs(MethodMetadata metadata, Encoder encoder) { - super(metadata); + private BuildEncodedTemplateFromArgs(MethodMetadata metadata, Encoder encoder, QueryMapEncoder queryMapEncoder) { + super(metadata, queryMapEncoder); this.encoder = encoder; } diff --git a/core/src/test/java/feign/DefaultQueryMapEncoderTest.java b/core/src/test/java/feign/DefaultQueryMapEncoderTest.java new file mode 100644 index 0000000000..63df04da72 --- /dev/null +++ b/core/src/test/java/feign/DefaultQueryMapEncoderTest.java @@ -0,0 +1,78 @@ +/** + * Copyright 2012-2018 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; + +import java.util.HashMap; +import java.util.Map; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +public class DefaultQueryMapEncoderTest { + + @Rule + public final ExpectedException thrown = ExpectedException.none(); + + private final QueryMapEncoder encoder = new QueryMapEncoder.Default(); + + @Test + public void testEncodesObject_visibleFields() { + Map expected = new HashMap<>(); + expected.put("foo", "fooz"); + expected.put("bar", "barz"); + expected.put("baz", "bazz"); + VisibleFieldsObject object = new VisibleFieldsObject(); + object.foo = "fooz"; + object.bar = "barz"; + object.baz = "bazz"; + + Map encodedMap = encoder.encode(object); + assertEquals("Unexpected encoded query map", expected, encodedMap); + } + + @Test + public void testEncodesObject_visibleFields_emptyObject() { + VisibleFieldsObject object = new VisibleFieldsObject(); + Map encodedMap = encoder.encode(object); + assertTrue("Non-empty map generated from null fields: " + encodedMap, encodedMap.isEmpty()); + } + + @Test + public void testEncodesObject_nonVisibleFields() { + Map expected = new HashMap<>(); + expected.put("foo", "fooz"); + expected.put("bar", "barz"); + QueryMapEncoderObject object = new QueryMapEncoderObject("fooz", "barz"); + + Map encodedMap = encoder.encode(object); + assertEquals("Unexpected encoded query map", expected, encodedMap); + } + + @Test + public void testEncodesObject_nonVisibleFields_emptyObject() { + QueryMapEncoderObject object = new QueryMapEncoderObject(null, null); + Map encodedMap = encoder.encode(object); + assertTrue("Non-empty map generated from null fields", encodedMap.isEmpty()); + } + + static class VisibleFieldsObject { + String foo; + String bar; + String baz; + } +} + diff --git a/core/src/test/java/feign/FeignBuilderTest.java b/core/src/test/java/feign/FeignBuilderTest.java index a8656ce5a9..6bf8077753 100644 --- a/core/src/test/java/feign/FeignBuilderTest.java +++ b/core/src/test/java/feign/FeignBuilderTest.java @@ -13,6 +13,7 @@ */ package feign; +import java.util.HashMap; import okhttp3.mockwebserver.MockResponse; import okhttp3.mockwebserver.MockWebServer; @@ -160,6 +161,28 @@ public Object decode(Response response, Type type) { assertEquals(1, server.getRequestCount()); } + @Test + public void testOverrideQueryMapEnoder() throws Exception { + server.enqueue(new MockResponse()); + + String url = "http://localhost:" + server.getPort(); + QueryMapEncoder customMapEncoder = new QueryMapEncoder() { + @Override + public Map encode(Object ignored) { + Map queryMap = new HashMap(); + queryMap.put("key1", "value1"); + queryMap.put("key2", "value2"); + return queryMap; + } + }; + + TestInterface api = Feign.builder().queryMapEncoder(customMapEncoder).target(TestInterface.class, url); + api.queryMapEncoded("ignored"); + + assertThat(server.takeRequest()).hasQueryParams(Arrays.asList("key1=value1", "key2=value2")); + assertEquals(1, server.getRequestCount()); + } + @Test public void testProvideRequestInterceptors() throws Exception { server.enqueue(new MockResponse().setBody("response data")); @@ -307,6 +330,9 @@ interface TestInterface { @RequestLine("GET api/thing") Response getNoInitialSlashOnSlash(); + @RequestLine(value = "GET /api/querymap/object") + String queryMapEncoded(@QueryMap Object object); + @RequestLine("POST /") Response codecPost(String data); diff --git a/core/src/test/java/feign/FeignTest.java b/core/src/test/java/feign/FeignTest.java index 2ab91b8ddd..fcba7548ad 100644 --- a/core/src/test/java/feign/FeignTest.java +++ b/core/src/test/java/feign/FeignTest.java @@ -392,7 +392,7 @@ public void queryMapPojoWithFullParams() throws Exception { server.enqueue(new MockResponse()); api.queryMapPojo(customPojo); assertThat(server.takeRequest()) - .hasPath("/?name=Name&number=3"); + .hasQueryParams(Arrays.asList("name=Name", "number=3")); } @Test diff --git a/core/src/test/java/feign/QueryMapEncoderObject.java b/core/src/test/java/feign/QueryMapEncoderObject.java new file mode 100644 index 0000000000..5ca8b5113b --- /dev/null +++ b/core/src/test/java/feign/QueryMapEncoderObject.java @@ -0,0 +1,24 @@ +/** + * Copyright 2012-2018 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; + +class QueryMapEncoderObject { + private final String foo; + private final String bar; + + QueryMapEncoderObject (String foo, String bar) { + this.foo = foo; + this.bar = bar; + } +} diff --git a/core/src/test/java/feign/assertj/RecordedRequestAssert.java b/core/src/test/java/feign/assertj/RecordedRequestAssert.java index 105449cde9..83f6686c07 100644 --- a/core/src/test/java/feign/assertj/RecordedRequestAssert.java +++ b/core/src/test/java/feign/assertj/RecordedRequestAssert.java @@ -13,6 +13,9 @@ */ package feign.assertj; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; import okhttp3.Headers; import okhttp3.mockwebserver.RecordedRequest; @@ -62,6 +65,29 @@ public RecordedRequestAssert hasPath(String expected) { return this; } + public RecordedRequestAssert hasQueryParams(String... expectedParams) { + return hasQueryParams(Arrays.asList(expectedParams)); + } + + public RecordedRequestAssert hasQueryParams(Collection expectedParams) { + isNotNull(); + Collection actualQueryParams = getQueryParams(); + objects.assertEqual(info, expectedParams.size(), actualQueryParams.size()); + for (String expectedParam : expectedParams) { + objects.assertIsIn(info, expectedParam, actualQueryParams); + } + return this; + } + + private Collection getQueryParams () { + String path = actual.getPath(); + int queryStart = path.indexOf("?") + 1; + String[] queryParams = actual.getPath() + .substring(queryStart) + .split("&"); + return Arrays.asList(queryParams); + } + public RecordedRequestAssert hasBody(String utf8Expected) { isNotNull(); objects.assertEqual(info, actual.getBody().readUtf8(), utf8Expected); From eb8faf90aa5ef8f3825ee16d63d00b66c2fe8152 Mon Sep 17 00:00:00 2001 From: Ralph Jennings Date: Thu, 19 Apr 2018 17:45:48 -0700 Subject: [PATCH 12/13] Corrected typo in README.md --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index e1884e83ca..1a8bef21de 100644 --- a/README.md +++ b/README.md @@ -443,14 +443,14 @@ A Map parameter can be annotated with `QueryMap` to construct a query that uses V find(@QueryMap Map queryMap); ``` -This may also be used to generate the query parameters from a POJO object using a `QueryParamEncoder`. +This may also be used to generate the query parameters from a POJO object using a `QueryMapEncoder`. ```java @RequestLine("GET /find") V find(@QueryMap CustomPojo customPojo); ``` -When used in this manner, without specifying a custom `QueryParamEncoder`, the query map will be generated using member variable names as query parameter names. The following POJO will generate query params of "/find?name={name}&number={number}" (order of included query parameters not guaranteed, and as usual, if any value is null, it will be left out). +When used in this manner, without specifying a custom `QueryMapEncoder`, the query map will be generated using member variable names as query parameter names. The following POJO will generate query params of "/find?name={name}&number={number}" (order of included query parameters not guaranteed, and as usual, if any value is null, it will be left out). ```java public class CustomPojo { @@ -464,11 +464,11 @@ public class CustomPojo { } ``` -To setup a custom `QueryParamEncoder`: +To setup a custom `QueryMapEncoder`: ```java MyApi myApi = Feign.builder() - .queryParamEncoder(new MyCustomQueryParamEncoder()) + .queryMapEncoder(new MyCustomQueryMapEncoder()) .target(MyApi.class, "https://api.hostname.com"); ``` From 818deb552e6f777d1f0cf4cced48c6745d862beb Mon Sep 17 00:00:00 2001 From: Ralph Jennings Date: Mon, 23 Apr 2018 10:11:06 -0700 Subject: [PATCH 13/13] Fixed merge conflict and typo in test name --- core/src/test/java/feign/FeignBuilderTest.java | 2 +- .../test/java/feign/assertj/RecordedRequestAssert.java | 8 +++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/core/src/test/java/feign/FeignBuilderTest.java b/core/src/test/java/feign/FeignBuilderTest.java index 6bf8077753..ff5a403bbc 100644 --- a/core/src/test/java/feign/FeignBuilderTest.java +++ b/core/src/test/java/feign/FeignBuilderTest.java @@ -162,7 +162,7 @@ public Object decode(Response response, Type type) { } @Test - public void testOverrideQueryMapEnoder() throws Exception { + public void testOverrideQueryMapEncoder() throws Exception { server.enqueue(new MockResponse()); String url = "http://localhost:" + server.getPort(); diff --git a/core/src/test/java/feign/assertj/RecordedRequestAssert.java b/core/src/test/java/feign/assertj/RecordedRequestAssert.java index 83f6686c07..b10c9c1901 100644 --- a/core/src/test/java/feign/assertj/RecordedRequestAssert.java +++ b/core/src/test/java/feign/assertj/RecordedRequestAssert.java @@ -79,7 +79,7 @@ public RecordedRequestAssert hasQueryParams(Collection expectedParams) { return this; } - private Collection getQueryParams () { + private Collection getQueryParams() { String path = actual.getPath(); int queryStart = path.indexOf("?") + 1; String[] queryParams = actual.getPath() @@ -88,6 +88,12 @@ private Collection getQueryParams () { return Arrays.asList(queryParams); } + public RecordedRequestAssert hasOneOfPath(String... expected) { + isNotNull(); + objects.assertIsIn(info, actual.getPath(), expected); + return this; + } + public RecordedRequestAssert hasBody(String utf8Expected) { isNotNull(); objects.assertEqual(info, actual.getBody().readUtf8(), utf8Expected);