>();
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() */
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.
*
Values can be passed in decoded or in url-encoded form depending on the value of the
* {@code encoded} parameter.
*
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}");
*
*
Note: 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.
*
ex.
*
* template.query(true, "param[]", "value");
* template.query(false, "param[]", "value");
*
*
* @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 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)}
* @see #query(boolean, String, String...)
*/
public RequestTemplate query(String name, Iterable values) {
return doQuery(false, name, values);
}
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 paramValues = new ArrayList();
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 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.
*
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 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 headers = new ArrayList();
headers.addAll(Arrays.asList(values));
this.headers.put(name, headers);
}
return this;
}
/* @see #header(String, String...) */
public RequestTemplate header(String name, Iterable 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.
relationship to
* JAXRS 2.0
Like {@code Invocation.Builder.headers(MultivaluedMap)}, except the
* values can be templatized.
ex.
*
* template.headers(mapOf("X-Application-Version", asList("{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(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.
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> 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 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 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 unencoded) {
replaceQueryValues(unencoded, Collections.emptyMap());
}
/**
* Replaces query values which are templated with corresponding values from the {@code unencoded}
* map. Any unresolved queries are removed.
*/
void replaceQueryValues(Map unencoded, Map alreadyEncoded) {
Iterator>> iterator = queries.entrySet().iterator();
while (iterator.hasNext()) {
Entry> entry = iterator.next();
if (entry.getValue() == null) {
continue;
}
Collection values = new ArrayList();
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);
}
}