/** * Copyright 2012-2021 The Feign 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 feign; import static feign.Util.checkArgument; import static feign.Util.checkNotNull; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; import java.util.*; import java.util.Map.Entry; import feign.InvocationHandlerFactory.MethodHandler; import feign.Param.Expander; import feign.Request.Options; import feign.codec.*; import feign.template.UriUtils; public class ReflectiveFeign extends Feign { private final ParseHandlersByName targetToHandlersByName; private final InvocationHandlerFactory factory; private final QueryMapEncoder queryMapEncoder; ReflectiveFeign(ParseHandlersByName targetToHandlersByName, InvocationHandlerFactory factory, QueryMapEncoder queryMapEncoder) { this.targetToHandlersByName = targetToHandlersByName; this.factory = factory; this.queryMapEncoder = queryMapEncoder; } /** * 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(); List defaultMethodHandlers = new LinkedList(); for (Method method : target.type().getMethods()) { if (method.getDeclaringClass() == Object.class) { continue; } else if (Util.isDefault(method)) { DefaultMethodHandler handler = new DefaultMethodHandler(method); defaultMethodHandlers.add(handler); methodToHandler.put(method, handler); } else { methodToHandler.put(method, nameToHandler.get(Feign.configKey(target.type(), method))); } } InvocationHandler handler = factory.create(target, methodToHandler); T proxy = (T) Proxy.newProxyInstance(target.type().getClassLoader(), new Class[] {target.type()}, handler); for (DefaultMethodHandler defaultMethodHandler : defaultMethodHandlers) { defaultMethodHandler.bindTo(proxy); } return proxy; } 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 QueryMapEncoder queryMapEncoder; private final SynchronousMethodHandler.Factory factory; ParseHandlersByName( Contract contract, Options options, Encoder encoder, Decoder decoder, QueryMapEncoder queryMapEncoder, ErrorDecoder errorDecoder, SynchronousMethodHandler.Factory factory) { this.contract = contract; this.options = options; this.factory = factory; this.errorDecoder = errorDecoder; this.queryMapEncoder = queryMapEncoder; this.encoder = checkNotNull(encoder, "encoder"); this.decoder = checkNotNull(decoder, "decoder"); } public Map apply(Target target) { List metadata = contract.parseAndValidateMetadata(target.type()); Map result = new LinkedHashMap(); for (MethodMetadata md : metadata) { BuildTemplateByResolvingArgs buildTemplate; if (!md.formParams().isEmpty() && md.template().bodyTemplate() == null) { buildTemplate = new BuildFormEncodedTemplateFromArgs(md, encoder, queryMapEncoder, target); } else if (md.bodyIndex() != null || md.alwaysEncodeBody()) { buildTemplate = new BuildEncodedTemplateFromArgs(md, encoder, queryMapEncoder, target); } else { buildTemplate = new BuildTemplateByResolvingArgs(md, queryMapEncoder, target); } if (md.isIgnored()) { result.put(md.configKey(), args -> { throw new IllegalStateException(md.configKey() + " is not a method handled by feign"); }); } else { result.put(md.configKey(), factory.create(target, md, buildTemplate, options, decoder, errorDecoder)); } } return result; } } private static class BuildTemplateByResolvingArgs implements RequestTemplate.Factory { private final QueryMapEncoder queryMapEncoder; protected final MethodMetadata metadata; protected final Target target; private final Map indexToExpander = new LinkedHashMap(); private BuildTemplateByResolvingArgs(MethodMetadata metadata, QueryMapEncoder queryMapEncoder, Target target) { this.metadata = metadata; this.target = target; this.queryMapEncoder = queryMapEncoder; if (metadata.indexToExpander() != null) { indexToExpander.putAll(metadata.indexToExpander()); return; } 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 = RequestTemplate.from(metadata.template()); mutable.feignTarget(target); if (metadata.urlIndex() != null) { int urlIndex = metadata.urlIndex(); checkArgument(argv[urlIndex] != null, "URI parameter %s was null", urlIndex); mutable.target(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 = expandElements(indexToExpander.get(i), value); } for (String name : entry.getValue()) { varBuilder.put(name, value); } } } RequestTemplate template = resolve(argv, mutable, varBuilder); if (metadata.queryMapIndex() != null) { // add query map parameters after initial resolve so that they take // precedence over any predefined values Object value = argv[metadata.queryMapIndex()]; Map queryMap = toQueryMap(value); template = addQueryMapQueryParameters(queryMap, template); } if (metadata.headerMapIndex() != null) { template = addHeaderMapHeaders((Map) argv[metadata.headerMapIndex()], template); } return template; } private Map toQueryMap(Object value) { if (value instanceof Map) { return (Map) value; } try { return queryMapEncoder.encode(value); } catch (EncodeException e) { throw new IllegalStateException(e); } } private Object expandElements(Expander expander, Object value) { if (value instanceof Iterable) { return expandIterable(expander, (Iterable) value); } return expander.expand(value); } private List expandIterable(Expander expander, Iterable value) { List values = new ArrayList(); for (Object element : value) { if (element != null) { values.add(expander.expand(element)); } } return values; } @SuppressWarnings("unchecked") private RequestTemplate addHeaderMapHeaders(Map headerMap, RequestTemplate mutable) { for (Entry currEntry : headerMap.entrySet()) { Collection values = new ArrayList(); Object currValue = currEntry.getValue(); if (currValue instanceof Iterable) { Iterator iter = ((Iterable) currValue).iterator(); while (iter.hasNext()) { Object nextObject = iter.next(); values.add(nextObject == null ? null : nextObject.toString()); } } else { values.add(currValue == null ? null : currValue.toString()); } mutable.header(currEntry.getKey(), values); } return mutable; } @SuppressWarnings("unchecked") private RequestTemplate addQueryMapQueryParameters(Map queryMap, RequestTemplate mutable) { for (Entry currEntry : queryMap.entrySet()) { Collection values = new ArrayList(); boolean encoded = metadata.queryMapEncoded(); Object currValue = currEntry.getValue(); if (currValue instanceof Iterable) { Iterator iter = ((Iterable) currValue).iterator(); while (iter.hasNext()) { Object nextObject = iter.next(); values.add(nextObject == null ? null : encoded ? nextObject.toString() : UriUtils.encode(nextObject.toString())); } } else if (currValue instanceof Object[]) { for (Object value : (Object[]) currValue) { values.add(value == null ? null : encoded ? value.toString() : UriUtils.encode(value.toString())); } } else { values.add(currValue == null ? null : encoded ? currValue.toString() : UriUtils.encode(currValue.toString())); } mutable.query(encoded ? currEntry.getKey() : UriUtils.encode(currEntry.getKey()), values); } return mutable; } 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, QueryMapEncoder queryMapEncoder, Target target) { super(metadata, queryMapEncoder, target); 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, Encoder.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, QueryMapEncoder queryMapEncoder, Target target) { super(metadata, queryMapEncoder, target); this.encoder = encoder; } @Override protected RequestTemplate resolve(Object[] argv, RequestTemplate mutable, Map variables) { boolean alwaysEncodeBody = mutable.methodMetadata().alwaysEncodeBody(); Object body = null; if (!alwaysEncodeBody) { body = argv[metadata.bodyIndex()]; checkArgument(body != null, "Body parameter %s was null", metadata.bodyIndex()); } try { if (alwaysEncodeBody) { body = argv == null ? new Object[0] : argv; encoder.encode(body, Object[].class, mutable); } else { 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); } } }