package com.stripe.android.util;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.Size;
import android.support.annotation.VisibleForTesting;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
/**
* A set of JSON parsing utility functions.
*/
public class StripeJsonUtils {
static final String EMPTY = "";
static final String NULL = "null";
/**
* Calls through to {@link JSONObject#getString(String)} while safely
* converting the raw string "null" and the empty string to {@code null}.
*
* @param jsonObject the input object
* @param fieldName the required field name
* @return the value stored in the requested field
* @throws JSONException if the field does not exist
*/
@Nullable
public static String getString(
@NonNull JSONObject jsonObject,
@NonNull @Size(min = 1) String fieldName) throws JSONException {
return nullIfNullOrEmpty(jsonObject.getString(fieldName));
}
/**
* Calls through to {@link JSONObject#optInt(String)} only in the case that the
* key exists. This returns {@code null} if the key is not in the object.
*
* @param jsonObject the input object
* @param fieldName the required field name
* @return the value stored in the requested field, or {@code null} if the key is not present
*/
@Nullable
public static Integer optInteger(
@NonNull JSONObject jsonObject,
@NonNull @Size(min = 1) String fieldName) {
if (!jsonObject.has(fieldName)) {
return null;
}
return jsonObject.optInt(fieldName);
}
/**
* Calls through to {@link JSONObject#optLong(String)} only in the case that the
* key exists. This returns {@code null} if the key is not in the object.
*
* @param jsonObject the input object
* @param fieldName the required field name
* @return the value stored in the requested field, or {@code null} if the key is not present
*/
@Nullable
public static Long optLong(
@NonNull JSONObject jsonObject,
@NonNull @Size(min = 1) String fieldName) {
if (!jsonObject.has(fieldName)) {
return null;
}
return jsonObject.optLong(fieldName);
}
/**
* Calls through to {@link JSONObject#optString(String)} while safely
* converting the raw string "null" and the empty string to {@code null}. Will not throw
* an exception if the field isn't found.
*
* @param jsonObject the input object
* @param fieldName the optional field name
* @return the value stored in the field, or {@code null} if the field isn't present
*/
@Nullable
public static String optString(
@NonNull JSONObject jsonObject,
@NonNull @Size(min = 1) String fieldName) {
return nullIfNullOrEmpty(jsonObject.optString(fieldName));
}
/**
* Calls through to {@link JSONObject#optString(String)} while safely converting
* the raw string "null" and the empty string to {@code null}, along with any value that isn't
* a two-character string.
* @param jsonObject the object from which to retrieve the country code
* @param fieldName the name of the field in which the country code is stored
* @return a two-letter country code if one is found, or {@code null}
*/
@Nullable
@Size(2)
public static String optCountryCode(
@NonNull JSONObject jsonObject,
@NonNull @Size(min = 1) String fieldName) {
String value = nullIfNullOrEmpty(jsonObject.optString(fieldName));
if (value != null && value.length() == 2) {
return value;
}
return null;
}
/**
* Calls through to {@link JSONObject#optString(String)} while safely converting
* the raw string "null" and the empty string to {@code null}, along with any value that isn't
* a three-character string.
* @param jsonObject the object from which to retrieve the currency code
* @param fieldName the name of the field in which the currency code is stored
* @return a three-letter currency code if one is found, or {@code null}
*/
@Nullable
@Size(3)
public static String optCurrency(
@NonNull JSONObject jsonObject,
@NonNull @Size(min = 1) String fieldName) {
String value = nullIfNullOrEmpty(jsonObject.optString(fieldName));
if (value != null && value.length() == 3) {
return value;
}
return null;
}
/**
* Convert a {@link JSONObject} to a {@link Map}.
*
* @param jsonObject a {@link JSONObject} to be converted
* @return a {@link Map} representing the input, or {@code null} if the input is {@code null}
*/
@Nullable
public static Map<String, Object> jsonObjectToMap(@Nullable JSONObject jsonObject) {
if (jsonObject == null) {
return null;
}
Map<String, Object> map = new HashMap<>();
Iterator<String> keyIterator = jsonObject.keys();
while(keyIterator.hasNext()) {
String key = keyIterator.next();
Object value = jsonObject.opt(key);
if (NULL.equals(value) || value == null) {
continue;
}
if (value instanceof JSONObject) {
map.put(key, jsonObjectToMap((JSONObject) value));
} else if (value instanceof JSONArray) {
map.put(key, jsonArrayToList((JSONArray) value));
} else {
map.put(key, value);
}
}
return map;
}
/**
* Convert a {@link JSONObject} to a flat, string-keyed and string-valued map. All values
* are recorded as strings.
*
* @param jsonObject the input {@link JSONObject} to be converted
* @return a {@link Map} representing the input, or {@code null} if the input is {@code null}
*/
@Nullable
public static Map<String, String> jsonObjectToStringMap(@Nullable JSONObject jsonObject) {
if (jsonObject == null) {
return null;
}
Map<String, String> map = new HashMap<>();
Iterator<String> keyIterator = jsonObject.keys();
while (keyIterator.hasNext()) {
String key = keyIterator.next();
Object value = jsonObject.opt(key);
if (NULL.equals(value) || value == null) {
continue;
}
map.put(key, value.toString());
}
return map;
}
/**
* Converts a {@link JSONArray} to a {@link List}.
*
* @param jsonArray a {@link JSONArray} to be converted
* @return a {@link List} representing the input, or {@code null} if said input is {@code null}
*/
@Nullable
public static List<Object> jsonArrayToList(@Nullable JSONArray jsonArray) {
if (jsonArray == null) {
return null;
}
List<Object> objectList = new ArrayList<>();
for (int i = 0; i < jsonArray.length(); i++) {
try {
Object ob = jsonArray.get(i);
if (ob instanceof JSONArray) {
objectList.add(jsonArrayToList((JSONArray) ob));
} else if (ob instanceof JSONObject) {
Map<String, Object> objectMap = jsonObjectToMap((JSONObject) ob);
if (objectMap != null) {
objectList.add(objectMap);
}
} else {
if (NULL.equals(ob)) {
continue;
}
objectList.add(ob);
}
} catch (JSONException ignored) {
// Nothing to do in this case.
}
}
return objectList;
}
/**
* Converts a string-keyed {@link Map} into a {@link JSONObject}. This will cause a
* {@link ClassCastException} if any sub-map has keys that are not {@link String Strings}.
*
* @param mapObject the {@link Map} that you'd like in JSON form
* @return a {@link JSONObject} representing the input map, or {@code null} if the input
* object is {@code null}
*/
@Nullable
@SuppressWarnings("unchecked")
public static JSONObject mapToJsonObject(@Nullable Map<String, ? extends Object> mapObject) {
if (mapObject == null) {
return null;
}
JSONObject jsonObject = new JSONObject();
for (String key : mapObject.keySet()) {
Object value = mapObject.get(key);
if (value == null) {
continue;
}
try {
if (value instanceof Map<?, ?>) {
try {
Map<String, Object> mapValue = (Map<String, Object>) value;
jsonObject.put(key, mapToJsonObject(mapValue));
} catch (ClassCastException classCastException) {
// We don't include the item in the JSONObject if the keys are not Strings.
}
} else if (value instanceof List<?>) {
jsonObject.put(key, listToJsonArray((List<Object>) value));
} else if (value instanceof Number || value instanceof Boolean) {
jsonObject.put(key, value);
} else {
jsonObject.put(key, value.toString());
}
} catch (JSONException jsonException) {
continue;
}
}
return jsonObject;
}
/**
* Converts a {@link List} into a {@link JSONArray}. A {@link ClassCastException} will be
* thrown if any object in the list (or any sub-list or sub-map) is a {@link Map} whose keys
* are not {@link String Strings}.
*
* @param values a {@link List} of values to be put in a {@link JSONArray}
* @return a {@link JSONArray}, or {@code null} if the input was {@code null}
*/
@Nullable
@SuppressWarnings("unchecked")
public static JSONArray listToJsonArray(@Nullable List values) {
if (values == null) {
return null;
}
JSONArray jsonArray = new JSONArray();
for (Object object : values) {
if (object instanceof Map<?, ?>) {
try {
Map<String, Object> mapObject = (Map<String, Object>) object;
jsonArray.put(mapToJsonObject(mapObject));
} catch (ClassCastException classCastException) {
// We don't include the item in the array if the keys are not Strings.
}
} else if (object instanceof List<?>) {
jsonArray.put(listToJsonArray((List) object));
} else if (object instanceof Number || object instanceof Boolean) {
jsonArray.put(object);
} else {
jsonArray.put(object.toString());
}
}
return jsonArray;
}
/**
* Util function for putting a string value into a {@link JSONObject} if that
* string is not null or empty. This ignores any {@link JSONException} that may be thrown
* due to insertion.
*
* @param jsonObject the {@link JSONObject} into which to put the field
* @param fieldName the field name
* @param value the potential field value
*/
public static void putStringIfNotNull(
@NonNull JSONObject jsonObject,
@NonNull @Size(min = 1) String fieldName,
@Nullable String value) {
if (!StripeTextUtils.isBlank(value)) {
try {
jsonObject.put(fieldName, value);
} catch (JSONException ignored) { }
}
}
/**
* Util function for putting an integer value into a {@link JSONObject} if that
* value is not null. This ignores any {@link JSONException} that may be thrown
* due to insertion.
*
* @param jsonObject the {@link JSONObject} into which to put the field
* @param fieldName the field name
* @param value the potential field value
*/
public static void putIntegerIfNotNull(
@NonNull JSONObject jsonObject,
@NonNull @Size(min = 1) String fieldName,
@Nullable Integer value) {
if (value == null) {
return;
}
try {
jsonObject.put(fieldName, value.intValue());
} catch (JSONException ignored) { }
}
@Nullable
public static String nullIfNullOrEmpty(@Nullable String possibleNull) {
return NULL.equals(possibleNull) || EMPTY.equals(possibleNull)
? null
: possibleNull;
}
}