package io.sentry.android.core; import com.google.gson.FieldNamingStrategy; import com.google.gson.Gson; import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.google.gson.JsonParser; import com.google.gson.TypeAdapter; import com.google.gson.TypeAdapterFactory; import com.google.gson.internal.Excluder; import com.google.gson.reflect.TypeToken; import com.google.gson.stream.JsonReader; import com.google.gson.stream.JsonWriter; import io.sentry.core.IUnknownPropertiesConsumer; import java.io.IOException; import java.lang.reflect.Field; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.Map; final class UnknownPropertiesTypeAdapterFactory implements TypeAdapterFactory { private static final TypeAdapterFactory instance = new UnknownPropertiesTypeAdapterFactory(); private UnknownPropertiesTypeAdapterFactory() {} static TypeAdapterFactory get() { return instance; } @Override public <T> TypeAdapter<T> create(final Gson gson, final TypeToken<T> typeToken) { // Check if we can deal with the given type if (!IUnknownPropertiesConsumer.class.isAssignableFrom(typeToken.getRawType())) { return null; } // If we can, we should get the backing class to fetch its fields from @SuppressWarnings("unchecked") final Class<IUnknownPropertiesConsumer> rawType = (Class<IUnknownPropertiesConsumer>) typeToken.getRawType(); @SuppressWarnings("unchecked") final TypeAdapter<IUnknownPropertiesConsumer> delegateTypeAdapter = (TypeAdapter<IUnknownPropertiesConsumer>) gson.getDelegateAdapter(this, typeToken); // Excluder is necessary to check if the field can be processed // Basically it's not required, but it makes the check more complete final Excluder excluder = gson.excluder(); // This is crucial to map fields and JSON object properties since Gson supports name remapping final FieldNamingStrategy fieldNamingStrategy = gson.fieldNamingStrategy(); final TypeAdapter<IUnknownPropertiesConsumer> unknownPropertiesTypeAdapter = UnknownPropertiesTypeAdapter.create( rawType, delegateTypeAdapter, excluder, fieldNamingStrategy); @SuppressWarnings("unchecked") final TypeAdapter<T> castTypeAdapter = (TypeAdapter<T>) unknownPropertiesTypeAdapter; return castTypeAdapter; } private static final class UnknownPropertiesTypeAdapter<T extends IUnknownPropertiesConsumer> extends TypeAdapter<T> { private final TypeAdapter<T> typeAdapter; private final Collection<String> propertyNames; private UnknownPropertiesTypeAdapter( final TypeAdapter<T> typeAdapter, final Collection<String> propertyNames) { this.typeAdapter = typeAdapter; this.propertyNames = propertyNames; } private static <T extends IUnknownPropertiesConsumer> TypeAdapter<T> create( final Class<? super T> clazz, final TypeAdapter<T> typeAdapter, final Excluder excluder, final FieldNamingStrategy fieldNamingStrategy) { final Collection<String> propertyNames = getPropertyNames(clazz, excluder, fieldNamingStrategy); return new UnknownPropertiesTypeAdapter<>(typeAdapter, propertyNames); } private static Collection<String> getPropertyNames( final Class<?> clazz, final Excluder excluder, final FieldNamingStrategy fieldNamingStrategy) { final Collection<String> propertyNames = new ArrayList<>(); // Class fields are declared per class so we have to traverse the whole hierarchy for (Class<?> i = clazz; i.getSuperclass() != null && i != Object.class; i = i.getSuperclass()) { for (final Field declaredField : i.getDeclaredFields()) { // If the class field is not excluded if (!excluder.excludeField(declaredField, false)) { // We can translate the field name to its property name counter-part final String propertyName = fieldNamingStrategy.translateName(declaredField); propertyNames.add(propertyName); } } } return propertyNames; } @Override public void write(final JsonWriter out, final T value) throws IOException { typeAdapter.write(out, value); } @Override public T read(final JsonReader in) { // In its simplest solution, we can just collect a JSON tree because its much easier to // process JsonParser parser = new JsonParser(); JsonElement jsonElement = parser.parse(in); if (jsonElement == null || jsonElement.isJsonNull()) { return null; } final JsonObject jsonObjectToParse = jsonElement.getAsJsonObject(); Map<String, Object> unknownProperties = new HashMap<>(); for (final Map.Entry<String, JsonElement> e : jsonObjectToParse.entrySet()) { final String propertyName = e.getKey(); // Not in the object fields? if (!propertyNames.contains(propertyName)) { // Then we assume the property is unknown unknownProperties.put(propertyName, e.getValue()); } } // First convert the above JSON tree to an object final T object = typeAdapter.fromJsonTree(jsonObjectToParse); if (!unknownProperties.isEmpty()) { // And do the post-processing object.acceptUnknownProperties(unknownProperties); } return object; } } }