Skip to content

Commit 5e2fdee

Browse files
rage-shadowmanvelo
authored andcommitted
Added support for custom POJO query param encoding (OpenFeign#667)
* Added support for custom param encoding * Added ability to inherit @CustomParam annotation * Updated class cast style to match rest of code * Updated to use QueryMap for custom pojo query parameters * Clarification in README of QueryMap POJO usage * Removed unused line * Updated custom POJO QueryMap test to prove that private fields can be used * Removed no-longer-valid test endpoint * Renamed tests to more accurately reflect their contents * More test cleanup * Modified QueryMap POJO encoding to use specified QueryMapEncoder (default implementation provided) * Corrected typo in README.md * Fixed merge conflict and typo in test name
1 parent 213494d commit 5e2fdee

12 files changed

Lines changed: 421 additions & 32 deletions

README.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -444,6 +444,35 @@ A Map parameter can be annotated with `QueryMap` to construct a query that uses
444444
V find(@QueryMap Map<String, Object> queryMap);
445445
```
446446

447+
This may also be used to generate the query parameters from a POJO object using a `QueryMapEncoder`.
448+
449+
```java
450+
@RequestLine("GET /find")
451+
V find(@QueryMap CustomPojo customPojo);
452+
```
453+
454+
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).
455+
456+
```java
457+
public class CustomPojo {
458+
private final String name;
459+
private final int number;
460+
461+
public CustomPojo (String name, int number) {
462+
this.name = name;
463+
this.number = number;
464+
}
465+
}
466+
```
467+
468+
To setup a custom `QueryMapEncoder`:
469+
470+
```java
471+
MyApi myApi = Feign.builder()
472+
.queryMapEncoder(new MyCustomQueryMapEncoder())
473+
.target(MyApi.class, "https://api.hostname.com");
474+
```
475+
447476
#### Static and Default Methods
448477
Interfaces targeted by Feign may have static or default methods (if using Java 8+).
449478
These allows Feign clients to contain logic that is not expressly defined by the underlying API.

core/src/main/java/feign/Contract.java

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,9 @@ protected MethodMetadata parseAndValidateMetadata(Class<?> targetType, Method me
123123
}
124124

125125
if (data.queryMapIndex() != null) {
126-
checkMapString("QueryMap", parameterTypes[data.queryMapIndex()], genericParameterTypes[data.queryMapIndex()]);
126+
if (Map.class.isAssignableFrom(parameterTypes[data.queryMapIndex()])) {
127+
checkMapKeys("QueryMap", genericParameterTypes[data.queryMapIndex()]);
128+
}
127129
}
128130

129131
return data;
@@ -132,6 +134,10 @@ protected MethodMetadata parseAndValidateMetadata(Class<?> targetType, Method me
132134
private static void checkMapString(String name, Class<?> type, Type genericType) {
133135
checkState(Map.class.isAssignableFrom(type),
134136
"%s parameter must be a Map: %s", name, type);
137+
checkMapKeys(name, genericType);
138+
}
139+
140+
private static void checkMapKeys(String name, Type genericType) {
135141
Type[] parameterTypes = ((ParameterizedType) genericType).getActualTypeArguments();
136142
Class<?> keyClass = (Class<?>) parameterTypes[0];
137143
checkState(String.class.equals(keyClass),

core/src/main/java/feign/Feign.java

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ public static class Builder {
101101
private Logger logger = new NoOpLogger();
102102
private Encoder encoder = new Encoder.Default();
103103
private Decoder decoder = new Decoder.Default();
104+
private QueryMapEncoder queryMapEncoder = new QueryMapEncoder.Default();
104105
private ErrorDecoder errorDecoder = new ErrorDecoder.Default();
105106
private Options options = new Options();
106107
private InvocationHandlerFactory invocationHandlerFactory =
@@ -143,6 +144,11 @@ public Builder decoder(Decoder decoder) {
143144
return this;
144145
}
145146

147+
public Builder queryMapEncoder(QueryMapEncoder queryMapEncoder) {
148+
this.queryMapEncoder = queryMapEncoder;
149+
return this;
150+
}
151+
146152
/**
147153
* Allows to map the response before passing it to the decoder.
148154
*/
@@ -241,9 +247,9 @@ public Feign build() {
241247
new SynchronousMethodHandler.Factory(client, retryer, requestInterceptors, logger,
242248
logLevel, decode404, closeAfterDecode);
243249
ParseHandlersByName handlersByName =
244-
new ParseHandlersByName(contract, options, encoder, decoder,
250+
new ParseHandlersByName(contract, options, encoder, decoder, queryMapEncoder,
245251
errorDecoder, synchronousMethodHandlerFactory);
246-
return new ReflectiveFeign(handlersByName, invocationHandlerFactory);
252+
return new ReflectiveFeign(handlersByName, invocationHandlerFactory, queryMapEncoder);
247253
}
248254
}
249255

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
/**
2+
* Copyright 2012-2018 The Feign Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
5+
* in compliance with the License. You may obtain a copy of the License at
6+
*
7+
* http://www.apache.org/licenses/LICENSE-2.0
8+
*
9+
* Unless required by applicable law or agreed to in writing, software distributed under the License
10+
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
11+
* or implied. See the License for the specific language governing permissions and limitations under
12+
* the License.
13+
*/
14+
package feign;
15+
16+
import feign.codec.EncodeException;
17+
import java.lang.reflect.Field;
18+
import java.util.ArrayList;
19+
import java.util.Collections;
20+
import java.util.HashMap;
21+
import java.util.List;
22+
import java.util.Map;
23+
24+
/**
25+
* A QueryMapEncoder encodes Objects into maps of query parameter names to values.
26+
*/
27+
public interface QueryMapEncoder {
28+
29+
/**
30+
* Encodes the given object into a query map.
31+
*
32+
* @param object the object to encode
33+
* @return the map represented by the object
34+
*/
35+
Map<String, Object> encode (Object object);
36+
37+
class Default implements QueryMapEncoder {
38+
39+
private final Map<Class<?>, ObjectParamMetadata> classToMetadata =
40+
new HashMap<Class<?>, ObjectParamMetadata>();
41+
42+
@Override
43+
public Map<String, Object> encode (Object object) throws EncodeException {
44+
try {
45+
ObjectParamMetadata metadata = getMetadata(object.getClass());
46+
Map<String, Object> fieldNameToValue = new HashMap<String, Object>();
47+
for (Field field : metadata.objectFields) {
48+
Object value = field.get(object);
49+
if (value != null && value != object) {
50+
fieldNameToValue.put(field.getName(), value);
51+
}
52+
}
53+
return fieldNameToValue;
54+
} catch (IllegalAccessException e) {
55+
throw new EncodeException("Failure encoding object into query map", e);
56+
}
57+
}
58+
59+
private ObjectParamMetadata getMetadata(Class<?> objectType) {
60+
ObjectParamMetadata metadata = classToMetadata.get(objectType);
61+
if (metadata == null) {
62+
metadata = ObjectParamMetadata.parseObjectType(objectType);
63+
classToMetadata.put(objectType, metadata);
64+
}
65+
return metadata;
66+
}
67+
68+
private static class ObjectParamMetadata {
69+
70+
private final List<Field> objectFields;
71+
72+
private ObjectParamMetadata (List<Field> objectFields) {
73+
this.objectFields = Collections.unmodifiableList(objectFields);
74+
}
75+
76+
private static ObjectParamMetadata parseObjectType(Class<?> type) {
77+
List<Field> fields = new ArrayList<Field>();
78+
for (Field field : type.getDeclaredFields()) {
79+
if (!field.isAccessible()) {
80+
field.setAccessible(true);
81+
}
82+
fields.add(field);
83+
}
84+
return new ObjectParamMetadata(fields);
85+
}
86+
}
87+
}
88+
}

core/src/main/java/feign/ReflectiveFeign.java

Lines changed: 39 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -29,16 +29,17 @@
2929

3030
import static feign.Util.checkArgument;
3131
import static feign.Util.checkNotNull;
32-
import static feign.Util.checkState;
3332

3433
public class ReflectiveFeign extends Feign {
3534

3635
private final ParseHandlersByName targetToHandlersByName;
3736
private final InvocationHandlerFactory factory;
37+
private final QueryMapEncoder queryMapEncoder;
3838

39-
ReflectiveFeign(ParseHandlersByName targetToHandlersByName, InvocationHandlerFactory factory) {
39+
ReflectiveFeign(ParseHandlersByName targetToHandlersByName, InvocationHandlerFactory factory, QueryMapEncoder queryMapEncoder) {
4040
this.targetToHandlersByName = targetToHandlersByName;
4141
this.factory = factory;
42+
this.queryMapEncoder = queryMapEncoder;
4243
}
4344

4445
/**
@@ -128,14 +129,22 @@ static final class ParseHandlersByName {
128129
private final Encoder encoder;
129130
private final Decoder decoder;
130131
private final ErrorDecoder errorDecoder;
132+
private final QueryMapEncoder queryMapEncoder;
131133
private final SynchronousMethodHandler.Factory factory;
132134

133-
ParseHandlersByName(Contract contract, Options options, Encoder encoder, Decoder decoder,
134-
ErrorDecoder errorDecoder, SynchronousMethodHandler.Factory factory) {
135+
ParseHandlersByName(
136+
Contract contract,
137+
Options options,
138+
Encoder encoder,
139+
Decoder decoder,
140+
QueryMapEncoder queryMapEncoder,
141+
ErrorDecoder errorDecoder,
142+
SynchronousMethodHandler.Factory factory) {
135143
this.contract = contract;
136144
this.options = options;
137145
this.factory = factory;
138146
this.errorDecoder = errorDecoder;
147+
this.queryMapEncoder = queryMapEncoder;
139148
this.encoder = checkNotNull(encoder, "encoder");
140149
this.decoder = checkNotNull(decoder, "decoder");
141150
}
@@ -146,11 +155,11 @@ public Map<String, MethodHandler> apply(Target key) {
146155
for (MethodMetadata md : metadata) {
147156
BuildTemplateByResolvingArgs buildTemplate;
148157
if (!md.formParams().isEmpty() && md.template().bodyTemplate() == null) {
149-
buildTemplate = new BuildFormEncodedTemplateFromArgs(md, encoder);
158+
buildTemplate = new BuildFormEncodedTemplateFromArgs(md, encoder, queryMapEncoder);
150159
} else if (md.bodyIndex() != null) {
151-
buildTemplate = new BuildEncodedTemplateFromArgs(md, encoder);
160+
buildTemplate = new BuildEncodedTemplateFromArgs(md, encoder, queryMapEncoder);
152161
} else {
153-
buildTemplate = new BuildTemplateByResolvingArgs(md);
162+
buildTemplate = new BuildTemplateByResolvingArgs(md, queryMapEncoder);
154163
}
155164
result.put(md.configKey(),
156165
factory.create(key, md, buildTemplate, options, decoder, errorDecoder));
@@ -161,11 +170,14 @@ public Map<String, MethodHandler> apply(Target key) {
161170

162171
private static class BuildTemplateByResolvingArgs implements RequestTemplate.Factory {
163172

173+
private final QueryMapEncoder queryMapEncoder;
174+
164175
protected final MethodMetadata metadata;
165176
private final Map<Integer, Expander> indexToExpander = new LinkedHashMap<Integer, Expander>();
166177

167-
private BuildTemplateByResolvingArgs(MethodMetadata metadata) {
178+
private BuildTemplateByResolvingArgs(MethodMetadata metadata, QueryMapEncoder queryMapEncoder) {
168179
this.metadata = metadata;
180+
this.queryMapEncoder = queryMapEncoder;
169181
if (metadata.indexToExpander() != null) {
170182
indexToExpander.putAll(metadata.indexToExpander());
171183
return;
@@ -212,7 +224,9 @@ public RequestTemplate create(Object[] argv) {
212224
if (metadata.queryMapIndex() != null) {
213225
// add query map parameters after initial resolve so that they take
214226
// precedence over any predefined values
215-
template = addQueryMapQueryParameters((Map<String, Object>) argv[metadata.queryMapIndex()], template);
227+
Object value = argv[metadata.queryMapIndex()];
228+
Map<String, Object> queryMap = toQueryMap(value);
229+
template = addQueryMapQueryParameters(queryMap, template);
216230
}
217231

218232
if (metadata.headerMapIndex() != null) {
@@ -222,6 +236,17 @@ public RequestTemplate create(Object[] argv) {
222236
return template;
223237
}
224238

239+
private Map<String, Object> toQueryMap (Object value) {
240+
if (value instanceof Map) {
241+
return (Map<String, Object>)value;
242+
}
243+
try {
244+
return queryMapEncoder.encode(value);
245+
} catch (EncodeException e) {
246+
throw new IllegalStateException(e);
247+
}
248+
}
249+
225250
private Object expandElements(Expander expander, Object value) {
226251
if (value instanceof Iterable) {
227252
return expandIterable(expander, (Iterable) value);
@@ -231,7 +256,7 @@ private Object expandElements(Expander expander, Object value) {
231256

232257
private List<String> expandIterable(Expander expander, Iterable value) {
233258
List<String> values = new ArrayList<String>();
234-
for (Object element : (Iterable) value) {
259+
for (Object element : value) {
235260
if (element!=null) {
236261
values.add(expander.expand(element));
237262
}
@@ -300,8 +325,8 @@ private static class BuildFormEncodedTemplateFromArgs extends BuildTemplateByRes
300325

301326
private final Encoder encoder;
302327

303-
private BuildFormEncodedTemplateFromArgs(MethodMetadata metadata, Encoder encoder) {
304-
super(metadata);
328+
private BuildFormEncodedTemplateFromArgs(MethodMetadata metadata, Encoder encoder, QueryMapEncoder queryMapEncoder) {
329+
super(metadata, queryMapEncoder);
305330
this.encoder = encoder;
306331
}
307332

@@ -329,8 +354,8 @@ private static class BuildEncodedTemplateFromArgs extends BuildTemplateByResolvi
329354

330355
private final Encoder encoder;
331356

332-
private BuildEncodedTemplateFromArgs(MethodMetadata metadata, Encoder encoder) {
333-
super(metadata);
357+
private BuildEncodedTemplateFromArgs(MethodMetadata metadata, Encoder encoder, QueryMapEncoder queryMapEncoder) {
358+
super(metadata, queryMapEncoder);
334359
this.encoder = encoder;
335360
}
336361

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
/**
2+
* Copyright 2012-2018 The Feign Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
5+
* in compliance with the License. You may obtain a copy of the License at
6+
*
7+
* http://www.apache.org/licenses/LICENSE-2.0
8+
*
9+
* Unless required by applicable law or agreed to in writing, software distributed under the License
10+
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
11+
* or implied. See the License for the specific language governing permissions and limitations under
12+
* the License.
13+
*/
14+
package feign;
15+
16+
public class CustomPojo {
17+
18+
private final String name;
19+
private final Integer number;
20+
21+
CustomPojo(String name, Integer number) {
22+
this.name = name;
23+
this.number = number;
24+
}
25+
}

0 commit comments

Comments
 (0)