>();
private String body;
private String bodyTemplate;
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.body = toCopy.body;
this.bodyTemplate = toCopy.bodyTemplate;
}
/**
* Targets a template to this target, adding the {@link #url() base url} and
* any authentication headers.
*
*
* For example:
*
*
* public Request apply(RequestTemplate input) {
* input.insert(0, url());
* input.replaceHeader("X-Auth", currentToken);
* return input.asRequest();
* }
*
*
*
relationship to JAXRS 2.0
*
* 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
*/
public RequestTemplate resolve(Map unencoded) {
Map encoded = new LinkedHashMap();
for (Entry entry : unencoded.entrySet()) {
encoded.put(entry.getKey(), urlEncode(String.valueOf(entry.getValue())));
}
String queryLine = expand(queryLine(), encoded);
queries.clear();
pullAnyQueriesOutOfUrl(new StringBuilder(queryLine));
String resolvedUrl = expand(url.toString(), encoded).replace("%2F", "/");
url = new StringBuilder(resolvedUrl);
Map> resolvedHeaders = new LinkedHashMap>();
for (String field : headers.keySet()) {
Collection resolvedValues = new ArrayList();
for (String value : valuesOrEmpty(headers, field)) {
String resolved;
if (value.indexOf('{') == 0) {
resolved = String.valueOf(unencoded.get(field));
} else {
resolved = value;
}
if (resolved != null)
resolvedValues.add(resolved);
}
resolvedHeaders.put(field, resolvedValues);
}
headers.clear();
headers.putAll(resolvedHeaders);
if (bodyTemplate != null)
body(urlDecode(expand(bodyTemplate, unencoded)));
return this;
}
/* roughly analogous to {@code javax.ws.rs.client.Target.request()}. */
public Request request() {
return new Request(method, new StringBuilder(url).append(queryLine()).toString(),
headers, body);
}
private static String urlDecode(String arg) {
try {
return URLDecoder.decode(arg, UTF_8.name());
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e);
}
}
private static String urlEncode(Object arg) {
try {
return URLEncoder.encode(String.valueOf(arg), UTF_8.name());
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e);
}
}
/**
* Expands a {@code template}, such as {@code username}, using the {@code variables} supplied. Any unresolved
* parameters will remain.
*
* 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 RFC6570 form.
* @param variables to the URI template
* @return expanded template, leaving any unresolved parameters literal
*/
public static String expand(String template, Map variables) {
// skip expansion if there's no valid variables set. ex. {a} is the
// first valid
if (checkNotNull(template, "template").length() < 3)
return template.toString();
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 '{':
inVar = true;
break;
case '}':
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();
}
/* @see Request#method() */
public RequestTemplate method(String method) {
this.method = checkNotNull(method, "method");
return this;
}
/* @see Request#method() */
public String method() {
return method;
}
/* @see #url() */
public RequestTemplate append(CharSequence value) {
url.append(value);
url = pullAnyQueriesOutOfUrl(url);
return this;
}
/* @see #url() */
public RequestTemplate insert(int pos, CharSequence value) {
url.insert(pos, value);
url = pullAnyQueriesOutOfUrl(url);
return this;
}
public String url() {
return url.toString();
}
/**
* Replaces queries with the specified {@code configKey} with url decoded
* {@code values} supplied.
*
* When the {@code value} is {@code null}, all queries with the {@code configKey}
* are removed.
*
*
relationship to JAXRS 2.0
*
* Like {@code WebTarget.query}, except the values can be templatized.
*
* ex.
*
*
* template.query("Signature", "{signature}");
*
*
* @param configKey the configKey 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(String configKey, String... values) {
queries.remove(checkNotNull(configKey, "configKey"));
if (values != null && values.length > 0 && values[0] != null) {
ArrayList encoded = new ArrayList();
for (String value : values) {
encoded.add(encodeIfNotVariable(value));
}
this.queries.put(encodeIfNotVariable(configKey), encoded);
}
return this;
}
/* @see #query(String, String...) */
public RequestTemplate query(String configKey, Iterable values) {
if (values != null)
return query(configKey, toArray(values, String.class));
return query(configKey, (String[]) null);
}
private 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.
*
*
relationship to JAXRS 2.0
*
* Like {@code WebTarget.queries}, except the values can be templatized.
*
* ex.
*
*
* template.queries(ImmutableMultimap.of("Signature", "{signature}"));
*
*
* @param queries if null, remove all queries. else value to replace all queries
* with.
* @see #queries()
*/
public RequestTemplate queries(Map> queries) {
if (queries == null || queries.isEmpty()) {
this.queries.clear();
} else {
for (Entry> 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> queries() {
Map> decoded = new LinkedHashMap>();
for (String field : queries.keySet()) {
Collection decodedValues = new ArrayList();
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.
*
* When the {@code value} is {@code null}, all headers with the {@code configKey}
* are removed.
*
*
relationship to JAXRS 2.0
*
* Like {@code WebTarget.queries} and {@code javax.ws.rs.client.Invocation.Builder.header},
* except the values can be templatized.
*
* ex.
*
*
* template.query("X-Application-Version", "{version}");
*
*
* @param configKey the configKey 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 configKey, String... values) {
checkNotNull(configKey, "header configKey");
if (values == null || (values.length == 1 && values[0] == null)) {
headers.remove(configKey);
} else {
List headers = new ArrayList();
headers.addAll(Arrays.asList(values));
this.headers.put(configKey, headers);
}
return this;
}
/* @see #header(String, String...) */
public RequestTemplate header(String configKey, Iterable values) {
if (values != null)
return header(configKey, toArray(values, String.class));
return header(configKey, (String[]) null);
}
/**
* Replaces all existing headers with the newly supplied headers.
*
*
relationship to JAXRS 2.0
*
* Like {@code Invocation.Builder.headers(MultivaluedMap)}, except the
* values can be templatized.
*
* ex.
*
*
* template.headers(ImmutableMultimap.of("X-Application-Version", "{version}"));
*
*
* @param headers if null, remove all headers. else value to replace all headers
* with.
* @see #headers()
*/
public RequestTemplate headers(Map> 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> headers() {
return Collections.unmodifiableMap(headers);
}
/**
* replaces the {@link feign.Util#CONTENT_LENGTH} header.
*
* Usually populated by an {@link feign.codec.Encoder}.
*
* @see Request#body()
*/
public RequestTemplate body(String body) {
this.body = body;
if (this.body != null) {
byte[] contentLength = body.getBytes(UTF_8);
header(CONTENT_LENGTH, String.valueOf(contentLength.length));
}
this.bodyTemplate = null;
return this;
}
/* @see Request#body() */
public String body() {
return body;
}
/**
* populated by {@link Body}
*
* @see Request#body()
*/
public RequestTemplate bodyTemplate(String bodyTemplate) {
this.bodyTemplate = bodyTemplate;
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 {@link #body()}, this will extract
* them out.
*
* @return
*/
private StringBuilder pullAnyQueriesOutOfUrl(StringBuilder url) {
// parse out queries
int queryIndex = url.indexOf("?");
if (queryIndex != -1) {
String queryLine = url.substring(queryIndex + 1);
Map> firstQueries = parseAndDecodeQueries(queryLine);
if (!queries.isEmpty()) {
firstQueries.putAll(queries);
queries.clear();
}
queries.putAll(firstQueries);
return new StringBuilder(url.substring(0, queryIndex));
}
return url;
}
private static Map> parseAndDecodeQueries(String queryLine) {
Map> map = new LinkedHashMap>();
if (emptyToNull(queryLine) == null)
return map;
if (queryLine.indexOf('&') == -1) {
if (queryLine.indexOf('=') != -1)
putKV(queryLine, map);
else
map.put(queryLine, null);
} 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> 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 values = map.containsKey(key) ? map.get(key) : new ArrayList();
values.add(value);
map.put(key, values);
}
@Override public String toString() {
return request().toString();
}
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();
}
private static final long serialVersionUID = 1L;
}