Commit 45d769b0 by Abhijit Sarkar Committed by Spencer Gibb

Support header map and query map (#1361)

Adds support for Map types in feign for `@RequestHeader` and `@RequestParam`. Fixes gh-1360
parent af032476
...@@ -17,6 +17,7 @@ ...@@ -17,6 +17,7 @@
package org.springframework.cloud.netflix.feign; package org.springframework.cloud.netflix.feign;
import java.lang.annotation.Annotation; import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.Collection; import java.util.Collection;
import feign.MethodMetadata; import feign.MethodMetadata;
...@@ -25,6 +26,7 @@ import feign.MethodMetadata; ...@@ -25,6 +26,7 @@ import feign.MethodMetadata;
* Feign contract method parameter processor. * Feign contract method parameter processor.
* *
* @author Jakub Narloch * @author Jakub Narloch
* @author Abhijit Sarkar
*/ */
public interface AnnotatedParameterProcessor { public interface AnnotatedParameterProcessor {
...@@ -40,9 +42,10 @@ public interface AnnotatedParameterProcessor { ...@@ -40,9 +42,10 @@ public interface AnnotatedParameterProcessor {
* *
* @param context the parameter context * @param context the parameter context
* @param annotation the annotation instance * @param annotation the annotation instance
* @param method the method that contains the annotation
* @return whether the parameter is http * @return whether the parameter is http
*/ */
boolean processArgument(AnnotatedParameterContext context, Annotation annotation); boolean processArgument(AnnotatedParameterContext context, Annotation annotation, Method method);
/** /**
* Specifies the parameter context. * Specifies the parameter context.
......
...@@ -17,6 +17,7 @@ ...@@ -17,6 +17,7 @@
package org.springframework.cloud.netflix.feign.annotation; package org.springframework.cloud.netflix.feign.annotation;
import java.lang.annotation.Annotation; import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.Collection; import java.util.Collection;
import java.util.Map; import java.util.Map;
...@@ -32,6 +33,7 @@ import static feign.Util.emptyToNull; ...@@ -32,6 +33,7 @@ import static feign.Util.emptyToNull;
* {@link PathVariable} parameter processor. * {@link PathVariable} parameter processor.
* *
* @author Jakub Narloch * @author Jakub Narloch
* @author Abhijit Sarkar
* @see AnnotatedParameterProcessor * @see AnnotatedParameterProcessor
*/ */
public class PathVariableParameterProcessor implements AnnotatedParameterProcessor { public class PathVariableParameterProcessor implements AnnotatedParameterProcessor {
...@@ -44,7 +46,7 @@ public class PathVariableParameterProcessor implements AnnotatedParameterProcess ...@@ -44,7 +46,7 @@ public class PathVariableParameterProcessor implements AnnotatedParameterProcess
} }
@Override @Override
public boolean processArgument(AnnotatedParameterContext context, Annotation annotation) { public boolean processArgument(AnnotatedParameterContext context, Annotation annotation, Method method) {
String name = ANNOTATION.cast(annotation).value(); String name = ANNOTATION.cast(annotation).value();
checkState(emptyToNull(name) != null, checkState(emptyToNull(name) != null,
"PathVariable annotation was empty on param %s.", context.getParameterIndex()); "PathVariable annotation was empty on param %s.", context.getParameterIndex());
......
...@@ -17,7 +17,9 @@ ...@@ -17,7 +17,9 @@
package org.springframework.cloud.netflix.feign.annotation; package org.springframework.cloud.netflix.feign.annotation;
import java.lang.annotation.Annotation; import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.Collection; import java.util.Collection;
import java.util.Map;
import org.springframework.cloud.netflix.feign.AnnotatedParameterProcessor; import org.springframework.cloud.netflix.feign.AnnotatedParameterProcessor;
import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RequestHeader;
...@@ -31,6 +33,7 @@ import static feign.Util.emptyToNull; ...@@ -31,6 +33,7 @@ import static feign.Util.emptyToNull;
* {@link RequestHeader} parameter processor. * {@link RequestHeader} parameter processor.
* *
* @author Jakub Narloch * @author Jakub Narloch
* @author Abhijit Sarkar
* @see AnnotatedParameterProcessor * @see AnnotatedParameterProcessor
*/ */
public class RequestHeaderParameterProcessor implements AnnotatedParameterProcessor { public class RequestHeaderParameterProcessor implements AnnotatedParameterProcessor {
...@@ -43,13 +46,23 @@ public class RequestHeaderParameterProcessor implements AnnotatedParameterProces ...@@ -43,13 +46,23 @@ public class RequestHeaderParameterProcessor implements AnnotatedParameterProces
} }
@Override @Override
public boolean processArgument(AnnotatedParameterContext context, Annotation annotation) { public boolean processArgument(AnnotatedParameterContext context, Annotation annotation, Method method) {
int parameterIndex = context.getParameterIndex();
Class<?> parameterType = method.getParameterTypes()[parameterIndex];
MethodMetadata data = context.getMethodMetadata();
if (Map.class.isAssignableFrom(parameterType)) {
checkState(data.headerMapIndex() == null, "Header map can only be present once.");
data.headerMapIndex(parameterIndex);
return true;
}
String name = ANNOTATION.cast(annotation).value(); String name = ANNOTATION.cast(annotation).value();
checkState(emptyToNull(name) != null, checkState(emptyToNull(name) != null,
"RequestHeader.value() was empty on parameter %s", context.getParameterIndex()); "RequestHeader.value() was empty on parameter %s", parameterIndex);
context.setParameterName(name); context.setParameterName(name);
MethodMetadata data = context.getMethodMetadata();
Collection<String> header = context.setTemplateParameter(name, data.template().headers().get(name)); Collection<String> header = context.setTemplateParameter(name, data.template().headers().get(name));
data.template().header(name, header); data.template().header(name, header);
return true; return true;
......
...@@ -17,7 +17,9 @@ ...@@ -17,7 +17,9 @@
package org.springframework.cloud.netflix.feign.annotation; package org.springframework.cloud.netflix.feign.annotation;
import java.lang.annotation.Annotation; import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.Collection; import java.util.Collection;
import java.util.Map;
import org.springframework.cloud.netflix.feign.AnnotatedParameterProcessor; import org.springframework.cloud.netflix.feign.AnnotatedParameterProcessor;
import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RequestParam;
...@@ -31,6 +33,7 @@ import feign.MethodMetadata; ...@@ -31,6 +33,7 @@ import feign.MethodMetadata;
* {@link RequestParam} parameter processor. * {@link RequestParam} parameter processor.
* *
* @author Jakub Narloch * @author Jakub Narloch
* @author Abhijit Sarkar
* @see AnnotatedParameterProcessor * @see AnnotatedParameterProcessor
*/ */
public class RequestParamParameterProcessor implements AnnotatedParameterProcessor { public class RequestParamParameterProcessor implements AnnotatedParameterProcessor {
...@@ -43,22 +46,28 @@ public class RequestParamParameterProcessor implements AnnotatedParameterProcess ...@@ -43,22 +46,28 @@ public class RequestParamParameterProcessor implements AnnotatedParameterProcess
} }
@Override @Override
public boolean processArgument(AnnotatedParameterContext context, public boolean processArgument(AnnotatedParameterContext context, Annotation annotation, Method method) {
Annotation annotation) { int parameterIndex = context.getParameterIndex();
Class<?> parameterType = method.getParameterTypes()[parameterIndex];
MethodMetadata data = context.getMethodMetadata();
if (Map.class.isAssignableFrom(parameterType)) {
checkState(data.queryMapIndex() == null, "Query map can only be present once.");
data.queryMapIndex(parameterIndex);
return true;
}
RequestParam requestParam = ANNOTATION.cast(annotation); RequestParam requestParam = ANNOTATION.cast(annotation);
String name = requestParam.value(); String name = requestParam.value();
if (emptyToNull(name) != null) { checkState(emptyToNull(name) != null,
"RequestParam.value() was empty on parameter %s",
parameterIndex);
context.setParameterName(name); context.setParameterName(name);
MethodMetadata data = context.getMethodMetadata();
Collection<String> query = context.setTemplateParameter(name, Collection<String> query = context.setTemplateParameter(name,
data.template().queries().get(name)); data.template().queries().get(name));
data.template().query(name, query); data.template().query(name, query);
} else {
// supports `Map` types
MethodMetadata data = context.getMethodMetadata();
data.queryMapIndex(context.getParameterIndex());
}
return true; return true;
} }
} }
...@@ -58,6 +58,7 @@ import feign.Param; ...@@ -58,6 +58,7 @@ import feign.Param;
/** /**
* @author Spencer Gibb * @author Spencer Gibb
* @author Abhijit Sarkar
*/ */
public class SpringMvcContract extends Contract.BaseContract public class SpringMvcContract extends Contract.BaseContract
implements ResourceLoaderAware { implements ResourceLoaderAware {
...@@ -235,7 +236,7 @@ public class SpringMvcContract extends Contract.BaseContract ...@@ -235,7 +236,7 @@ public class SpringMvcContract extends Contract.BaseContract
processParameterAnnotation = synthesizeWithMethodParameterNameAsFallbackValue( processParameterAnnotation = synthesizeWithMethodParameterNameAsFallbackValue(
parameterAnnotation, method, paramIndex); parameterAnnotation, method, paramIndex);
isHttpAnnotation |= processor.processArgument(context, isHttpAnnotation |= processor.processArgument(context,
processParameterAnnotation); processParameterAnnotation, method);
} }
} }
if (isHttpAnnotation && data.indexToExpander().get(paramIndex) == null if (isHttpAnnotation && data.indexToExpander().get(paramIndex) == null
......
...@@ -18,6 +18,8 @@ package org.springframework.cloud.netflix.feign.support; ...@@ -18,6 +18,8 @@ package org.springframework.cloud.netflix.feign.support;
import java.lang.reflect.InvocationTargetException; import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.text.CollationElementIterator;
import java.util.Collection;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
...@@ -25,6 +27,7 @@ import org.junit.Before; ...@@ -25,6 +27,7 @@ import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.util.MultiValueMap;
import org.springframework.util.ReflectionUtils; import org.springframework.util.ReflectionUtils;
import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PathVariable;
...@@ -340,6 +343,48 @@ public class SpringMvcContractTests { ...@@ -340,6 +343,48 @@ public class SpringMvcContractTests {
return false; return false;
} }
@Test
public void testProcessHeaderMap() throws Exception {
Method method = TestTemplate_HeaderMap.class.getDeclaredMethod("headerMap",
MultiValueMap.class, String.class);
MethodMetadata data = this.contract
.parseAndValidateMetadata(method.getDeclaringClass(), method);
assertEquals("/headerMap", data.template().url());
assertEquals("GET", data.template().method());
assertEquals(0, data.headerMapIndex().intValue());
Map<String, Collection<String>> headers = data.template().headers();
assertEquals("{aHeader}", headers.get("aHeader").iterator().next());
}
@Test(expected = IllegalStateException.class)
public void testProcessHeaderMapMoreThanOnce() throws Exception {
Method method = TestTemplate_HeaderMap.class.getDeclaredMethod(
"headerMapMoreThanOnce", MultiValueMap.class, MultiValueMap.class);
this.contract.parseAndValidateMetadata(method.getDeclaringClass(), method);
}
@Test
public void testProcessQueryMap() throws Exception {
Method method = TestTemplate_QueryMap.class.getDeclaredMethod("queryMap",
MultiValueMap.class, String.class);
MethodMetadata data = this.contract
.parseAndValidateMetadata(method.getDeclaringClass(), method);
assertEquals("/queryMap", data.template().url());
assertEquals("GET", data.template().method());
assertEquals(0, data.queryMapIndex().intValue());
Map<String, Collection<String>> params = data.template().queries();
assertEquals("{aParam}", params.get("aParam").iterator().next());
}
@Test(expected = IllegalStateException.class)
public void testProcessQueryMapMoreThanOnce() throws Exception {
Method method = TestTemplate_QueryMap.class.getDeclaredMethod(
"queryMapMoreThanOnce", MultiValueMap.class, MultiValueMap.class);
this.contract.parseAndValidateMetadata(method.getDeclaringClass(), method);
}
public interface TestTemplate_Simple { public interface TestTemplate_Simple {
@RequestMapping(value = "/test/{id}", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE) @RequestMapping(value = "/test/{id}", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
ResponseEntity<TestObject> getTest(@PathVariable("id") String id); ResponseEntity<TestObject> getTest(@PathVariable("id") String id);
...@@ -380,6 +425,30 @@ public class SpringMvcContractTests { ...@@ -380,6 +425,30 @@ public class SpringMvcContractTests {
ResponseEntity<TestObject> getTest(@RequestParam Map<String, String> params); ResponseEntity<TestObject> getTest(@RequestParam Map<String, String> params);
} }
public interface TestTemplate_HeaderMap {
@RequestMapping(path = "/headerMap")
String headerMap(
@RequestHeader MultiValueMap<String, String> headerMap,
@RequestHeader(name = "aHeader") String aHeader);
@RequestMapping(path = "/headerMapMoreThanOnce")
String headerMapMoreThanOnce(
@RequestHeader MultiValueMap<String, String> headerMap1,
@RequestHeader MultiValueMap<String, String> headerMap2);
}
public interface TestTemplate_QueryMap {
@RequestMapping(path = "/queryMap")
String queryMap(
@RequestParam MultiValueMap<String, String> queryMap,
@RequestParam(name = "aParam") String aParam);
@RequestMapping(path = "/queryMapMoreThanOnce")
String queryMapMoreThanOnce(
@RequestParam MultiValueMap<String, String> queryMap1,
@RequestParam MultiValueMap<String, String> queryMap2);
}
@JsonAutoDetect @JsonAutoDetect
@RequestMapping("/advanced") @RequestMapping("/advanced")
public interface TestTemplate_Advanced { public interface TestTemplate_Advanced {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment