Browse Source

Merge pull request #362 from nmiyake/feature/headerMap

Add HeaderMap annotation
pull/360/merge
Adrian Cole 9 years ago
parent
commit
bd305edc9f
  1. 3
      CHANGELOG.md
  2. 17
      README.md
  3. 9
      core/src/main/java/feign/Contract.java
  4. 55
      core/src/main/java/feign/HeaderMap.java
  5. 10
      core/src/main/java/feign/MethodMetadata.java
  6. 28
      core/src/main/java/feign/ReflectiveFeign.java
  7. 54
      core/src/test/java/feign/FeignTest.java

3
CHANGELOG.md

@ -1,3 +1,6 @@ @@ -1,3 +1,6 @@
### Version 8.16
* Adds `@HeaderMap` annotation to support dynamic header fields and values
### Version 8.15
* Adds `@QueryMap` annotation to support dynamic query parameters
* Supports runtime injection of `Param.Expander` via `MethodMetadata.indexToExpander`

17
README.md

@ -273,9 +273,18 @@ Methods can specify dynamic content for static headers using using variable expa @@ -273,9 +273,18 @@ Methods can specify dynamic content for static headers using using variable expa
void post(@Param("token") String token);
```
These approaches specify specific header entries as part of the api without requiring any customizations
when buildling Feing clients. It is not currently possible to customize the header entries themselves
on a per-request basis at the api level.
In cases where both the header field keys and values are dynamic and the range of possible keys cannot
be known ahead of time and may vary between different method calls in the same api/client (e.g. custom
metadata header fields such as "x-amz-meta-\*" or "x-goog-meta-\*"), a Map parameter can be annotated
with `HeaderMap` to construct a query that uses the contents of the map as its header parameters.
```java
@RequestLine("POST /")
void post(@HeaderMap Map<String, Object> headerMap);
```
These approaches specify header entries as part of the api and do not require any customizations
when building the Feign client.
#### Setting headers per target
In cases where headers should differ for the same api based on different endpoints or where per-request
@ -417,5 +426,5 @@ A Map parameter can be annotated with `QueryMap` to construct a query that uses @@ -417,5 +426,5 @@ A Map parameter can be annotated with `QueryMap` to construct a query that uses
```java
@RequestLine("GET /find")
V find(@QueryMap Map<String, Object>);
V find(@QueryMap Map<String, Object> queryMap);
```

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

@ -114,6 +114,11 @@ public interface Contract { @@ -114,6 +114,11 @@ public interface Contract {
}
}
if (data.headerMapIndex() != null) {
checkState(Map.class.isAssignableFrom(parameterTypes[data.headerMapIndex()]),
"HeaderMap parameter must be a Map: %s", parameterTypes[data.headerMapIndex()]);
}
if (data.queryMapIndex() != null) {
checkState(Map.class.isAssignableFrom(parameterTypes[data.queryMapIndex()]),
"QueryMap parameter must be a Map: %s", parameterTypes[data.queryMapIndex()]);
@ -258,6 +263,10 @@ public interface Contract { @@ -258,6 +263,10 @@ public interface Contract {
checkState(data.queryMapIndex() == null, "QueryMap annotation was present on multiple parameters.");
data.queryMapIndex(paramIndex);
isHttpAnnotation = true;
} else if (annotationType == HeaderMap.class) {
checkState(data.queryMapIndex() == null, "HeaderMap annotation was present on multiple parameters.");
data.headerMapIndex(paramIndex);
isHttpAnnotation = true;
}
}
return isHttpAnnotation;

55
core/src/main/java/feign/HeaderMap.java

@ -0,0 +1,55 @@ @@ -0,0 +1,55 @@
package feign;
import java.lang.annotation.Retention;
import java.util.List;
import java.util.Map;
import static java.lang.annotation.ElementType.PARAMETER;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
/**
* A template parameter that can be applied to a Map that contains header
* entries, where the keys are Strings that are the header field names and the
* values are the header field values. The headers specified by the map will be
* applied to the request after all other processing, and will take precedence
* over any previously specified header parameters.
* <br>
* This parameter is useful in cases where different header fields and values
* need to be set on an API method on a per-request basis in a thread-safe manner
* and independently of Feign client construction. A concrete example of a case
* like this are custom metadata header fields (e.g. as "x-amz-meta-*" or
* "x-goog-meta-*") where the header field names are dynamic and the range of keys
* cannot be determined a priori. The {@link Headers} annotation does not allow this
* because the header fields that it defines are static (it is not possible to add or
* remove fields on a per-request basis), and doing this using a custom {@link Target}
* or {@link RequestInterceptor} can be cumbersome (it requires more code for per-method
* customization, it is difficult to implement in a thread-safe manner and it requires
* customization when the Feign client for the API is built).
* <br>
* <pre>
* ...
* &#64;RequestLine("GET /servers/{serverId}")
* void get(&#64;Param("serverId") String serverId, &#64;HeaderMap Map<String, Object>);
* ...
* </pre>
* The annotated parameter must be an instance of {@link Map}, and the keys must
* be Strings. The header field value of a key will be the value of its toString
* method, except in the following cases:
* <br>
* <br>
* <ul>
* <li>if the value is null, the value will remain null (rather than converting
* to the String "null")
* <li>if the value is an {@link Iterable}, it is converted to a {@link List} of
* String objects where each value in the list is either null if the original
* value was null or the value's toString representation otherwise.
* </ul>
* <br>
* Once this conversion is applied, the query keys and resulting String values
* follow the same contract as if they were set using
* {@link RequestTemplate#header(String, String...)}.
*/
@Retention(RUNTIME)
@java.lang.annotation.Target(PARAMETER)
public @interface HeaderMap {
}

10
core/src/main/java/feign/MethodMetadata.java

@ -32,6 +32,7 @@ public final class MethodMetadata implements Serializable { @@ -32,6 +32,7 @@ public final class MethodMetadata implements Serializable {
private transient Type returnType;
private Integer urlIndex;
private Integer bodyIndex;
private Integer headerMapIndex;
private Integer queryMapIndex;
private transient Type bodyType;
private RequestTemplate template = new RequestTemplate();
@ -84,6 +85,15 @@ public final class MethodMetadata implements Serializable { @@ -84,6 +85,15 @@ public final class MethodMetadata implements Serializable {
return this;
}
public Integer headerMapIndex() {
return headerMapIndex;
}
public MethodMetadata headerMapIndex(Integer headerMapIndex) {
this.headerMapIndex = headerMapIndex;
return this;
}
public Integer queryMapIndex() {
return queryMapIndex;
}

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

@ -211,9 +211,37 @@ public class ReflectiveFeign extends Feign { @@ -211,9 +211,37 @@ public class ReflectiveFeign extends Feign {
template = addQueryMapQueryParameters(argv, template);
}
if (metadata.headerMapIndex() != null) {
template = addHeaderMapHeaders(argv, template);
}
return template;
}
@SuppressWarnings("unchecked")
private RequestTemplate addHeaderMapHeaders(Object[] argv, RequestTemplate mutable) {
Map<Object, Object> headerMap = (Map<Object, Object>) argv[metadata.headerMapIndex()];
for (Entry<Object, Object> currEntry : headerMap.entrySet()) {
checkState(currEntry.getKey().getClass() == String.class, "HeaderMap key must be a String: %s", currEntry.getKey());
Collection<String> values = new ArrayList<String>();
Object currValue = currEntry.getValue();
if (currValue instanceof Iterable<?>) {
Iterator<?> iter = ((Iterable<?>) currValue).iterator();
while (iter.hasNext()) {
Object nextObject = iter.next();
values.add(nextObject == null ? null : nextObject.toString());
}
} else {
values.add(currValue == null ? null : currValue.toString());
}
mutable.header((String) currEntry.getKey(), values);
}
return mutable;
}
@SuppressWarnings("unchecked")
private RequestTemplate addQueryMapQueryParameters(Object[] argv, RequestTemplate mutable) {
Map<Object, Object> queryMap = (Map<Object, Object>) argv[metadata.queryMapIndex()];

54
core/src/test/java/feign/FeignTest.java

@ -26,6 +26,7 @@ import java.util.Collection; @@ -26,6 +26,7 @@ import java.util.Collection;
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;
import org.junit.rules.ExpectedException;
@ -216,6 +217,52 @@ public class FeignTest { @@ -216,6 +217,52 @@ public class FeignTest {
.hasPath("/?date=1234");
}
@Test
public void headerMap() throws Exception {
server.enqueue(new MockResponse());
TestInterface api = new TestInterfaceBuilder().target("http://localhost:" + server.getPort());
Map<String, Object> headerMap = new LinkedHashMap<String, Object>();
headerMap.put("Content-Type", "myContent");
headerMap.put("Custom-Header", "fooValue");
api.headerMap(headerMap);
assertThat(server.takeRequest())
.hasHeaders(
MapEntry.entry("Content-Type", Arrays.asList("myContent")),
MapEntry.entry("Custom-Header", Arrays.asList("fooValue")));
}
@Test
public void headerMapWithHeaderAnnotations() throws Exception {
server.enqueue(new MockResponse());
TestInterface api = new TestInterfaceBuilder().target("http://localhost:" + server.getPort());
Map<String, Object> headerMap = new LinkedHashMap<String, Object>();
headerMap.put("Custom-Header", "fooValue");
api.headerMapWithHeaderAnnotations(headerMap);
// header map should be additive for headers provided by annotations
assertThat(server.takeRequest())
.hasHeaders(
MapEntry.entry("Content-Encoding", Arrays.asList("deflate")),
MapEntry.entry("Custom-Header", Arrays.asList("fooValue")));
server.enqueue(new MockResponse());
headerMap.put("Content-Encoding", "overrideFromMap");
api.headerMapWithHeaderAnnotations(headerMap);
// if header map has entry that collides with annotation, value specified
// by header map should be used
assertThat(server.takeRequest())
.hasHeaders(
MapEntry.entry("Content-Encoding", Arrays.asList("overrideFromMap")),
MapEntry.entry("Custom-Header", Arrays.asList("fooValue")));
}
@Test
public void queryMap() throws Exception {
server.enqueue(new MockResponse());
@ -609,6 +656,13 @@ public class FeignTest { @@ -609,6 +656,13 @@ public class FeignTest {
@RequestLine("POST /?date={date}")
void expand(@Param(value = "date", expander = DateToMillis.class) Date date);
@RequestLine("GET /")
void headerMap(@HeaderMap Map<String, Object> headerMap);
@RequestLine("GET /")
@Headers("Content-Encoding: deflate")
void headerMapWithHeaderAnnotations(@HeaderMap Map<String, Object> headerMap);
@RequestLine("GET /")
void queryMap(@QueryMap Map<String, Object> queryMap);

Loading…
Cancel
Save