源码解析: Feign RequestTemplate

2018-01-11  本文已影响0人  lazyguy

MethodMetaData在Contract组件中被创建的时候已经创建了RequestTemplate,里面已经包含了一些抽取出来的原始数据。但是MethodMetaData在Contract的apply方法中被使用时,你会发现MethodMetaData里面的RequestTemplate,又被拿出来作为原材料按照不同的情况去创建已了一个新的RequestTemplate.Factory,塞入MethodHandler中待用。为啥呢?

public Map<String, MethodHandler> apply(Target key) {
      //通过Contract抽取出MethodMetadata,里面包含RequestTemplate。
      List<MethodMetadata> metadata = contract.parseAndValidatateMetadata(key.type());
      Map<String, MethodHandler> result = new LinkedHashMap<String, MethodHandler>();
      for (MethodMetadata md : metadata) {
        BuildTemplateByResolvingArgs buildTemplate;
        //如果表单参数不为空并且请求体模板为空(说明我们的请求参数是普通的路径参数/查询参数)
        if (!md.formParams().isEmpty() && md.template().bodyTemplate() == null) {
        //创建BuildFormEncodedTemplateFromArgs。从名字理解这个工厂的意思是“从参数创建表单已编码的RequestTemplate”
          buildTemplate = new BuildFormEncodedTemplateFromArgs(md, encoder);
        } else if (md.bodyIndex() != null) {
        //如果有啥也没标记的方法参数,创建工厂“从参数创建已编码的RequestTemplate”
          buildTemplate = new BuildEncodedTemplateFromArgs(md, encoder);
        } else {
        //其他情况,创建工厂“通过解析参数创建RequestTemplate”
          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 {
    //我们解析出的md
    protected final MethodMetadata metadata;
    //@Param参数中的Expander,key是哪个参数的序号
    private final Map<Integer, Expander> indexToExpander = new LinkedHashMap<Integer, Expander>();
    //这个方法
    private BuildTemplateByResolvingArgs(MethodMetadata metadata) {
      this.metadata = metadata;
      //找到MD中是否有expander信息,有就倒腾过来。
      //注意这里其实一开始是没有的。因为在Contract的解析逻辑中我们是把Expander的信息放在了indexToExpanderClass里面,没有去实例化那些Expander类。
      //只有重写了Contract逻辑,调用了feign.MethodMetadata#indexToExpander(java.util.Map<java.lang.Integer,feign.Param.Expander>)方法,动态注入了Expander实例的情况才会使用。
     //这样原本的indexToExpanderClass就没用了,因为可以看到代码直接返回了。
      if (metadata.indexToExpander() != null) {
        indexToExpander.putAll(metadata.indexToExpander());
        return;
      }
      if (metadata.indexToExpanderClass().isEmpty()) {
        return;
      }
      //如果MD中填充了indexToExpanderClass相关信息,将Expander实例化,放入工厂类自己的indexToExpander待用。
      for (Entry<Integer, Class<? extends Expander>> 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) {
      //将md中原本的RequestTemplate取出,创建新的RequestTemplate,以供修改,咋改呢?
      RequestTemplate mutable = new RequestTemplate(metadata.template());
     //从Contract的分析可知这里的urlIndex,代表的是方法中的参数可以是一个URI。
     //会在这里抽取出来被拼接到原本的url的前面。
     //但是我翻遍了wiki也没发现这样的用法的讲解。而且想想这样用不是很麻烦么,估计是以前的遗留功能,后来没用了吧。
      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]));
      }
     //通过indexToName,解析Param注解了的参数的值,特别是自定义了Expander的,将在这替换成真正的值,存入varBuilder。key是注解里面的名字,value是真正展开后的值。
       Map<String, Object> varBuilder = new LinkedHashMap<String, Object>();
      for (Entry<Integer, Collection<String>> 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
        template = addQueryMapQueryParameters((Map<String, Object>) argv[metadata.queryMapIndex()], template);
      }

      if (metadata.headerMapIndex() != null) {
        template = addHeaderMapHeaders((Map<String, Object>) argv[metadata.headerMapIndex()], template);
      }

      return template;
    }

    private Object expandElements(Expander expander, Object value) {
      if (value instanceof Iterable) {
        return expandIterable(expander, (Iterable) value);
      }
      return expander.expand(value);
    }

    private List<String> expandIterable(Expander expander, Iterable value) {
      List<String> values = new ArrayList<String>();
      for (Object element : (Iterable) value) {
        if (element!=null) {
          values.add(expander.expand(element));
        }
      }
      return values;
    }

    @SuppressWarnings("unchecked")
    private RequestTemplate addHeaderMapHeaders(Map<String, Object> headerMap, RequestTemplate mutable) {
      for (Entry<String, Object> currEntry : headerMap.entrySet()) {
        Collection<String> values = new ArrayList<String>();

        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<String, Object> queryMap, RequestTemplate mutable) {
      for (Entry<String, Object> currEntry : queryMap.entrySet()) {
        Collection<String> values = new ArrayList<String>();

        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() : RequestTemplate.urlEncode(nextObject.toString()));
          }
        } else {
          values.add(currValue == null ? null : encoded ? currValue.toString() : RequestTemplate.urlEncode(currValue.toString()));
        }

        mutable.query(true, encoded ? currEntry.getKey() : RequestTemplate.urlEncode(currEntry.getKey()), values);
      }
      return mutable;
    }

    protected RequestTemplate resolve(Object[] argv, RequestTemplate mutable,
                                      Map<String, Object> variables) {
      // Resolving which variable names are already encoded using their indices
      Map<String, Boolean> variableToEncoded = new LinkedHashMap<String, Boolean>();
      for (Entry<Integer, Boolean> entry : metadata.indexToEncoded().entrySet()) {
        Collection<String> names = metadata.indexToName().get(entry.getKey());
        for (String name : names) {
          variableToEncoded.put(name, entry.getValue());
        }
      }
      return mutable.resolve(variables, variableToEncoded);
    }
  }
/*
 * 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.io.Serializable;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

import static feign.Util.CONTENT_LENGTH;
import static feign.Util.UTF_8;
import static feign.Util.checkArgument;
import static feign.Util.checkNotNull;
import static feign.Util.emptyToNull;
import static feign.Util.toArray;
import static feign.Util.valuesOrEmpty;

/**
 * Builds a request to an http target. Not thread safe. <br> <br><br><b>relationship to JAXRS
 * 2.0</b><br> <br> A combination of {@code javax.ws.rs.client.WebTarget} and {@code
 * javax.ws.rs.client.Invocation.Builder}, ensuring you can modify any part of the request. However,
 * this object is mutable, so needs to be guarded with the copy constructor.
 */
public final class RequestTemplate implements Serializable {

  private static final long serialVersionUID = 1L;
  private final Map<String, Collection<String>> queries =
      new LinkedHashMap<String, Collection<String>>();
  private final Map<String, Collection<String>> headers =
      new LinkedHashMap<String, Collection<String>>();
  private String method;
  /* final to encourage mutable use vs replacing the object. */
  private StringBuilder url = new StringBuilder();
  private transient Charset charset;
  private byte[] body;
  private String bodyTemplate;
  private boolean decodeSlash = true;

  public RequestTemplate() {
  }

  /* Copy constructor. Use this when making templates. */
  public RequestTemplate(RequestTemplate toCopy) {
    checkNotNull(toCopy, "toCopy");
    this.method = toCopy.method;
    this.url.append(toCopy.url);
    this.queries.putAll(toCopy.queries);
    this.headers.putAll(toCopy.headers);
    this.charset = toCopy.charset;
    this.body = toCopy.body;
    this.bodyTemplate = toCopy.bodyTemplate;
    this.decodeSlash = toCopy.decodeSlash;
  }

  private static String urlDecode(String arg) {
    try {
      return URLDecoder.decode(arg, UTF_8.name());
    } catch (UnsupportedEncodingException e) {
      throw new RuntimeException(e);
    }
  }

  static String urlEncode(Object arg) {
    try {
      return URLEncoder.encode(String.valueOf(arg), UTF_8.name());
    } catch (UnsupportedEncodingException e) {
      throw new RuntimeException(e);
    }
  }

  private static boolean isHttpUrl(CharSequence value) {
    return value.length() >= 4 && value.subSequence(0, 3).equals("http".substring(0,  3));
  }

  private static CharSequence removeTrailingSlash(CharSequence charSequence) {
    if (charSequence != null && charSequence.length() > 0 && charSequence.charAt(charSequence.length() - 1) == '/') {
      return charSequence.subSequence(0, charSequence.length() - 1);
    } else {
      return charSequence;
    }
  }

  /**
   * Expands a {@code template}, such as {@code username}, using the {@code variables} supplied. Any
   * unresolved parameters will remain. <br> Note that if you'd like curly braces literally in the
   * {@code template}, urlencode them first.
   *
   * @param template  URI template that can be in level 1 <a href="http://tools.ietf.org/html/rfc6570">RFC6570</a>
   *                  form.
   * @param variables to the URI template
   * @return expanded template, leaving any unresolved parameters literal
   */
  public static String expand(String template, Map<String, ?> variables) {
    // skip expansion if there's no valid variables set. ex. {a} is the
    // first valid
    if (checkNotNull(template, "template").length() < 3) {
      return template;
    }
    checkNotNull(variables, "variables for %s", template);

    boolean inVar = false;
    StringBuilder var = new StringBuilder();
    StringBuilder builder = new StringBuilder();
    for (char c : template.toCharArray()) {
      switch (c) {
        case '{':
          if (inVar) {
            // '{{' is an escape: write the brace and don't interpret as a variable
            builder.append("{");
            inVar = false;
            break;
          }
          inVar = true;
          break;
        case '}':
          if (!inVar) { // then write the brace literally
            builder.append('}');
            break;
          }
          inVar = false;
          String key = var.toString();
          Object value = variables.get(var.toString());
          if (value != null) {
            builder.append(value);
          } else {
            builder.append('{').append(key).append('}');
          }
          var = new StringBuilder();
          break;
        default:
          if (inVar) {
            var.append(c);
          } else {
            builder.append(c);
          }
      }
    }
    return builder.toString();
  }
 //将查询字符串解析成ma
  private static Map<String, Collection<String>> parseAndDecodeQueries(String queryLine) {
    Map<String, Collection<String>> map = new LinkedHashMap<String, Collection<String>>();
    if (emptyToNull(queryLine) == null) {
      return map;
    }
    //如果没有&这个符号说明争哥问号后面的查询参数就一个,直接放进去。
    //如果有,则遍历此字符串一个个的塞。值得注意的是,putKV这个方法中还将我们的查询字符串先解码了的。也就是说就算我们当时写的时候有中文而且是编过码的。
    //比如“name=张三”,我们写的是“name%3D%E5%BC%A0%E4%B8%89”。也能正确解码出来。放入结果集中。
    if (queryLine.indexOf('&') == -1) {
      putKV(queryLine, map);
    } else {
      char[] chars = queryLine.toCharArray();
      int start = 0;
      int i = 0;
      for (; i < chars.length; i++) {
        if (chars[i] == '&') {
          putKV(queryLine.substring(start, i), map);
          start = i + 1;
        }
      }
      putKV(queryLine.substring(start, i), map);
    }
    return map;
  }

  private static void putKV(String stringToParse, Map<String, Collection<String>> map) {
    String key;
    String value;
    // note that '=' can be a valid part of the value
    int firstEq = stringToParse.indexOf('=');
    if (firstEq == -1) {
      key = urlDecode(stringToParse);
      value = null;
    } else {
      key = urlDecode(stringToParse.substring(0, firstEq));
      value = urlDecode(stringToParse.substring(firstEq + 1));
    }
    Collection<String> values = map.containsKey(key) ? map.get(key) : new ArrayList<String>();
    values.add(value);
    map.put(key, values);
  }

  /** {@link #resolve(Map, Map)}, which assumes no parameter is encoded */
  public RequestTemplate resolve(Map<String, ?> unencoded) {
    return resolve(unencoded, Collections.<String, Boolean>emptyMap());
  }

  /**
   * Resolves any template parameters in the requests path, query, or headers against the supplied
   * unencoded arguments. <br> <br><br><b>relationship to JAXRS 2.0</b><br> <br> This call is
   * similar to {@code javax.ws.rs.client.WebTarget.resolveTemplates(templateValues, true)} , except
   * that the template values apply to any part of the request, not just the URL
   */
  RequestTemplate resolve(Map<String, ?> unencoded, Map<String, Boolean> alreadyEncoded) {
    replaceQueryValues(unencoded, alreadyEncoded);
    Map<String, String> encoded = new LinkedHashMap<String, String>();
    for (Entry<String, ?> entry : unencoded.entrySet()) {
      final String key = entry.getKey();
      final Object objectValue = entry.getValue();
      String encodedValue = encodeValueIfNotEncoded(key, objectValue, alreadyEncoded);
      encoded.put(key, encodedValue);
    }
    String resolvedUrl = expand(url.toString(), encoded).replace("+", "%20");
    if (decodeSlash) {
      resolvedUrl = resolvedUrl.replace("%2F", "/");
    }
    url = new StringBuilder(resolvedUrl);

    Map<String, Collection<String>> resolvedHeaders = new LinkedHashMap<String, Collection<String>>();
    for (String field : headers.keySet()) {
      Collection<String> resolvedValues = new ArrayList<String>();
      for (String value : valuesOrEmpty(headers, field)) {
        String resolved = expand(value, unencoded);
        resolvedValues.add(resolved);
      }
      resolvedHeaders.put(field, resolvedValues);
    }
    headers.clear();
    headers.putAll(resolvedHeaders);
    if (bodyTemplate != null) {
      body(urlDecode(expand(bodyTemplate, encoded)));
    }
    return this;
  }

  private String encodeValueIfNotEncoded(String key, Object objectValue, Map<String, Boolean> alreadyEncoded) {
    String value = String.valueOf(objectValue);
    final Boolean isEncoded = alreadyEncoded.get(key);
    if (isEncoded == null || !isEncoded) {
      value = urlEncode(value);
    }
    return value;
  }

  /* roughly analogous to {@code javax.ws.rs.client.Target.request()}. */
  public Request request() {
    Map<String, Collection<String>> safeCopy = new LinkedHashMap<String, Collection<String>>();
    safeCopy.putAll(headers);
    return Request.create(
        method, url + queryLine(),
        Collections.unmodifiableMap(safeCopy),
        body, charset
    );
  }

  /* @see Request#method() */
  public RequestTemplate method(String method) {
    this.method = checkNotNull(method, "method");
    checkArgument(method.matches("^[A-Z]+$"), "Invalid HTTP Method: %s", method);
    return this;
  }
  
  /* @see Request#method() */
  public String method() {
    return method;
  }

  public RequestTemplate decodeSlash(boolean decodeSlash) {
    this.decodeSlash = decodeSlash;
    return this;
  }
  
  public boolean decodeSlash() {
    return decodeSlash;
  }

  /* @see #url() */
  //这个方法是在Contract设置url的时候被调用的,他不仅简单设置了url,而且调用了RequestTemplate的pullAnyQueriesOutOfUrl。分析了url上的查询参数,并从url上抽走,设置到queries成员变量里面去
  public RequestTemplate append(CharSequence value) {
    url.append(value);
    url = pullAnyQueriesOutOfUrl(url);
    return this;
  }

  /* @see #url() */
  public RequestTemplate insert(int pos, CharSequence value) {
    if(isHttpUrl(value)) {
      value = removeTrailingSlash(value);
      if(url.length() > 0 && url.charAt(0) != '/') {
        url.insert(0, '/');
      }
    }
    url.insert(pos, pullAnyQueriesOutOfUrl(new StringBuilder(value)));
    return this;
  }

  public String url() {
    return url.toString();
  }

  /**
   * Replaces queries with the specified {@code name} with the {@code values} supplied.
   * <br> Values can be passed in decoded or in url-encoded form depending on the value of the
   * {@code encoded} parameter.
   * <br> When the {@code value} is {@code null}, all queries with the {@code configKey} are
   * removed. <br> <br><br><b>relationship to JAXRS 2.0</b><br> <br> Like {@code WebTarget.query},
   * except the values can be templatized. <br> ex. <br>
   * <pre>
   * template.query(&quot;Signature&quot;, &quot;{signature}&quot;);
   * </pre>
   * <br> <b>Note:</b> behavior of RequestTemplate is not consistent if a query parameter with
   * unsafe characters is passed as both encoded and unencoded, although no validation is performed.
   * <br> ex. <br>
   * <pre>
   * template.query(true, &quot;param[]&quot;, &quot;value&quot;);
   * template.query(false, &quot;param[]&quot;, &quot;value&quot;);
   * </pre>
   *
   * @param encoded   whether name and values are already url-encoded
   * @param name      the name of the query
   * @param values    can be a single null to imply removing all values. Else no values are expected
   *                  to be null.
   * @see #queries()
   */
  public RequestTemplate query(boolean encoded, String name, String... values) {
    return doQuery(encoded, name, values);
  }

  /* @see #query(boolean, String, String...) */
  public RequestTemplate query(boolean encoded, String name, Iterable<String> values) {
    return doQuery(encoded, name, values);
  }

  /**
   * Shortcut for {@code query(false, String, String...)}
   * @see #query(boolean, String, String...)
   */
  public RequestTemplate query(String name, String... values) {
    return doQuery(false, name, values);
  }

  /**
   * Shortcut for {@code query(false, String, Iterable<String>)}
   * @see #query(boolean, String, String...)
   */
  public RequestTemplate query(String name, Iterable<String> values) {
    return doQuery(false, name, values);
  }
  //doQuery方法实际逻辑在这,其实就是为RequestTemplate的queries属性放入查询参数的键值对,而且是编过码的。
  private RequestTemplate doQuery(boolean encoded, String name, String... values) {
    checkNotNull(name, "name");
    String paramName = encoded ? name : encodeIfNotVariable(name);
    queries.remove(paramName);
    if (values != null && values.length > 0 && values[0] != null) {
      ArrayList<String> paramValues = new ArrayList<String>();
      for (String value : values) {
        paramValues.add(encoded ? value : encodeIfNotVariable(value));
      }
      this.queries.put(paramName, paramValues);
    }
    return this;
  }

  private RequestTemplate doQuery(boolean encoded, String name, Iterable<String> values) {
    if (values != null) {
      return doQuery(encoded, name, toArray(values, String.class));
    }
    return doQuery(encoded, name, (String[]) null);
  }

  private static String encodeIfNotVariable(String in) {
    if (in == null || in.indexOf('{') == 0) {
      return in;
    }
    return urlEncode(in);
  }

  /**
   * Replaces all existing queries with the newly supplied url decoded queries. <br>
   * <br><br><b>relationship to JAXRS 2.0</b><br> <br> Like {@code WebTarget.queries}, except the
   * values can be templatized. <br> ex. <br>
   * <pre>
   * template.queries(ImmutableMultimap.of(&quot;Signature&quot;, &quot;{signature}&quot;));
   * </pre>
   *
   * @param queries if null, remove all queries. else value to replace all queries with.
   * @see #queries()
   */
  public RequestTemplate queries(Map<String, Collection<String>> queries) {
    if (queries == null || queries.isEmpty()) {
      this.queries.clear();
    } else {
      for (Entry<String, Collection<String>> entry : queries.entrySet()) {
        query(entry.getKey(), toArray(entry.getValue(), String.class));
      }
    }
    return this;
  }

  /**
   * Returns an immutable copy of the url decoded queries.
   *
   * @see Request#url()
   */
  public Map<String, Collection<String>> queries() {
    Map<String, Collection<String>> decoded = new LinkedHashMap<String, Collection<String>>();
    for (String field : queries.keySet()) {
      Collection<String> decodedValues = new ArrayList<String>();
      for (String value : valuesOrEmpty(queries, field)) {
        if (value != null) {
          decodedValues.add(urlDecode(value));
        } else {
          decodedValues.add(null);
        }
      }
      decoded.put(urlDecode(field), decodedValues);
    }
    return Collections.unmodifiableMap(decoded);
  }

  /**
   * Replaces headers with the specified {@code configKey} with the {@code values} supplied. <br>
   * When the {@code value} is {@code null}, all headers with the {@code configKey} are removed.
   * <br> <br><br><b>relationship to JAXRS 2.0</b><br> <br> Like {@code WebTarget.queries} and
   * {@code javax.ws.rs.client.Invocation.Builder.header}, except the values can be templatized.
   * <br> ex. <br>
   * <pre>
   * template.query(&quot;X-Application-Version&quot;, &quot;{version}&quot;);
   * </pre>
   *
   * @param name   the name of the header
   * @param values can be a single null to imply removing all values. Else no values are expected to
   *               be null.
   * @see #headers()
   */
  public RequestTemplate header(String name, String... values) {
    checkNotNull(name, "header name");
    if (values == null || (values.length == 1 && values[0] == null)) {
      headers.remove(name);
    } else {
      List<String> headers = new ArrayList<String>();
      headers.addAll(Arrays.asList(values));
      this.headers.put(name, headers);
    }
    return this;
  }

  /* @see #header(String, String...) */
  public RequestTemplate header(String name, Iterable<String> values) {
    if (values != null) {
      return header(name, toArray(values, String.class));
    }
    return header(name, (String[]) null);
  }

  /**
   * Replaces all existing headers with the newly supplied headers. <br> <br><br><b>relationship to
   * JAXRS 2.0</b><br> <br> Like {@code Invocation.Builder.headers(MultivaluedMap)}, except the
   * values can be templatized. <br> ex. <br>
   * <pre>
   * template.headers(mapOf(&quot;X-Application-Version&quot;, asList(&quot;{version}&quot;)));
   * </pre>
   *
   * @param headers if null, remove all headers. else value to replace all headers with.
   * @see #headers()
   */
  public RequestTemplate headers(Map<String, Collection<String>> headers) {
    if (headers == null || headers.isEmpty()) {
      this.headers.clear();
    } else {
      this.headers.putAll(headers);
    }
    return this;
  }

  /**
   * Returns an immutable copy of the current headers.
   *
   * @see Request#headers()
   */
  public Map<String, Collection<String>> headers() {
    return Collections.unmodifiableMap(headers);
  }

  /**
   * replaces the {@link feign.Util#CONTENT_LENGTH} header. <br> Usually populated by an {@link
   * feign.codec.Encoder}.
   *
   * @see Request#body()
   */
  public RequestTemplate body(byte[] bodyData, Charset charset) {
    this.bodyTemplate = null;
    this.charset = charset;
    this.body = bodyData;
    int bodyLength = bodyData != null ? bodyData.length : 0;
    header(CONTENT_LENGTH, String.valueOf(bodyLength));
    return this;
  }

  /**
   * replaces the {@link feign.Util#CONTENT_LENGTH} header. <br> Usually populated by an {@link
   * feign.codec.Encoder}.
   *
   * @see Request#body()
   */
  public RequestTemplate body(String bodyText) {
    byte[] bodyData = bodyText != null ? bodyText.getBytes(UTF_8) : null;
    return body(bodyData, UTF_8);
  }

  /**
   * The character set with which the body is encoded, or null if unknown or not applicable.  When
   * this is present, you can use {@code new String(req.body(), req.charset())} to access the body
   * as a String.
   */
  public Charset charset() {
    return charset;
  }

  /**
   * @see Request#body()
   */
  public byte[] body() {
    return body;
  }

  /**
   * populated by {@link Body}
   *
   * @see Request#body()
   */
  public RequestTemplate bodyTemplate(String bodyTemplate) {
    this.bodyTemplate = bodyTemplate;
    this.charset = null;
    this.body = null;
    return this;
  }

  /**
   * @see Request#body()
   * @see #expand(String, Map)
   */
  public String bodyTemplate() {
    return bodyTemplate;
  }

  /**
   * if there are any query params in the URL, this will extract them out.
   */
  private StringBuilder pullAnyQueriesOutOfUrl(StringBuilder url) {
    // parse out queries
    int queryIndex = url.indexOf("?");
    if (queryIndex != -1) {
      //抽取问号后面的查询参数字符串
      String queryLine = url.substring(queryIndex + 1);
      //
      Map<String, Collection<String>> firstQueries = parseAndDecodeQueries(queryLine);
      if (!queries.isEmpty()) {
        firstQueries.putAll(queries);
        queries.clear();
      }
      //Since we decode all queries, we want to use the
      //query()-method to re-add them to ensure that all
      //logic (such as url-encoding) are executed, giving
      //a valid queryLine()
      for (String key : firstQueries.keySet()) {
        Collection<String> values = firstQueries.get(key);
        if (allValuesAreNull(values)) {
          //Queries where all values are null will
          //be ignored by the query(key, value)-method
          //So we manually avoid this case here, to ensure that
          //we still fulfill the contract (ex. parameters without values)
          queries.put(urlEncode(key), values);
        } else {
          query(key, values);
        }

      }
      return new StringBuilder(url.substring(0, queryIndex));
    }
    return url;
  }

  private boolean allValuesAreNull(Collection<String> values) {
    if (values == null || values.isEmpty()) {
      return true;
    }
    for (String val : values) {
      if (val != null) {
        return false;
      }
    }
    return true;
  }

  @Override
  public String toString() {
    return request().toString();
  }

  /** {@link #replaceQueryValues(Map, Map)}, which assumes no parameter is encoded */
  public void replaceQueryValues(Map<String, ?> unencoded) {
    replaceQueryValues(unencoded, Collections.<String, Boolean>emptyMap());
  }

  /**
   * Replaces query values which are templated with corresponding values from the {@code unencoded}
   * map. Any unresolved queries are removed.
   */
  void replaceQueryValues(Map<String, ?> unencoded, Map<String, Boolean> alreadyEncoded) {
    //
    Iterator<Entry<String, Collection<String>>> iterator = queries.entrySet().iterator();
    while (iterator.hasNext()) {
      Entry<String, Collection<String>> entry = iterator.next();
      if (entry.getValue() == null) {
        continue;
      }
      Collection<String> values = new ArrayList<String>();
      for (String value : entry.getValue()) {
        if (value.indexOf('{') == 0 && value.indexOf('}') == value.length() - 1) {
          Object variableValue = unencoded.get(value.substring(1, value.length() - 1));
          // only add non-null expressions
          if (variableValue == null) {
            continue;
          }
          if (variableValue instanceof Iterable) {
            for (Object val : Iterable.class.cast(variableValue)) {
              String encodedValue = encodeValueIfNotEncoded(entry.getKey(), val, alreadyEncoded);
              values.add(encodedValue);
            }
          } else {
            String encodedValue = encodeValueIfNotEncoded(entry.getKey(), variableValue, alreadyEncoded);
            values.add(encodedValue);
          }
        } else {
          values.add(value);
        }
      }
      if (values.isEmpty()) {
        iterator.remove();
      } else {
        entry.setValue(values);
      }
    }
  }

  public String queryLine() {
    if (queries.isEmpty()) {
      return "";
    }
    StringBuilder queryBuilder = new StringBuilder();
    for (String field : queries.keySet()) {
      for (String value : valuesOrEmpty(queries, field)) {
        queryBuilder.append('&');
        queryBuilder.append(field);
        if (value != null) {
          queryBuilder.append('=');
          if (!value.isEmpty()) {
            queryBuilder.append(value);
          }
        }
      }
    }
    queryBuilder.deleteCharAt(0);
    return queryBuilder.insert(0, '?').toString();
  }

  interface Factory {

    /**
     * create a request template using args passed to a method invocation.
     */
    RequestTemplate create(Object[] argv);
  }
}

上一篇下一篇

猜你喜欢

热点阅读