Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions core/src/main/java/feign/QueryObject.java
Original file line number Diff line number Diff line change
@@ -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();
}

}

74 changes: 74 additions & 0 deletions core/src/main/java/feign/codec/QueryObjectEncoder.java
Original file line number Diff line number Diff line change
@@ -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
* <pre>
* {@code
* @QueryObject
* class FindUsersQuery {
* .....
* @QueryObject.Param("userId")
* long getUserId() {...}
* @QueryObject.Param("userName")
* long getUserName() {...}
* }
* </pre>
* Then you can declare the client like this:
* <pre>
* {@code
* @RequestLine("GET /users?userId={userId}&userName={userName}")
* List<User> getUsers(FindUsersQuery query);
* </pre>
*/
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<String, Object> params = new HashMap<String, Object>();
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 + "}";
}

}

77 changes: 70 additions & 7 deletions core/src/test/java/feign/FeignTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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<String, String> 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<String, String> 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
Expand Down Expand Up @@ -798,13 +827,47 @@ 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
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 {
Expand Down