Commit 4fdb85e6 by Spencer Gibb

Merge pull request #619 from jmnarloch/feign-parameter-processors

* feign-parameter-processors: Feign annotated parameter processors
parents 50efa853 31d2f655
/*
* Copyright 2013-2015 the original author or 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 org.springframework.cloud.netflix.feign;
import java.lang.annotation.Annotation;
import java.util.Collection;
import feign.MethodMetadata;
/**
* Feign contract method parameter processor.
*
* @author Jakub Narloch
*/
public interface AnnotatedParameterProcessor {
/**
* Retrieves the processor supported annotation type.
*
* @return the annotation type
*/
Class<? extends Annotation> getAnnotationType();
/**
* Process the annotated parameter.
*
* @param context the parameter context
* @param annotation the annotation instance
* @return whether the parameter is http
*/
boolean processArgument(AnnotatedParameterContext context, Annotation annotation);
/**
* Specifies the parameter context.
*
* @author Jakub Narloch
*/
interface AnnotatedParameterContext {
/**
* Retrieves the method metadata.
*
* @return the method metadata
*/
MethodMetadata getMethodMetadata();
/**
* Retrieves the index of the parameter.
*
* @return the parameter index
*/
int getParameterIndex();
/**
* Sets the parameter name.
*
* @param name the name of the parameter
*/
void setParameterName(String name);
/**
* Sets the template parameter.
*
* @param name the template parameter
* @param rest the existing parameter values
* @return parameters
*/
Collection<String> setTemplateParameter(String name, Collection<String> rest);
}
}
......@@ -16,6 +16,9 @@
package org.springframework.cloud.netflix.feign;
import java.util.ArrayList;
import java.util.List;
import org.apache.http.client.HttpClient;
import org.springframework.beans.factory.ObjectFactory;
import org.springframework.beans.factory.annotation.Autowired;
......@@ -44,6 +47,9 @@ public class FeignClientsConfiguration {
@Autowired
private ObjectFactory<HttpMessageConverters> messageConverters;
@Autowired(required = false)
private List<AnnotatedParameterProcessor> parameterProcessors = new ArrayList<>();
@Bean
@ConditionalOnMissingBean
public Decoder feignDecoder() {
......@@ -59,7 +65,7 @@ public class FeignClientsConfiguration {
@Bean
@ConditionalOnMissingBean
public Contract feignContract() {
return new SpringMvcContract();
return new SpringMvcContract(parameterProcessors);
}
@Configuration
......
/*
* Copyright 2013-2015 the original author or 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 org.springframework.cloud.netflix.feign.annotation;
import java.lang.annotation.Annotation;
import java.util.Collection;
import java.util.Map;
import org.springframework.cloud.netflix.feign.AnnotatedParameterProcessor;
import org.springframework.web.bind.annotation.PathVariable;
import feign.MethodMetadata;
import static feign.Util.checkState;
import static feign.Util.emptyToNull;
/**
* {@link PathVariable} parameter processor.
*
* @author Jakub Narloch
* @see AnnotatedParameterProcessor
*/
public class PathVariableParameterProcessor implements AnnotatedParameterProcessor {
private static final Class<PathVariable> ANNOTATION = PathVariable.class;
@Override
public Class<? extends Annotation> getAnnotationType() {
return ANNOTATION;
}
@Override
public boolean processArgument(AnnotatedParameterContext context, Annotation annotation) {
String name = ANNOTATION.cast(annotation).value();
checkState(emptyToNull(name) != null,
"PathVariable annotation was empty on param %s.", context.getParameterIndex());
context.setParameterName(name);
MethodMetadata data = context.getMethodMetadata();
String varName = '{' + name + '}';
if (!data.template().url().contains(varName)
&& !searchMapValues(data.template().queries(), varName)
&& !searchMapValues(data.template().headers(), varName)) {
data.formParams().add(name);
}
return true;
}
private <K, V> boolean searchMapValues(Map<K, Collection<V>> map, V search) {
Collection<Collection<V>> values = map.values();
if (values == null) {
return false;
}
for (Collection<V> entry : values) {
if (entry.contains(search)) {
return true;
}
}
return false;
}
}
/*
* Copyright 2013-2015 the original author or 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 org.springframework.cloud.netflix.feign.annotation;
import java.lang.annotation.Annotation;
import java.util.Collection;
import org.springframework.cloud.netflix.feign.AnnotatedParameterProcessor;
import org.springframework.web.bind.annotation.RequestHeader;
import feign.MethodMetadata;
import static feign.Util.checkState;
import static feign.Util.emptyToNull;
/**
* {@link RequestHeader} parameter processor.
*
* @author Jakub Narloch
* @see AnnotatedParameterProcessor
*/
public class RequestHeaderParameterProcessor implements AnnotatedParameterProcessor {
private static final Class<RequestHeader> ANNOTATION = RequestHeader.class;
@Override
public Class<? extends Annotation> getAnnotationType() {
return ANNOTATION;
}
@Override
public boolean processArgument(AnnotatedParameterContext context, Annotation annotation) {
String name = ANNOTATION.cast(annotation).value();
checkState(emptyToNull(name) != null,
"RequestHeader.value() was empty on parameter %s", context.getParameterIndex());
context.setParameterName(name);
MethodMetadata data = context.getMethodMetadata();
Collection<String> header = context.setTemplateParameter(name, data.template().headers().get(name));
data.template().header(name, header);
return true;
}
}
/*
* Copyright 2013-2015 the original author or 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 org.springframework.cloud.netflix.feign.annotation;
import java.lang.annotation.Annotation;
import java.util.Collection;
import org.springframework.cloud.netflix.feign.AnnotatedParameterProcessor;
import org.springframework.web.bind.annotation.RequestParam;
import feign.MethodMetadata;
import static feign.Util.checkState;
import static feign.Util.emptyToNull;
/**
* {@link RequestParam} parameter processor.
*
* @author Jakub Narloch
* @see AnnotatedParameterProcessor
*/
public class RequestParamParameterProcessor implements AnnotatedParameterProcessor {
private static final Class<RequestParam> ANNOTATION = RequestParam.class;
@Override
public Class<? extends Annotation> getAnnotationType() {
return ANNOTATION;
}
@Override
public boolean processArgument(AnnotatedParameterContext context, Annotation annotation) {
String name = ANNOTATION.cast(annotation).value();
checkState(emptyToNull(name) != null,
"RequestParam.value() was empty on parameter %s", context.getParameterIndex());
context.setParameterName(name);
MethodMetadata data = context.getMethodMetadata();
Collection<String> query = context.setTemplateParameter(name, data.template().queries().get(name));
data.template().query(name, query);
return true;
}
}
......@@ -16,19 +16,26 @@
package org.springframework.cloud.netflix.feign.support;
import feign.Contract;
import feign.MethodMetadata;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.springframework.cloud.netflix.feign.AnnotatedParameterProcessor;
import org.springframework.cloud.netflix.feign.annotation.PathVariableParameterProcessor;
import org.springframework.cloud.netflix.feign.annotation.RequestHeaderParameterProcessor;
import org.springframework.cloud.netflix.feign.annotation.RequestParamParameterProcessor;
import org.springframework.util.Assert;
import org.springframework.web.bind.annotation.RequestMapping;
import feign.Contract;
import feign.MethodMetadata;
import static feign.Util.checkState;
import static feign.Util.emptyToNull;
......@@ -41,6 +48,24 @@ public class SpringMvcContract extends Contract.BaseContract {
private static final String CONTENT_TYPE = "Content-Type";
private final Map<Class<? extends Annotation>, AnnotatedParameterProcessor> annotatedArgumentProcessors;
public SpringMvcContract() {
this(Collections.<AnnotatedParameterProcessor>emptyList());
}
public SpringMvcContract(List<AnnotatedParameterProcessor> annotatedParameterProcessors) {
Assert.notNull(annotatedParameterProcessors, "Parameter processors can not be null.");
List<AnnotatedParameterProcessor> processors;
if(!annotatedParameterProcessors.isEmpty()) {
processors = new ArrayList<>(annotatedParameterProcessors);
} else {
processors = getDefaultAnnotatedArgumentsProcessors();
}
this.annotatedArgumentProcessors = toAnnotatedArgumentProcessorMap(processors);
}
@Override
public MethodMetadata parseAndValidateMetadata(Class<?> targetType, Method method) {
MethodMetadata md = super.parseAndValidateMetadata(targetType, method);
......@@ -109,6 +134,7 @@ public class SpringMvcContract extends Contract.BaseContract {
parseHeaders(data, method, methodMapping);
}
private void checkAtMostOne(Method method, Object[] values, String fieldName) {
checkState(values != null && (values.length == 0 || values.length == 1),
"Method %s can only contain at most 1 %s field. Found: %s",
......@@ -123,72 +149,19 @@ public class SpringMvcContract extends Contract.BaseContract {
}
@Override
protected boolean processAnnotationsOnParameter(MethodMetadata data,
Annotation[] annotations, int paramIndex) {
protected boolean processAnnotationsOnParameter(MethodMetadata data, Annotation[] annotations, int paramIndex) {
boolean isHttpAnnotation = false;
// TODO: support spring parameter annotations?
for (Annotation parameterAnnotation : annotations) {
if (parameterAnnotation instanceof PathVariable) {
String name = PathVariable.class.cast(parameterAnnotation).value();
checkState(emptyToNull(name) != null,
"PathVariable annotation was empty on param %s.", paramIndex);
nameParam(data, name, paramIndex);
isHttpAnnotation = true;
String varName = '{' + name + '}';
if (data.template().url().indexOf(varName) == -1
&& !searchMapValues(data.template().queries(), varName)
&& !searchMapValues(data.template().headers(), varName)) {
data.formParams().add(name);
}
}
else if (parameterAnnotation instanceof RequestParam) {
String name = RequestParam.class.cast(parameterAnnotation).value();
checkState(emptyToNull(name) != null,
"QueryParam.value() was empty on parameter %s", paramIndex);
Collection<String> query = addTemplatedParam(data.template().queries()
.get(name), name);
data.template().query(name, query);
nameParam(data, name, paramIndex);
isHttpAnnotation = true;
}
else if (parameterAnnotation instanceof RequestHeader) {
String name = RequestHeader.class.cast(parameterAnnotation).value();
checkState(emptyToNull(name) != null,
"HeaderParam.value() was empty on parameter %s", paramIndex);
Collection<String> header = addTemplatedParam(data.template().headers()
.get(name), name);
data.template().header(name, header);
nameParam(data, name, paramIndex);
isHttpAnnotation = true;
}
// TODO
/*
* else if (annotationType == FormParam.class) { String name =
* FormParam.class.cast(parameterAnnotation).value();
* checkState(emptyToNull(name) != null,
* "FormParam.value() was empty on parameter %s", paramIndex);
* data.formParams().add(name); nameParam(data, name, paramIndex);
* isHttpAnnotation = true; }
*/
AnnotatedParameterProcessor.AnnotatedParameterContext context =
new SimpleAnnotatedParameterContext(data, paramIndex);
for (Annotation parameterAnnotation : annotations) {
AnnotatedParameterProcessor processor =
annotatedArgumentProcessors.get(parameterAnnotation.annotationType());
isHttpAnnotation |= processor.processArgument(context, parameterAnnotation);
}
return isHttpAnnotation;
}
private <K, V> boolean searchMapValues(Map<K, Collection<V>> map, V search) {
Collection<Collection<V>> values = map.values();
if (values == null) {
return false;
}
for (Collection<V> entry : values) {
if (entry.contains(search)) {
return true;
}
}
return false;
}
private void parseProduces(MethodMetadata md, Method method, RequestMapping annotation) {
checkAtMostOne(method, annotation.produces(), "produces");
String[] serverProduces = annotation.produces();
......@@ -220,4 +193,55 @@ public class SpringMvcContract extends Contract.BaseContract {
}
}
private Map<Class<? extends Annotation>, AnnotatedParameterProcessor> toAnnotatedArgumentProcessorMap(List<AnnotatedParameterProcessor> processors) {
Map<Class<? extends Annotation>, AnnotatedParameterProcessor> result = new HashMap<>();
for(AnnotatedParameterProcessor processor : processors) {
result.put(processor.getAnnotationType(), processor);
}
return result;
}
private List<AnnotatedParameterProcessor> getDefaultAnnotatedArgumentsProcessors() {
List<AnnotatedParameterProcessor> annotatedArgumentResolvers = new ArrayList<>();
annotatedArgumentResolvers.add(new PathVariableParameterProcessor());
annotatedArgumentResolvers.add(new RequestParamParameterProcessor());
annotatedArgumentResolvers.add(new RequestHeaderParameterProcessor());
return annotatedArgumentResolvers;
}
private class SimpleAnnotatedParameterContext implements AnnotatedParameterProcessor.AnnotatedParameterContext {
private final MethodMetadata methodMetadata;
private final int parameterIndex;
public SimpleAnnotatedParameterContext(MethodMetadata methodMetadata, int parameterIndex) {
this.methodMetadata = methodMetadata;
this.parameterIndex = parameterIndex;
}
@Override
public MethodMetadata getMethodMetadata() {
return methodMetadata;
}
@Override
public int getParameterIndex() {
return parameterIndex;
}
@Override
public void setParameterName(String name) {
nameParam(methodMetadata, name, parameterIndex);
}
@Override
public Collection<String> setTemplateParameter(String name, Collection<String> rest) {
return addTemplatedParam(rest, name);
}
}
}
package org.springframework.cloud.netflix.feign.support;
import static org.junit.Assert.assertEquals;
import java.lang.reflect.Method;
import lombok.AllArgsConstructor;
import lombok.NoArgsConstructor;
import lombok.ToString;
import org.junit.Before;
import org.junit.Test;
import org.springframework.http.MediaType;
......@@ -22,6 +16,11 @@ import org.springframework.web.bind.annotation.RequestParam;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import feign.MethodMetadata;
import lombok.AllArgsConstructor;
import lombok.NoArgsConstructor;
import lombok.ToString;
import static org.junit.Assert.assertEquals;
/**
* @author chadjaros
......
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