/* * Copyright 2014 Netflix, Inc. * * 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 com.netflix.ribbon.proxy; import com.netflix.ribbon.RibbonRequest; import com.netflix.ribbon.proxy.annotation.Content; import com.netflix.ribbon.proxy.annotation.ContentTransformerClass; import com.netflix.ribbon.proxy.annotation.TemplateName; import com.netflix.ribbon.proxy.annotation.Var; import io.netty.buffer.ByteBuf; import io.reactivex.netty.channel.ContentTransformer; import rx.Observable; import java.lang.annotation.Annotation; import java.lang.reflect.Method; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import java.util.ArrayList; import java.util.List; import static java.lang.String.format; /** * Extracts information from Ribbon annotated method, to automatically populate the Ribbon request template. * A few validations are performed as well: * - a return type must be {@link com.netflix.ribbon.RibbonRequest} * - HTTP method must be always specified explicitly (there are no defaults) * - only one parameter with {@link com.netflix.ribbon.proxy.annotation.Content} annotation is allowed * * @author Tomasz Bak */ class MethodTemplate { private final Method method; private final String templateName; private final String[] paramNames; private final int[] valueIdxs; private final int contentArgPosition; private final Class<? extends ContentTransformer<?>> contentTansformerClass; private final Class<?> resultType; private final Class<?> genericContentType; MethodTemplate(Method method) { this.method = method; MethodAnnotationValues values = new MethodAnnotationValues(method); templateName = values.templateName; paramNames = values.paramNames; valueIdxs = values.valueIdxs; contentArgPosition = values.contentArgPosition; contentTansformerClass = values.contentTansformerClass; resultType = values.resultType; genericContentType = values.genericContentType; } public String getTemplateName() { return templateName; } public Method getMethod() { return method; } public String getParamName(int idx) { return paramNames[idx]; } public int getParamPosition(int idx) { return valueIdxs[idx]; } public int getParamSize() { return paramNames.length; } public int getContentArgPosition() { return contentArgPosition; } public Class<? extends ContentTransformer<?>> getContentTransformerClass() { return contentTansformerClass; } public Class<?> getResultType() { return resultType; } public Class<?> getGenericContentType() { return genericContentType; } public static <T> MethodTemplate[] from(Class<T> clientInterface) { List<MethodTemplate> list = new ArrayList<MethodTemplate>(clientInterface.getMethods().length); for (Method m : clientInterface.getMethods()) { list.add(new MethodTemplate(m)); } return list.toArray(new MethodTemplate[list.size()]); } static class CacheProviderEntry { private final String key; private final com.netflix.ribbon.CacheProvider cacheProvider; CacheProviderEntry(String key, com.netflix.ribbon.CacheProvider cacheProvider) { this.key = key; this.cacheProvider = cacheProvider; } public String getKey() { return key; } public com.netflix.ribbon.CacheProvider getCacheProvider() { return cacheProvider; } } private static class MethodAnnotationValues { private final Method method; private String templateName; private String[] paramNames; private int[] valueIdxs; private int contentArgPosition; private Class<? extends ContentTransformer<?>> contentTansformerClass; private Class<?> resultType; private Class<?> genericContentType; private MethodAnnotationValues(Method method) { this.method = method; extractTemplateName(); extractParamNamesWithIndexes(); extractContentArgPosition(); extractContentTransformerClass(); extractResultType(); } private void extractParamNamesWithIndexes() { List<String> nameList = new ArrayList<String>(); List<Integer> idxList = new ArrayList<Integer>(); Annotation[][] params = method.getParameterAnnotations(); for (int i = 0; i < params.length; i++) { for (Annotation a : params[i]) { if (a.annotationType().equals(Var.class)) { String name = ((Var) a).value(); nameList.add(name); idxList.add(i); } } } int size = nameList.size(); paramNames = new String[size]; valueIdxs = new int[size]; for (int i = 0; i < size; i++) { paramNames[i] = nameList.get(i); valueIdxs[i] = idxList.get(i); } } private void extractContentArgPosition() { Annotation[][] params = method.getParameterAnnotations(); int pos = -1; int count = 0; for (int i = 0; i < params.length; i++) { for (Annotation a : params[i]) { if (a.annotationType().equals(Content.class)) { pos = i; count++; } } } if (count > 1) { throw new ProxyAnnotationException(format("Method %s annotates multiple parameters as @Content - at most one is allowed ", methodName())); } contentArgPosition = pos; if (contentArgPosition >= 0) { Type type = method.getGenericParameterTypes()[contentArgPosition]; if (type instanceof ParameterizedType) { ParameterizedType pType = (ParameterizedType) type; if (pType.getActualTypeArguments() != null) { genericContentType = (Class<?>) pType.getActualTypeArguments()[0]; } } } } private void extractContentTransformerClass() { ContentTransformerClass annotation = method.getAnnotation(ContentTransformerClass.class); if (contentArgPosition == -1) { if (annotation != null) { throw new ProxyAnnotationException(format("ContentTransformClass defined on method %s with no @Content parameter", method.getName())); } return; } if (annotation == null) { Class<?> contentType = method.getParameterTypes()[contentArgPosition]; if (Observable.class.isAssignableFrom(contentType) && ByteBuf.class.isAssignableFrom(genericContentType) || ByteBuf.class.isAssignableFrom(contentType) || byte[].class.isAssignableFrom(contentType) || String.class.isAssignableFrom(contentType)) { return; } throw new ProxyAnnotationException(format("ContentTransformerClass annotation missing for content type %s in method %s", contentType.getName(), methodName())); } contentTansformerClass = annotation.value(); } private void extractTemplateName() { TemplateName annotation = method.getAnnotation(TemplateName.class); if (null != annotation) { templateName = annotation.value(); } else { templateName = method.getName(); } } private void extractResultType() { Class<?> returnClass = method.getReturnType(); if (!returnClass.isAssignableFrom(RibbonRequest.class)) { throw new ProxyAnnotationException(format("Method %s must return RibbonRequest<ByteBuf> type not %s", methodName(), returnClass.getSimpleName())); } ParameterizedType returnType = (ParameterizedType) method.getGenericReturnType(); resultType = (Class<?>) returnType.getActualTypeArguments()[0]; if (!ByteBuf.class.isAssignableFrom(resultType)) { throw new ProxyAnnotationException(format("Method %s must return RibbonRequest<ByteBuf> type; instead %s type parameter found", methodName(), resultType.getSimpleName())); } } private String methodName() { return method.getDeclaringClass().getSimpleName() + '.' + method.getName(); } } }