com.stripe.net.FormEncoder Maven / Gradle / Ivy
package com.stripe.net;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.Array;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
public final class FormEncoder {
public static HttpContent createHttpContent(Map params) throws IOException {
// If params is null, we create an empty HttpContent because we still want to send the
// Content-Type header.
if (params == null) {
return HttpContent.buildFormURLEncodedContent(new ArrayList>());
}
Collection> flatParams = flattenParams(params);
// If all parameters have been encoded as strings, then the content can be represented
// with application/x-www-form-url-encoded encoding. Otherwise, use
// multipart/form-data encoding.
if (flatParams.stream().allMatch(kvp -> kvp.getValue() instanceof String)) {
Collection> flatParamsString =
flatParams.stream()
.filter(kvp -> kvp.getValue() instanceof String)
.map(kvp -> new KeyValuePair(kvp.getKey(), (String) kvp.getValue()))
.collect(Collectors.toList());
return HttpContent.buildFormURLEncodedContent(flatParamsString);
} else {
return HttpContent.buildMultipartFormDataContent(flatParams);
}
}
/**
* Creates the HTTP query string for a given map of parameters.
*
* @param params The map of parameters.
* @return The query string.
*/
public static String createQueryString(Map params) {
if (params == null) {
return "";
}
Collection> flatParams =
flattenParams(params).stream()
.filter(kvp -> kvp.getValue() instanceof String)
.map(kvp -> new KeyValuePair(kvp.getKey(), (String) kvp.getValue()))
.collect(Collectors.toList());
return createQueryString(flatParams);
}
/**
* Creates the HTTP query string for a collection of name/value tuples.
*
* @param nameValueCollection The collection of name/value tuples.
* @return The query string.
*/
public static String createQueryString(
Collection> nameValueCollection) {
if (nameValueCollection == null) {
return "";
}
return String.join(
"&",
nameValueCollection.stream()
.map(kvp -> String.format("%s=%s", urlEncode(kvp.getKey()), urlEncode(kvp.getValue())))
.collect(Collectors.toList()));
}
/**
* Returns a list of flattened parameters for the given map of parameters.
*
* This is a "pre-encoding" step necessary to send requests to Stripe's API. Form encoding can
* be ambiguous when it comes to nested parameters (lists or maps). Stripe's API relies heavily on
* such parameters and expects them to be encoded in a certain way. This method takes a map of
* parameters that can contain deeply nested parameters and return a flat list of key/value pairs.
*
*
Values are always encoded as {@link String}s, except for {@link File} and {@link
* InputStream} values that are left as-is. When there is at least one {@link File} or {@link
* InputStream} value, the request should be encoded using {@code multipart/form-data} MIME type;
* otherwise (i.e. if all values are {@link String}s), the request should be encoded using {@code
* application/x-www-form-urlencoded} MIME type.
*
*
{@code
* Map item1 = new HashMap<>() { put("plan", "gold"); };
* Map item2 = new HashMap<>() { put("plan", "silver"); };
* List
*
* @param params The map of parameters.
* @return The flattened list of parameters.
*/
public static List> flattenParams(Map params) {
return flattenParamsValue(params, null);
}
/**
* URL-encodes a string.
*
* @param value The string to URL-encode.
* @return The URL-encoded string.
*/
private static String urlEncode(String value) {
if (value == null) {
return null;
}
try {
// Don't use strict form encoding by changing the square bracket control
// characters back to their literals. This is fine by the server, and
// makes these parameter strings easier to read.
return URLEncoder.encode(value, StandardCharsets.UTF_8.name())
.replaceAll("%5B", "[")
.replaceAll("%5D", "]");
} catch (UnsupportedEncodingException e) {
// This can literally never happen, and lets us avoid having to catch
// UnsupportedEncodingException in callers.
throw new AssertionError("UTF-8 is unknown");
}
}
/**
* Returns a list of parameters for a given value. The value can be basically anything, as long as
* it can be encoded in some way.
*
* @param value The value for which to create the list of parameters.
* @param keyPrefix The key under which new keys should be nested, if any.
* @return The list of parameters.
*/
private static List> flattenParamsValue(
Object value, String keyPrefix) {
List> flatParams = null;
// I wish Java had pattern matching :(
if (value == null) {
flatParams = singleParam(keyPrefix, "");
} else if (value instanceof Map, ?>) {
flatParams = flattenParamsMap((Map, ?>) value, keyPrefix);
} else if (value instanceof String) {
flatParams = singleParam(keyPrefix, value);
} else if (value instanceof File) {
flatParams = singleParam(keyPrefix, value);
} else if (value instanceof InputStream) {
flatParams = singleParam(keyPrefix, value);
} else if (value instanceof Collection>) {
flatParams = flattenParamsCollection((Collection>) value, keyPrefix);
} else if (value.getClass().isArray()) {
Object[] array = getArrayForObject(value);
Collection> collection = Arrays.stream(array).collect(Collectors.toList());
flatParams = flattenParamsCollection(collection, keyPrefix);
} else if (value.getClass().isEnum()) {
flatParams =
singleParam(keyPrefix, ApiResource.INTERNAL_GSON.toJson(value).replaceAll("\"", ""));
} else {
flatParams = singleParam(keyPrefix, value.toString());
}
return flatParams;
}
/**
* Returns a list of parameters for a given map. If a key prefix is provided, the keys for the new
* parameters will be nested under the key prefix. E.g. if the key prefix `foo` is passed and the
* map contains a key `bar`, then a parameter with key `foo[bar]` will be returned.
*
* @param map The map for which to create the list of parameters.
* @param keyPrefix The key under which new keys should be nested, if any.
* @return The list of parameters.
*/
private static List> flattenParamsMap(
Map, ?> map, String keyPrefix) {
List> flatParams = new ArrayList>();
if (map == null) {
return flatParams;
}
for (Map.Entry, ?> entry : map.entrySet()) {
String key = entry.getKey().toString();
Object value = entry.getValue();
String newPrefix = newPrefix(key, keyPrefix);
flatParams.addAll(flattenParamsValue(value, newPrefix));
}
return flatParams;
}
/**
* Returns a list of parameters for a given collection of objects. The parameter keys will be
* indexed under the `keyPrefix` parameter. E.g. if the `keyPrefix` is `foo`, then the key for the
* first element's will be `foo[0]`, etc.
*
* @param collection The collection for which to create the list of parameters.
* @param keyPrefix The key under which new keys should be nested.
* @return The list of parameters.
*/
private static List> flattenParamsCollection(
Collection> collection, String keyPrefix) {
List> flatParams = new ArrayList>();
if (collection == null) {
return flatParams;
}
int index = 0;
for (Object value : collection) {
String newPrefix = String.format("%s[%d]", keyPrefix, index);
flatParams.addAll(flattenParamsValue(value, newPrefix));
index += 1;
}
/* Because application/x-www-form-urlencoded cannot represent an empty list, convention
* is to take the list parameter and just set it to an empty string. (E.g. A regular
* list might look like `a[0]=1&b[1]=2`. Emptying it would look like `a=`.) */
if (flatParams.isEmpty()) {
flatParams.add(new KeyValuePair(keyPrefix, ""));
}
return flatParams;
}
/**
* Creates a list containing a single parameter.
*
* @param key The parameter's key.
* @param value The parameter's value.
* @return A list containg the single parameter.
*/
private static List> singleParam(String key, Object value) {
List> flatParams = new ArrayList>();
flatParams.add(new KeyValuePair(key, value));
return flatParams;
}
/**
* Computes the new key prefix, given a key and an existing prefix (if any). E.g. if the key is
* `bar` and the existing prefix is `foo`, then `foo[bar]` is returned.
*
* If a key already contains nested values, then only the non-nested part is nested under the
* prefix, e.g. if the key is `bar[baz]` and the prefix is `foo`, then `foo[bar][baz]` is
* returned.
*
*
If no prefix is provided, the key is returned unchanged.
*
* @param key The key.
* @param keyPrefix The existing key prefix, if any.
* @return The new key prefix.
*/
private static String newPrefix(String key, String keyPrefix) {
if (keyPrefix == null || keyPrefix.isEmpty()) {
return key;
}
int i = key.indexOf("[");
if (i == -1) {
return String.format("%s[%s]", keyPrefix, key);
} else {
return String.format("%s[%s][%s]", keyPrefix, key.substring(0, i), key.substring(i));
}
}
/**
* Accepts an object (that must be an array) and returns an `Object[]`. If the array already holds
* reference types, it is simply cast to `Object[]`. If the array holds primitive types, a new
* array is created with elements copied and boxed to the appropriate wrapper class. E.g. an
* `int[]` array will be returned as an `Integer[]` array.
*
* @param array The original array, as an Object.
* @return The Object[] array.
*/
private static Object[] getArrayForObject(Object array) {
if (!array.getClass().isArray()) {
throw new IllegalArgumentException("parameter is not an array");
}
// If element type is not a primitive, simply cast the object and return
if (!array.getClass().getComponentType().isPrimitive()) {
return (Object[]) array;
}
// Otherwise, initialize a new array of Object and copy elements one by one. Primitive
// elements will be autoboxed.
int length = Array.getLength(array);
Object[] newArray = new Object[length];
for (int index = 0; index < length; index++) {
newArray[index] = Array.get(array, index);
}
return newArray;
}
}