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

com.stripe.net.ApiRequestParamsConverter Maven / Gradle / Ivy

There is a newer version: 26.13.0-beta.1
Show newest version
package com.stripe.net;

import com.google.gson.FieldNamingPolicy;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParseException;
import com.google.gson.TypeAdapter;
import com.google.gson.TypeAdapterFactory;
import com.google.gson.reflect.TypeToken;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonWriter;
import com.stripe.Stripe;
import com.stripe.param.common.EmptyParam;
import java.io.IOException;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.Map;

/**
 * Converter to map an api request object to an untyped map. It is not called a *Serializer because
 * the outcome is not a JSON data. It is not called *UntypedMapDeserializer because it is not
 * converting from JSON.
 */
class ApiRequestParamsConverter {
  /** Strategy to flatten extra params in the API request parameters. */
  private static class ExtraParamsFlatteningStrategy implements UntypedMapDeserializer.Strategy {
    @Override
    public void deserializeAndTransform(
        Map outerMap,
        Map.Entry jsonEntry,
        UntypedMapDeserializer untypedMapDeserializer) {
      String key = jsonEntry.getKey();
      JsonElement jsonValue = jsonEntry.getValue();
      if (ApiRequestParams.EXTRA_PARAMS_KEY.equals(key)) {
        if (!jsonValue.isJsonObject()) {
          throw new IllegalStateException(
              String.format(
                  "Unexpected schema for extra params. JSON object is expected at key `%s`, but found"
                      + " `%s`. This is likely a problem with this current library version `%s`. "
                      + "Please contact [email protected] for assistance.",
                  ApiRequestParams.EXTRA_PARAMS_KEY, jsonValue, Stripe.VERSION));
        }
        // JSON value now corresponds to the extra params map, and is also deserialized as a map.
        // Instead of putting this result map under the original key, flatten the map
        // by adding all its key/value pairs to the outer map instead.
        Map extraParamsMap =
            untypedMapDeserializer.deserialize(jsonValue.getAsJsonObject());
        for (Map.Entry entry : extraParamsMap.entrySet()) {
          validateDuplicateKey(outerMap, entry.getKey(), entry.getValue());
          outerMap.put(entry.getKey(), entry.getValue());
        }
      } else {
        Object value = untypedMapDeserializer.deserializeJsonElement(jsonValue);
        validateDuplicateKey(outerMap, key, value);

        // Normal deserialization where output map has the same structure as the given JSON content.
        // The deserialized content is an untyped `Object` and added to the outer map at the
        // original key.
        outerMap.put(key, value);
      }
    }
  }

  private static void validateDuplicateKey(
      Map outerMap, String paramKey, Object paramValue) {
    if (outerMap.containsKey(paramKey)) {
      throw new IllegalArgumentException(
          String.format(
              "Found multiple param values for the same param key. This can happen because you passed "
                  + "additional parameters via `putExtraParam` that conflict with the existing params. "
                  + "Found param key `%s` with values `%s` and `%s`. "
                  + "If you wish to pass additional params for nested parameters, you "
                  + "should add extra params at the nested params themselves, not from the "
                  + "top-level param.",
              paramKey, outerMap.get(paramKey), paramValue));
    }
  }

  private static class HasNullMetadataTypeAdapterFactory implements TypeAdapterFactory {
    @SuppressWarnings("unchecked")
    @Override
    public  TypeAdapter create(Gson gson, TypeToken type) {
      if (!Map.class.equals(type.getRawType())) {
        return null;
      }

      // If non-parameterized `Map` alone is given it cannot be metadata with string key/value
      if (!(type.getType() instanceof ParameterizedType)) {
        return null;
      }
      ParameterizedType parameterizedMap = (ParameterizedType) type.getType();
      Type keyType = parameterizedMap.getActualTypeArguments()[0];
      Type valueType = parameterizedMap.getActualTypeArguments()[1];

      // Reject of other types that can't be metadata
      if (!String.class.equals(keyType) || !String.class.equals(valueType)) {
        return null;
      }

      TypeAdapter> paramEnum =
          new TypeAdapter>() {
            @Override
            public void write(JsonWriter out, Map value) throws IOException {
              if (value != null) {
                boolean previousSetting = out.getSerializeNulls();
                out.setSerializeNulls(true);

                out.beginObject();
                value.entrySet().stream()
                    .forEach(
                        entry -> {
                          try {
                            out.name(entry.getKey());
                            out.value(entry.getValue());
                          } catch (IOException e) {
                            throw new JsonParseException(
                                String.format(
                                    "Unable to serialize metadata with key=%ss, value=%s}",
                                    entry.getKey(), entry.getValue()));
                          }
                        });

                out.endObject();
                out.setSerializeNulls(previousSetting);
              }
            }

            @Override
            public Map read(JsonReader in) {
              throw new UnsupportedOperationException(
                  "No deserialization is expected from this private type adapter.");
            }
          };
      return (TypeAdapter) paramEnum.nullSafe();
    }
  }

  /**
   * Type adapter to convert an empty enum to null value to comply with the lower-lever encoding
   * logic for the API request parameters.
   */
  private static class HasEmptyEnumTypeAdapterFactory implements TypeAdapterFactory {
    @SuppressWarnings("unchecked")
    @Override
    public  TypeAdapter create(Gson gson, TypeToken type) {
      if (!ApiRequestParams.EnumParam.class.isAssignableFrom(type.getRawType())) {
        return null;
      }

      TypeAdapter paramEnum =
          new TypeAdapter() {
            @Override
            public void write(JsonWriter out, ApiRequestParams.EnumParam value) throws IOException {
              if (value.getValue().equals("")) {
                // need to restore serialize null setting
                // not to affect other fields
                boolean previousSetting = out.getSerializeNulls();
                out.setSerializeNulls(true);
                out.nullValue();
                out.setSerializeNulls(previousSetting);
              } else {
                out.value(value.getValue());
              }
            }

            @Override
            public ApiRequestParams.EnumParam read(JsonReader in) {
              throw new UnsupportedOperationException(
                  "No deserialization is expected from this private type adapter for enum param.");
            }
          };
      return (TypeAdapter) paramEnum.nullSafe();
    }
  }

  private static final Gson GSON =
      new GsonBuilder()
          .setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES)
          .registerTypeAdapterFactory(
              new ApiRequestParamsConverter.HasEmptyEnumTypeAdapterFactory())
          .registerTypeAdapterFactory(new HasNullMetadataTypeAdapterFactory())
          .create();

  private static final UntypedMapDeserializer FLATTENING_EXTRA_PARAMS_DESERIALIZER =
      new UntypedMapDeserializer(new ExtraParamsFlatteningStrategy());

  /**
   * Convert the given request params into an untyped map. This map is composed of {@code
   * Map}, {@code List}, and basic Java data types. This allows you to test
   * building the request params and verify compatibility with your prior integrations using the
   * untyped params map {@link ApiResource#request(ApiResource.RequestMethod, String, Map, Class,
   * RequestOptions)}.
   *
   * 

There are two peculiarities in this conversion: * *

1) {@link EmptyParam#EMPTY}, containing a raw empty string value, is converted to null. This * is because the form-encoding layer prohibits passing empty string as a param map value. It, * however, allows a null value in the map (present key but null value). Because of the * translation from `EMPTY` enum to null, deserializing this map back to a request instance is * lossy. The null value will not be converted back to the `EMPTY` enum. * *

2) Parameter with serialized name {@link ApiRequestParams#EXTRA_PARAMS_KEY} will be * flattened. This is to support passing new params that the current library has not yet * supported. */ Map convert(ApiRequestParams apiRequestParams) { JsonObject jsonParams = GSON.toJsonTree(apiRequestParams).getAsJsonObject(); return FLATTENING_EXTRA_PARAMS_DESERIALIZER.deserialize(jsonParams); } }