/* * Copyright 2013 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 feign; import dagger.Provides; import feign.Request.Options; import feign.codec.Decoder; import feign.codec.EncodeException; import feign.codec.Encoder; import feign.codec.ErrorDecoder; import javax.inject.Inject; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; import java.util.Collection; import java.util.Collections; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import static feign.Util.checkArgument; import static feign.Util.checkNotNull; @SuppressWarnings("rawtypes") public class ReflectiveFeign extends Feign { private final ParseHandlersByName targetToHandlersByName; @Inject ReflectiveFeign(ParseHandlersByName targetToHandlersByName) { this.targetToHandlersByName = targetToHandlersByName; } /** * creates an api binding to the {@code target}. As this invokes reflection, * care should be taken to cache the result. */ @SuppressWarnings("unchecked") @Override public T newInstance(Target target) { Map nameToHandler = targetToHandlersByName.apply(target); Map methodToHandler = new LinkedHashMap(); for (Method method : target.type().getDeclaredMethods()) { if (method.getDeclaringClass() == Object.class) continue; methodToHandler.put(method, nameToHandler.get(Feign.configKey(method))); } FeignInvocationHandler handler = new FeignInvocationHandler(target, methodToHandler); return (T) Proxy.newProxyInstance(target.type().getClassLoader(), new Class[]{target.type()}, handler); } static class FeignInvocationHandler implements InvocationHandler { private final Target target; private final Map methodToHandler; FeignInvocationHandler(Target target, Map methodToHandler) { this.target = checkNotNull(target, "target"); this.methodToHandler = checkNotNull(methodToHandler, "methodToHandler for %s", target); } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { if ("equals".equals(method.getName())) { try { Object otherHandler = args.length > 0 && args[0] != null ? Proxy.getInvocationHandler(args[0]) : null; return equals(otherHandler); } catch (IllegalArgumentException e) { return false; } } if ("hashCode".equals(method.getName())) { return hashCode(); } return methodToHandler.get(method).invoke(args); } @Override public int hashCode() { return target.hashCode(); } @Override public boolean equals(Object obj) { if (obj == null) { return false; } if (this == obj) { return true; } if (FeignInvocationHandler.class != obj.getClass()) { return false; } FeignInvocationHandler that = FeignInvocationHandler.class.cast(obj); return this.target.equals(that.target); } @Override public String toString() { return "target(" + target + ")"; } } @dagger.Module(complete = false, injects = {Feign.class, MethodHandler.Factory.class}, library = true) public static class Module { @Provides(type = Provides.Type.SET_VALUES) Set noRequestInterceptors() { return Collections.emptySet(); } @Provides Feign provideFeign(ReflectiveFeign in) { return in; } } static final class ParseHandlersByName { private final Contract contract; private final Options options; private final Encoder encoder; private final Decoder decoder; private final ErrorDecoder errorDecoder; private final MethodHandler.Factory factory; @SuppressWarnings("unchecked") @Inject ParseHandlersByName(Contract contract, Options options, Encoder encoder, Decoder decoder, ErrorDecoder errorDecoder, MethodHandler.Factory factory) { this.contract = contract; this.options = options; this.factory = factory; this.errorDecoder = errorDecoder; this.encoder = checkNotNull(encoder, "encoder"); this.decoder = checkNotNull(decoder, "decoder"); } public Map apply(Target key) { List metadata = contract.parseAndValidatateMetadata(key.type()); Map result = new LinkedHashMap(); for (MethodMetadata md : metadata) { BuildTemplateByResolvingArgs buildTemplate; if (!md.formParams().isEmpty() && md.template().bodyTemplate() == null) { buildTemplate = new BuildFormEncodedTemplateFromArgs(md, encoder); } else if (md.bodyIndex() != null) { buildTemplate = new BuildEncodedTemplateFromArgs(md, encoder); } else { buildTemplate = new BuildTemplateByResolvingArgs(md); } result.put(md.configKey(), factory.create(key, md, buildTemplate, options, decoder, errorDecoder)); } return result; } } private static class BuildTemplateByResolvingArgs implements MethodHandler.BuildTemplateFromArgs { protected final MethodMetadata metadata; private BuildTemplateByResolvingArgs(MethodMetadata metadata) { this.metadata = metadata; } public RequestTemplate apply(Object[] argv) { RequestTemplate mutable = new RequestTemplate(metadata.template()); if (metadata.urlIndex() != null) { int urlIndex = metadata.urlIndex(); checkArgument(argv[urlIndex] != null, "URI parameter %s was null", urlIndex); mutable.insert(0, String.valueOf(argv[urlIndex])); } Map varBuilder = new LinkedHashMap(); for (Entry> entry : metadata.indexToName().entrySet()) { Object value = argv[entry.getKey()]; if (value != null) { // Null values are skipped. for (String name : entry.getValue()) varBuilder.put(name, value); } } return resolve(argv, mutable, varBuilder); } protected RequestTemplate resolve(Object[] argv, RequestTemplate mutable, Map variables) { return mutable.resolve(variables); } } private static class BuildFormEncodedTemplateFromArgs extends BuildTemplateByResolvingArgs { private final Encoder encoder; private BuildFormEncodedTemplateFromArgs(MethodMetadata metadata, Encoder encoder) { super(metadata); this.encoder = encoder; } @Override protected RequestTemplate resolve(Object[] argv, RequestTemplate mutable, Map variables) { Map formVariables = new LinkedHashMap(); for (Entry entry : variables.entrySet()) { if (metadata.formParams().contains(entry.getKey())) formVariables.put(entry.getKey(), entry.getValue()); } try { encoder.encode(formVariables, mutable); } catch (EncodeException e) { throw e; } catch (RuntimeException e) { throw new EncodeException(e.getMessage(), e); } return super.resolve(argv, mutable, variables); } } private static class BuildEncodedTemplateFromArgs extends BuildTemplateByResolvingArgs { private final Encoder encoder; private BuildEncodedTemplateFromArgs(MethodMetadata metadata, Encoder encoder) { super(metadata); this.encoder = encoder; } @Override protected RequestTemplate resolve(Object[] argv, RequestTemplate mutable, Map variables) { Object body = argv[metadata.bodyIndex()]; checkArgument(body != null, "Body parameter %s was null", metadata.bodyIndex()); try { encoder.encode(body, mutable); } catch (EncodeException e) { throw e; } catch (RuntimeException e) { throw new EncodeException(e.getMessage(), e); } return super.resolve(argv, mutable, variables); } } }