From 31d2f65528f97cf4a9f5c5cdc50d8eb4112ec9bc Mon Sep 17 00:00:00 2001 From: Jakub Narloch Date: Fri, 30 Oct 2015 21:25:51 +0100 Subject: [PATCH] Feign annotated parameter processors --- .../feign/AnnotatedParameterProcessor.java | 84 ++++++++++ .../feign/FeignClientsConfiguration.java | 8 +- .../PathVariableParameterProcessor.java | 75 +++++++++ .../RequestHeaderParameterProcessor.java | 57 +++++++ .../RequestParamParameterProcessor.java | 57 +++++++ .../feign/support/SpringMvcContract.java | 158 ++++++++++-------- .../feign/support/SpringMvcContractTest.java | 11 +- 7 files changed, 376 insertions(+), 74 deletions(-) create mode 100644 spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/feign/AnnotatedParameterProcessor.java create mode 100644 spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/feign/annotation/PathVariableParameterProcessor.java create mode 100644 spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/feign/annotation/RequestHeaderParameterProcessor.java create mode 100644 spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/feign/annotation/RequestParamParameterProcessor.java diff --git a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/feign/AnnotatedParameterProcessor.java b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/feign/AnnotatedParameterProcessor.java new file mode 100644 index 00000000..acfccef0 --- /dev/null +++ b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/feign/AnnotatedParameterProcessor.java @@ -0,0 +1,84 @@ +/* + * 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 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 setTemplateParameter(String name, Collection rest); + } +} diff --git a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/feign/FeignClientsConfiguration.java b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/feign/FeignClientsConfiguration.java index 6e3dcda8..95346280 100644 --- a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/feign/FeignClientsConfiguration.java +++ b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/feign/FeignClientsConfiguration.java @@ -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 messageConverters; + @Autowired(required = false) + private List 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 diff --git a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/feign/annotation/PathVariableParameterProcessor.java b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/feign/annotation/PathVariableParameterProcessor.java new file mode 100644 index 00000000..cb7848ba --- /dev/null +++ b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/feign/annotation/PathVariableParameterProcessor.java @@ -0,0 +1,75 @@ +/* + * 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 ANNOTATION = PathVariable.class; + + @Override + public Class 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 boolean searchMapValues(Map> map, V search) { + Collection> values = map.values(); + if (values == null) { + return false; + } + for (Collection entry : values) { + if (entry.contains(search)) { + return true; + } + } + return false; + } +} diff --git a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/feign/annotation/RequestHeaderParameterProcessor.java b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/feign/annotation/RequestHeaderParameterProcessor.java new file mode 100644 index 00000000..a5a3942d --- /dev/null +++ b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/feign/annotation/RequestHeaderParameterProcessor.java @@ -0,0 +1,57 @@ +/* + * 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 ANNOTATION = RequestHeader.class; + + @Override + public Class 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 header = context.setTemplateParameter(name, data.template().headers().get(name)); + data.template().header(name, header); + return true; + } +} diff --git a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/feign/annotation/RequestParamParameterProcessor.java b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/feign/annotation/RequestParamParameterProcessor.java new file mode 100644 index 00000000..edd2b226 --- /dev/null +++ b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/feign/annotation/RequestParamParameterProcessor.java @@ -0,0 +1,57 @@ +/* + * 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 ANNOTATION = RequestParam.class; + + @Override + public Class 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 query = context.setTemplateParameter(name, data.template().queries().get(name)); + data.template().query(name, query); + return true; + } +} diff --git a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/feign/support/SpringMvcContract.java b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/feign/support/SpringMvcContract.java index d36d1359..ba7e5922 100644 --- a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/feign/support/SpringMvcContract.java +++ b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/feign/support/SpringMvcContract.java @@ -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, AnnotatedParameterProcessor> annotatedArgumentProcessors; + + public SpringMvcContract() { + this(Collections.emptyList()); + } + + public SpringMvcContract(List annotatedParameterProcessors) { + Assert.notNull(annotatedParameterProcessors, "Parameter processors can not be null."); + + List 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? + + AnnotatedParameterProcessor.AnnotatedParameterContext context = + new SimpleAnnotatedParameterContext(data, paramIndex); 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 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 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 processor = + annotatedArgumentProcessors.get(parameterAnnotation.annotationType()); + isHttpAnnotation |= processor.processArgument(context, parameterAnnotation); } return isHttpAnnotation; } - private boolean searchMapValues(Map> map, V search) { - Collection> values = map.values(); - if (values == null) { - return false; - } - for (Collection 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, AnnotatedParameterProcessor> toAnnotatedArgumentProcessorMap(List processors) { + Map, AnnotatedParameterProcessor> result = new HashMap<>(); + for(AnnotatedParameterProcessor processor : processors) { + result.put(processor.getAnnotationType(), processor); + } + return result; + } + + private List getDefaultAnnotatedArgumentsProcessors() { + + List 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 setTemplateParameter(String name, Collection rest) { + return addTemplatedParam(rest, name); + } + } + } diff --git a/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/feign/support/SpringMvcContractTest.java b/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/feign/support/SpringMvcContractTest.java index a7668575..4d0b80a5 100644 --- a/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/feign/support/SpringMvcContractTest.java +++ b/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/feign/support/SpringMvcContractTest.java @@ -1,13 +1,7 @@ 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