/* * 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.Lazy; import dagger.Provides; import feign.Request.Options; import feign.codec.Decoder; import feign.codec.EncodeException; import feign.codec.Encoder; import feign.codec.ErrorDecoder; import feign.codec.IncrementalDecoder; import feign.codec.StringDecoder; import feign.codec.StringIncrementalDecoder; import javax.inject.Inject; import javax.inject.Named; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; import java.lang.reflect.Type; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import java.util.concurrent.Executor; import static feign.Util.checkArgument; import static feign.Util.checkNotNull; import static feign.Util.checkState; import static feign.Util.resolveLastTypeParameter; import static java.lang.String.format; @SuppressWarnings("rawtypes") public class ReflectiveFeign extends Feign { private final ParseHandlersByName targetToHandlersByName; @Inject ReflectiveFeign(@Named("http") Lazy httpExecutor, ParseHandlersByName targetToHandlersByName) { super(httpExecutor); 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(type = Provides.Type.SET_VALUES) Set noEncoders() { return Collections.emptySet(); } @Provides(type = Provides.Type.SET_VALUES) Set noDecoders() { return Collections.emptySet(); } @Provides(type = Provides.Type.SET_VALUES) Set noIncrementalDecoders() { return Collections.emptySet(); } @Provides Feign provideFeign(ReflectiveFeign in) { return in; } } static final class ParseHandlersByName { private final Contract contract; private final Options options; private final Map> encoders = new HashMap>(); private final Encoder.Text> formEncoder; private final Map> decoders = new HashMap>(); private final Map> incrementalDecoders = new HashMap>(); private final ErrorDecoder errorDecoder; private final MethodHandler.Factory factory; @SuppressWarnings("unchecked") @Inject ParseHandlersByName(Contract contract, Options options, Set encoders, Set decoders, Set incrementalDecoders, ErrorDecoder errorDecoder, MethodHandler.Factory factory) { this.contract = contract; this.options = options; this.factory = factory; this.errorDecoder = errorDecoder; for (Encoder encoder : encoders) { checkState(encoder instanceof Encoder.Text, "Currently, only Encoder.Text is supported. Found: ", encoder); Type type = resolveLastTypeParameter(encoder.getClass(), Encoder.class); this.encoders.put(type, Encoder.Text.class.cast(encoder)); } try { Type formEncoderType = getClass().getDeclaredField("formEncoder").getGenericType(); Type formType = resolveLastTypeParameter(formEncoderType, Encoder.class); Encoder.Text formEncoder = this.encoders.get(formType); if (formEncoder == null) { formEncoder = this.encoders.get(Object.class); } this.formEncoder = (Encoder.Text) formEncoder; } catch (NoSuchFieldException e) { throw new AssertionError(e); } StringDecoder stringDecoder = new StringDecoder(); this.decoders.put(void.class, stringDecoder); this.decoders.put(Response.class, stringDecoder); this.decoders.put(String.class, stringDecoder); for (Decoder decoder : decoders) { checkState(decoder instanceof Decoder.TextStream, "Currently, only Decoder.TextStream is supported. Found: ", decoder); Type type = resolveLastTypeParameter(decoder.getClass(), Decoder.class); this.decoders.put(type, Decoder.TextStream.class.cast(decoder)); } StringIncrementalDecoder stringIncrementalDecoder = new StringIncrementalDecoder(); this.incrementalDecoders.put(Void.class, stringIncrementalDecoder); this.incrementalDecoders.put(Response.class, stringIncrementalDecoder); this.incrementalDecoders.put(String.class, stringIncrementalDecoder); for (IncrementalDecoder incrementalDecoder : incrementalDecoders) { checkState(incrementalDecoder instanceof IncrementalDecoder.TextStream, "Currently, only IncrementalDecoder.TextStream is supported. Found: ", incrementalDecoder); Type type = resolveLastTypeParameter(incrementalDecoder.getClass(), IncrementalDecoder.class); this.incrementalDecoders.put(type, IncrementalDecoder.TextStream.class.cast(incrementalDecoder)); } } 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) { if (formEncoder == null) { throw new IllegalStateException(format("%s needs @Provides(type = Set) Encoder encoder()" + "{ // Encoder.Text> or Encoder.Text}", md.configKey())); } buildTemplate = new BuildFormEncodedTemplateFromArgs(md, formEncoder); } else if (md.bodyIndex() != null) { Encoder.Text encoder = encoders.get(md.bodyType()); if (encoder == null) { encoder = encoders.get(Object.class); } if (encoder == null) { throw new IllegalStateException(format("%s needs @Provides(type = Set) Encoder encoder()" + "{ // Encoder.Text<%s> or Encoder.Text}", md.configKey(), md.bodyType())); } buildTemplate = new BuildEncodedTemplateFromArgs(md, encoder); } else { buildTemplate = new BuildTemplateByResolvingArgs(md); } if (md.incrementalType() != null) { IncrementalDecoder.TextStream incrementalDecoder = incrementalDecoders.get(md.incrementalType()); if (incrementalDecoder == null) { incrementalDecoder = incrementalDecoders.get(Object.class); } if (incrementalDecoder == null) { throw new IllegalStateException(format("%s needs @Provides(type = Set) IncrementalDecoder incrementalDecoder()" + "{ // IncrementalDecoder.TextStream<%s> or IncrementalDecoder.TextStream}", md.configKey(), md.incrementalType())); } result.put(md.configKey(), factory.create(key, md, buildTemplate, options, incrementalDecoder, errorDecoder)); } else { Decoder.TextStream decoder = decoders.get(md.returnType()); if (decoder == null) { decoder = decoders.get(Object.class); } if (decoder == null) { throw new IllegalStateException(format("%s needs @Provides(type = Set) Decoder decoder()" + "{ // Decoder.TextStream<%s> or Decoder.TextStream}", md.configKey(), md.returnType())); } 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.Text> formEncoder; private BuildFormEncodedTemplateFromArgs(MethodMetadata metadata, Encoder.Text> formEncoder) { super(metadata); this.formEncoder = formEncoder; } @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 { mutable.body(formEncoder.encode(formVariables)); } 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.Text encoder; private BuildEncodedTemplateFromArgs(MethodMetadata metadata, Encoder.Text 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 { mutable.body(encoder.encode(body)); } catch (EncodeException e) { throw e; } catch (RuntimeException e) { throw new EncodeException(e.getMessage(), e); } return super.resolve(argv, mutable, variables); } } }