diff --git a/core/src/main/java/feign/QueryObject.java b/core/src/main/java/feign/QueryObject.java new file mode 100644 index 0000000000..6bfa38937c --- /dev/null +++ b/core/src/main/java/feign/QueryObject.java @@ -0,0 +1,24 @@ +package feign; + +import java.lang.annotation.Retention; + +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +/** + * This is a tag annotation to support Object Query. By default, Feign does not support object query with a GET request, + * You can use use QueryObjectEncoder to support this. + */ +@Retention(RUNTIME) +@java.lang.annotation.Target(TYPE) +public @interface QueryObject { + + @Retention(RUNTIME) + @java.lang.annotation.Target(METHOD) + @interface Param { + String value(); + } + +} + diff --git a/core/src/main/java/feign/codec/QueryObjectEncoder.java b/core/src/main/java/feign/codec/QueryObjectEncoder.java new file mode 100644 index 0000000000..fedbe16d54 --- /dev/null +++ b/core/src/main/java/feign/codec/QueryObjectEncoder.java @@ -0,0 +1,74 @@ +package feign.codec; + + +import feign.QueryObject; +import feign.RequestTemplate; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Type; +import java.util.HashMap; +import java.util.Map; + +/** + * You can create an QueryObject like this + *
+ * {@code
+ * @QueryObject
+ * class FindUsersQuery {
+ *    .....
+ *    @QueryObject.Param("userId")
+ *    long getUserId() {...}
+ *    @QueryObject.Param("userName")
+ *    long getUserName() {...}
+ * }
+ * 
+ * Then you can declare the client like this: + *
+ * {@code
+ * @RequestLine("GET /users?userId={userId}&userName={userName}")
+ * List getUsers(FindUsersQuery query);
+ * 
+ */ +public class QueryObjectEncoder implements Encoder { + + private Encoder fallbackEncoder; + + public QueryObjectEncoder(Encoder fallbackEncoder) { + this.fallbackEncoder = fallbackEncoder; + } + + @Override + public void encode(Object parametersObject, Type bodyType, RequestTemplate template) throws EncodeException { + if (parametersObject.getClass().getAnnotation(QueryObject.class) != null) { + Map params = new HashMap(); + try { + for (Method method : parametersObject.getClass().getMethods()) { + QueryObject.Param param = method.getAnnotation(QueryObject.Param.class); + if (param != null && method.getName().startsWith("get")) { + String key = param.value(); + Object value = method.invoke(parametersObject); + if (value != null) { + value = String.valueOf(value); + } + params.put(key, value); + template.query(key, keyToTemplate(key)); + } + } + template.resolve(params); + } catch (IllegalAccessException e) { + throw new EncodeException("Could not encode object query correctly", e); + } catch (InvocationTargetException e) { + throw new EncodeException("Could not encode object query correctly", e); + } + } else { + fallbackEncoder.encode(parametersObject, bodyType, template); + } + } + + private String keyToTemplate(String key) { + return "{" + key + "}"; + } + +} + diff --git a/core/src/test/java/feign/FeignTest.java b/core/src/test/java/feign/FeignTest.java index 13a95efacf..190916d427 100644 --- a/core/src/test/java/feign/FeignTest.java +++ b/core/src/test/java/feign/FeignTest.java @@ -18,7 +18,9 @@ import com.google.gson.Gson; import com.google.gson.reflect.TypeToken; +import feign.codec.*; import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.RecordedRequest; import okhttp3.mockwebserver.SocketPolicy; import okhttp3.mockwebserver.MockWebServer; @@ -27,7 +29,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; @@ -43,14 +44,9 @@ import java.util.Map; import java.util.NoSuchElementException; import java.util.concurrent.atomic.AtomicReference; +import java.util.stream.Collectors; import feign.Target.HardCodedTarget; -import feign.codec.DecodeException; -import feign.codec.Decoder; -import feign.codec.EncodeException; -import feign.codec.Encoder; -import feign.codec.ErrorDecoder; -import feign.codec.StringDecoder; import feign.Feign.ResponseMappingDecoder; import static feign.Util.UTF_8; @@ -699,6 +695,39 @@ public void responseMapperIsAppliedBeforeDelegate() throws IOException { assertThat(output).isEqualTo("RESPONSE"); } + @Test + public void okWithQueryObject() throws Exception { + server.enqueue(new MockResponse().setBody("foo")); + TestInterface api = new TestInterfaceBuilder().encoder(new QueryObjectEncoder(new Encoder.Default())). + target("http://localhost:" + server.getPort()); + TestInterface.QueryObjectTestParam testParam = new TestInterface.QueryObjectTestParam("aa", 888); + testParam.setK3("123"); + api.queryObjectWithQueryParams(testParam); + RecordedRequest req = server.takeRequest(); + assertThat(req.getMethod()).isEqualToIgnoringCase("GET"); + Map map = Arrays.stream(req.getPath().substring(2).split("&")).map(kv -> kv.split("=")). + collect(Collectors.toMap(kv -> kv[0], kv -> kv[1])); + assertThat(map.get("k1")).isEqualTo("aa"); + assertThat(map.get("k2")).isEqualTo("888"); + assertThat(map.get("k3")).isEqualTo("123"); + } + + @Test + public void okWithQueryObjectOptional() throws Exception { + server.enqueue(new MockResponse().setBody("foo")); + TestInterface api = new TestInterfaceBuilder().encoder(new QueryObjectEncoder(new Encoder.Default())). + target("http://localhost:" + server.getPort()); + api.queryObjectWithQueryParams(new TestInterface.QueryObjectTestParam("xx", 55)); + RecordedRequest req = server.takeRequest(); + assertThat(req.getMethod()).isEqualToIgnoringCase("GET"); + Map map = Arrays.stream(req.getPath().substring(2).split("&")).map(kv -> kv.split("=")). + collect(Collectors.toMap(kv -> kv[0], kv -> kv[1])); + assertThat(map.get("k1")).isEqualTo("xx"); + assertThat(map.get("k2")).isEqualTo("55"); + assertThat(map.get("k3")).isNull(); + } + + private ResponseMapper upperCaseResponseMapper() { return new ResponseMapper() { @Override @@ -798,6 +827,9 @@ void form( @RequestLine("GET /?trim={trim}") void encodedQueryParam(@Param(value = "trim", encoded = true) String trim); + @RequestLine("GET /?k1={k1}&k2={k2}&k3={k3}") + void queryObjectWithQueryParams(QueryObjectTestParam test); + class DateToMillis implements Param.Expander { @Override @@ -805,6 +837,37 @@ public String expand(Object value) { return String.valueOf(((Date) value).getTime()); } } + + @QueryObject + class QueryObjectTestParam { + private final String k1; + private final int k2; + private String k3; //optional + + public QueryObjectTestParam(String k1, int k2) { + this.k1 = k1; + this.k2 = k2; + } + + @QueryObject.Param("k1") + public String getK1() { + return k1; + } + + @QueryObject.Param("k2") + public int getK2() { + return k2; + } + + @QueryObject.Param("k3") + public String getK3() { + return k3; + } + + public void setK3(String k3) { + this.k3 = k3; + } + } } interface OtherTestInterface {