All Downloads are FREE. Search and download functionalities are using the official Maven repository.

feign.RequestTemplate Maven / Gradle / Ivy

/*
 * 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.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.
 * 
*

relationship to JAXRS 2.0
*
* 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 { interface Factory { /** create a request template using args passed to a method invocation. */ RequestTemplate create(Object[] argv); } private String method; /* final to encourage mutable use vs replacing the object. */ private StringBuilder url = new StringBuilder(); private final Map> queries = new LinkedHashMap>(); private final Map> headers = new LinkedHashMap>(); private transient Charset charset; private byte[] 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.charset = toCopy.charset; this.body = toCopy.body; this.bodyTemplate = toCopy.bodyTemplate; } /** * Resolves any template parameters in the requests path, query, or headers * against the supplied unencoded arguments. *
*

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) { replaceQueryValues(unencoded); Map encoded = new LinkedHashMap(); for (Entry entry : unencoded.entrySet()) { encoded.put(entry.getKey(), urlEncode(String.valueOf(entry.getValue()))); } 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, charset); } 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, pullAnyQueriesOutOfUrl(new StringBuilder(value))); 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 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(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(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)) { //Queryies 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.isEmpty()) return true; for(String val : values) { if(val != null) return false; } return true; } 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(); } /** * Replaces query values which are templated with corresponding values from the {@code unencoded} map. * Any unresolved queries are removed. */ public void replaceQueryValues(Map unencoded) { 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)) { values.add(urlEncode(String.valueOf(val))); } } else { values.add(urlEncode(String.valueOf(variableValue))); } } 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(); } private static final long serialVersionUID = 1L; }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy