源码解析: 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("Signature", "{signature}");
* </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, "param[]", "value");
* template.query(false, "param[]", "value");
* </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("Signature", "{signature}"));
* </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("X-Application-Version", "{version}");
* </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("X-Application-Version", asList("{version}")));
* </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);
}
}