/* * 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 java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; import java.util.Collection; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; import feign.InvocationHandlerFactory.MethodHandler; import feign.Param.Expander; import feign.Request.Options; import feign.codec.Decoder; import feign.codec.EncodeException; import feign.codec.Encoder; import feign.codec.ErrorDecoder; import static feign.Util.checkArgument; import static feign.Util.checkNotNull; public class ReflectiveFeign extends Feign { private final ParseHandlersByName targetToHandlersByName; private final InvocationHandlerFactory factory; ReflectiveFeign(ParseHandlersByName targetToHandlersByName, InvocationHandlerFactory factory) { this.targetToHandlersByName = targetToHandlersByName; this.factory = factory; } /** * 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))); } InvocationHandler handler = factory.create(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 dispatch; FeignInvocationHandler(Target target, Map dispatch) { this.target = checkNotNull(target, "target"); this.dispatch = checkNotNull(dispatch, "dispatch 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; } } else if ("hashCode".equals(method.getName())) { return hashCode(); } else if ("toString".equals(method.getName())) { return toString(); } return dispatch.get(method).invoke(args); } @Override public boolean equals(Object obj) { if (obj instanceof FeignInvocationHandler) { FeignInvocationHandler other = (FeignInvocationHandler) obj; return target.equals(other.target); } return false; } @Override public int hashCode() { return target.hashCode(); } @Override public String toString() { return target.toString(); } } 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 SynchronousMethodHandler.Factory factory; ParseHandlersByName(Contract contract, Options options, Encoder encoder, Decoder decoder, ErrorDecoder errorDecoder, SynchronousMethodHandler.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 RequestTemplate.Factory { protected final MethodMetadata metadata; private final Map indexToExpander = new LinkedHashMap(); private BuildTemplateByResolvingArgs(MethodMetadata metadata) { this.metadata = metadata; if (metadata.indexToExpanderClass().isEmpty()) { return; } for (Entry> indexToExpanderClass : metadata .indexToExpanderClass().entrySet()) { try { indexToExpander .put(indexToExpanderClass.getKey(), indexToExpanderClass.getValue().newInstance()); } catch (InstantiationException e) { throw new IllegalStateException(e); } catch (IllegalAccessException e) { throw new IllegalStateException(e); } } } @Override public RequestTemplate create(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()) { int i = entry.getKey(); Object value = argv[entry.getKey()]; if (value != null) { // Null values are skipped. if (indexToExpander.containsKey(i)) { value = indexToExpander.get(i).expand(value); } 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, Types.MAP_STRING_WILDCARD, 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, metadata.bodyType(), mutable); } catch (EncodeException e) { throw e; } catch (RuntimeException e) { throw new EncodeException(e.getMessage(), e); } return super.resolve(argv, mutable, variables); } } }