Skip to content
Merged
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
29 changes: 29 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -444,6 +444,35 @@ A Map parameter can be annotated with `QueryMap` to construct a query that uses
V find(@QueryMap Map<String, Object> queryMap);
```

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 `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 {
private final String name;
private final int number;

public CustomPojo (String name, int number) {
this.name = name;
this.number = number;
}
}
```

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice docs.

We should discuss with the wider audience if we want all member variables (including non-exposed private ones), or if we want those that are exposed in some way (public or have a getter), and if we handle transient ones or not. It's worth asking as I see below in the code that we're changing access modifiers on the fly (making accessible if not) - something that may not be something we do out-of-the box or so...

@kdavisk6 kdavisk6 Apr 7, 2018

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In another PR of a similar nature, I made the suggestion that we should support objects via the Java Beans api. That should allow for the widest range of support and interoperability. However straight reflection is ok too from my perspective.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd still argue in favor of annotating the methods/fields that you want to use with something like @Param. The only reason to use all fields, was for simplicity (I guess additional annotations weren't simple enough). Personally, I'm less a fan of parsing all getter method names (javabeans style) than I am of reading all fields. But if it'll get this stuff merged...

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@rage-shadowman let's get more thoughts on it from a few people. It may be that the concensus is this works just fine and we like it (and then just leave it alone). It may also be that we go Java Beans style or something else. let's see what others think before taking any drastic decision.

@rage-shadowman rage-shadowman Apr 8, 2018

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the map builder has an interface that can be passed into the feign builder, then I could even write my own in my local client that would use my annotations without complicating anything internal to feign. We could then have an EJB implementation that parses getters as a separate possibility.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree that a Serializer would fit nicely, so as long as we have a sensible default Serializer.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I created a QueryMapEncoder that can be specified via the Feign.Builder. I think it answers this concern and cleans up the naming issues.

I haven't pushed it into this pull request as I don't want to jump the gun on this. You can check that out in https://github.com/rage-shadowman/feign/tree/feature/shadowman/query-param-encoder

Should I push that into this pull request?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there any agreement here? Shall I make the QueryMap encoder something to be added to the Feign.builder() as in the aforementioned branch (this one gets my vote)? Should I add it as a parameter to QueryMap requiring a 0-arg public constructor (similar to Param.Expander)? Or should I just clean up the naming and packaging of what is here and call it done?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@rage-shadowman I'd add your encoder to the PR and suggest that it be used by default, without any additional customization to start.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.


To setup a custom `QueryMapEncoder`:

```java
MyApi myApi = Feign.builder()
.queryMapEncoder(new MyCustomQueryMapEncoder())
.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.
Expand Down
8 changes: 7 additions & 1 deletion core/src/main/java/feign/Contract.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this looks like a good add - but I didn't see a test covering it in the case that it isn't a string (I assume tests already cover the case where it is)

@rage-shadowman rage-shadowman Apr 7, 2018

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This wasn't an addition, it was a DRY split. I suppose I could revert the checkMapString method and just leave the Map instance check even though I just checked that. The test is in DefaultContractTest.queryMapKeysMustBeStrings.

Type[] parameterTypes = ((ParameterizedType) genericType).getActualTypeArguments();
Class<?> keyClass = (Class<?>) parameterTypes[0];
checkState(String.class.equals(keyClass),
Expand Down
10 changes: 8 additions & 2 deletions core/src/main/java/feign/Feign.java
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand Down Expand Up @@ -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.
*/
Expand Down Expand Up @@ -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);
}
}

Expand Down
88 changes: 88 additions & 0 deletions core/src/main/java/feign/QueryMapEncoder.java
Original file line number Diff line number Diff line change
@@ -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<String, Object> encode (Object object);

class Default implements QueryMapEncoder {

private final Map<Class<?>, ObjectParamMetadata> classToMetadata =
new HashMap<Class<?>, ObjectParamMetadata>();

@Override
public Map<String, Object> encode (Object object) throws EncodeException {
try {
ObjectParamMetadata metadata = getMetadata(object.getClass());
Map<String, Object> fieldNameToValue = new HashMap<String, Object>();
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<Field> objectFields;

private ObjectParamMetadata (List<Field> objectFields) {
this.objectFields = Collections.unmodifiableList(objectFields);
}

private static ObjectParamMetadata parseObjectType(Class<?> type) {
List<Field> fields = new ArrayList<Field>();
for (Field field : type.getDeclaredFields()) {
if (!field.isAccessible()) {
field.setAccessible(true);
}
fields.add(field);
}
return new ObjectParamMetadata(fields);
}
}
}
}
53 changes: 39 additions & 14 deletions core/src/main/java/feign/ReflectiveFeign.java
Original file line number Diff line number Diff line change
Expand Up @@ -29,16 +29,17 @@

import static feign.Util.checkArgument;
import static feign.Util.checkNotNull;
import static feign.Util.checkState;

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;
}

/**
Expand Down Expand Up @@ -128,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");
}
Expand All @@ -146,11 +155,11 @@ public Map<String, MethodHandler> 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));
Expand All @@ -161,11 +170,14 @@ public Map<String, MethodHandler> apply(Target key) {

private static class BuildTemplateByResolvingArgs implements RequestTemplate.Factory {

private final QueryMapEncoder queryMapEncoder;

protected final MethodMetadata metadata;
private final Map<Integer, Expander> indexToExpander = new LinkedHashMap<Integer, Expander>();

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;
Expand Down Expand Up @@ -212,7 +224,9 @@ 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<String, Object>) argv[metadata.queryMapIndex()], template);
Object value = argv[metadata.queryMapIndex()];
Map<String, Object> queryMap = toQueryMap(value);
template = addQueryMapQueryParameters(queryMap, template);
}

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

private Map<String, Object> toQueryMap (Object value) {
if (value instanceof Map) {
return (Map<String, Object>)value;
}
try {
return queryMapEncoder.encode(value);
} catch (EncodeException e) {
throw new IllegalStateException(e);
}
}

private Object expandElements(Expander expander, Object value) {
if (value instanceof Iterable) {
return expandIterable(expander, (Iterable) value);
Expand All @@ -231,7 +256,7 @@ private Object expandElements(Expander expander, Object value) {

private List<String> expandIterable(Expander expander, Iterable value) {
List<String> values = new ArrayList<String>();
for (Object element : (Iterable) value) {
for (Object element : value) {
if (element!=null) {
values.add(expander.expand(element));
}
Expand Down Expand Up @@ -300,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;
}

Expand Down Expand Up @@ -329,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;
}

Expand Down
25 changes: 25 additions & 0 deletions core/src/test/java/feign/CustomPojo.java
Original file line number Diff line number Diff line change
@@ -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;
}
}
Loading