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

keycloakjar.com.google.gson.internal.bind.ReflectiveTypeAdapterFactory Maven / Gradle / Ivy

There is a newer version: 7.21.1
Show newest version
/*
 * Copyright (C) 2011 Google 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 com.google.gson.internal.bind;

import com.google.gson.FieldNamingStrategy;
import com.google.gson.Gson;
import com.google.gson.JsonIOException;
import com.google.gson.JsonParseException;
import com.google.gson.JsonSyntaxException;
import com.google.gson.ReflectionAccessFilter;
import com.google.gson.ReflectionAccessFilter.FilterResult;
import com.google.gson.TypeAdapter;
import com.google.gson.TypeAdapterFactory;
import com.google.gson.annotations.JsonAdapter;
import com.google.gson.annotations.SerializedName;
import com.google.gson.internal.$Gson$Types;
import com.google.gson.internal.ConstructorConstructor;
import com.google.gson.internal.Excluder;
import com.google.gson.internal.ObjectConstructor;
import com.google.gson.internal.Primitives;
import com.google.gson.internal.ReflectionAccessFilterHelper;
import com.google.gson.internal.reflect.ReflectionHelper;
import com.google.gson.reflect.TypeToken;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonToken;
import com.google.gson.stream.JsonWriter;
import java.io.IOException;
import java.lang.reflect.AccessibleObject;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Member;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

/**
 * Type adapter that reflects over the fields and methods of a class.
 */
public final class ReflectiveTypeAdapterFactory implements TypeAdapterFactory {
  private final ConstructorConstructor constructorConstructor;
  private final FieldNamingStrategy fieldNamingPolicy;
  private final Excluder excluder;
  private final JsonAdapterAnnotationTypeAdapterFactory jsonAdapterFactory;
  private final List reflectionFilters;

  public ReflectiveTypeAdapterFactory(ConstructorConstructor constructorConstructor,
      FieldNamingStrategy fieldNamingPolicy, Excluder excluder,
      JsonAdapterAnnotationTypeAdapterFactory jsonAdapterFactory,
      List reflectionFilters) {
    this.constructorConstructor = constructorConstructor;
    this.fieldNamingPolicy = fieldNamingPolicy;
    this.excluder = excluder;
    this.jsonAdapterFactory = jsonAdapterFactory;
    this.reflectionFilters = reflectionFilters;
  }

  private boolean includeField(Field f, boolean serialize) {
    return !excluder.excludeClass(f.getType(), serialize) && !excluder.excludeField(f, serialize);
  }

  /** first element holds the default name */
  private List getFieldNames(Field f) {
    SerializedName annotation = f.getAnnotation(SerializedName.class);
    if (annotation == null) {
      String name = fieldNamingPolicy.translateName(f);
      return Collections.singletonList(name);
    }

    String serializedName = annotation.value();
    String[] alternates = annotation.alternate();
    if (alternates.length == 0) {
      return Collections.singletonList(serializedName);
    }

    List fieldNames = new ArrayList<>(alternates.length + 1);
    fieldNames.add(serializedName);
    Collections.addAll(fieldNames, alternates);
    return fieldNames;
  }

  @Override
  public  TypeAdapter create(Gson gson, final TypeToken type) {
    Class raw = type.getRawType();

    if (!Object.class.isAssignableFrom(raw)) {
      return null; // it's a primitive!
    }

    FilterResult filterResult =
        ReflectionAccessFilterHelper.getFilterResult(reflectionFilters, raw);
    if (filterResult == FilterResult.BLOCK_ALL) {
      throw new JsonIOException(
          "ReflectionAccessFilter does not permit using reflection for " + raw
              + ". Register a TypeAdapter for this type or adjust the access filter.");
    }
    boolean blockInaccessible = filterResult == FilterResult.BLOCK_INACCESSIBLE;

    // If the type is actually a Java Record, we need to use the RecordAdapter instead. This will always be false
    // on JVMs that do not support records.
    if (ReflectionHelper.isRecord(raw)) {
      @SuppressWarnings("unchecked")
      TypeAdapter adapter = (TypeAdapter) new RecordAdapter<>(raw,
          getBoundFields(gson, type, raw, blockInaccessible, true), blockInaccessible);
      return adapter;
    }

    ObjectConstructor constructor = constructorConstructor.get(type);
    return new FieldReflectionAdapter<>(constructor, getBoundFields(gson, type, raw, blockInaccessible, false));
  }

  private static  void checkAccessible(Object object, M member) {
    if (!ReflectionAccessFilterHelper.canAccess(member, Modifier.isStatic(member.getModifiers()) ? null : object)) {
      String memberDescription = ReflectionHelper.getAccessibleObjectDescription(member, true);
      throw new JsonIOException(memberDescription + " is not accessible and ReflectionAccessFilter does not"
          + " permit making it accessible. Register a TypeAdapter for the declaring type, adjust the"
          + " access filter or increase the visibility of the element and its declaring type.");
    }
  }

  private BoundField createBoundField(
      final Gson context, final Field field, final Method accessor, final String name,
      final TypeToken fieldType, boolean serialize, boolean deserialize,
      final boolean blockInaccessible) {

    final boolean isPrimitive = Primitives.isPrimitive(fieldType.getRawType());

    int modifiers = field.getModifiers();
    final boolean isStaticFinalField = Modifier.isStatic(modifiers) && Modifier.isFinal(modifiers);

    JsonAdapter annotation = field.getAnnotation(JsonAdapter.class);
    TypeAdapter mapped = null;
    if (annotation != null) {
      // This is not safe; requires that user has specified correct adapter class for @JsonAdapter
      mapped = jsonAdapterFactory.getTypeAdapter(
          constructorConstructor, context, fieldType, annotation);
    }
    final boolean jsonAdapterPresent = mapped != null;
    if (mapped == null) mapped = context.getAdapter(fieldType);

    @SuppressWarnings("unchecked")
    final TypeAdapter typeAdapter = (TypeAdapter) mapped;
    return new BoundField(name, field, serialize, deserialize) {
      @Override void write(JsonWriter writer, Object source)
          throws IOException, IllegalAccessException {
        if (!serialized) return;
        if (blockInaccessible) {
          if (accessor == null) {
            checkAccessible(source, field);
          } else {
            // Note: This check might actually be redundant because access check for canonical
            // constructor should have failed already
            checkAccessible(source, accessor);
          }
        }

        Object fieldValue;
        if (accessor != null) {
          try {
            fieldValue = accessor.invoke(source);
          } catch (InvocationTargetException e) {
            String accessorDescription = ReflectionHelper.getAccessibleObjectDescription(accessor, false);
            throw new JsonIOException("Accessor " + accessorDescription + " threw exception", e.getCause());
          }
        } else {
          fieldValue = field.get(source);
        }
        if (fieldValue == source) {
          // avoid direct recursion
          return;
        }
        writer.name(name);
        TypeAdapter t = jsonAdapterPresent ? typeAdapter
            : new TypeAdapterRuntimeTypeWrapper<>(context, typeAdapter, fieldType.getType());
        t.write(writer, fieldValue);
      }

      @Override
      void readIntoArray(JsonReader reader, int index, Object[] target) throws IOException, JsonParseException {
        Object fieldValue = typeAdapter.read(reader);
        if (fieldValue == null && isPrimitive) {
          throw new JsonParseException("null is not allowed as value for record component '" + fieldName + "'"
              + " of primitive type; at path " + reader.getPath());
        }
        target[index] = fieldValue;
      }

      @Override
      void readIntoField(JsonReader reader, Object target)
          throws IOException, IllegalAccessException {
        Object fieldValue = typeAdapter.read(reader);
        if (fieldValue != null || !isPrimitive) {
          if (blockInaccessible) {
            checkAccessible(target, field);
          } else if (isStaticFinalField) {
            // Reflection does not permit setting value of `static final` field, even after calling `setAccessible`
            // Handle this here to avoid causing IllegalAccessException when calling `Field.set`
            String fieldDescription = ReflectionHelper.getAccessibleObjectDescription(field, false);
            throw new JsonIOException("Cannot set value of 'static final' " + fieldDescription);
          }
          field.set(target, fieldValue);
        }
      }
    };
  }

  private Map getBoundFields(Gson context, TypeToken type, Class raw,
                                                 boolean blockInaccessible, boolean isRecord) {
    Map result = new LinkedHashMap<>();
    if (raw.isInterface()) {
      return result;
    }

    Class originalRaw = raw;
    while (raw != Object.class) {
      Field[] fields = raw.getDeclaredFields();

      // For inherited fields, check if access to their declaring class is allowed
      if (raw != originalRaw && fields.length > 0) {
        FilterResult filterResult = ReflectionAccessFilterHelper.getFilterResult(reflectionFilters, raw);
        if (filterResult == FilterResult.BLOCK_ALL) {
          throw new JsonIOException("ReflectionAccessFilter does not permit using reflection for " + raw
              + " (supertype of " + originalRaw + "). Register a TypeAdapter for this type"
              + " or adjust the access filter.");
        }
        blockInaccessible = filterResult == FilterResult.BLOCK_INACCESSIBLE;
      }

      for (Field field : fields) {
        boolean serialize = includeField(field, true);
        boolean deserialize = includeField(field, false);
        if (!serialize && !deserialize) {
          continue;
        }
        // The accessor method is only used for records. If the type is a record, we will read out values
        // via its accessor method instead of via reflection. This way we will bypass the accessible restrictions
        Method accessor = null;
        if (isRecord) {
          // If there is a static field on a record, there will not be an accessor. Instead we will use the default
          // field serialization logic, but for deserialization the field is excluded for simplicity. Note that Gson
          // ignores static fields by default, but GsonBuilder.excludeFieldsWithModifiers can overwrite this.
          if (Modifier.isStatic(field.getModifiers())) {
            deserialize = false;
          } else {
            accessor = ReflectionHelper.getAccessor(raw, field);
            // If blockInaccessible, skip and perform access check later
            if (!blockInaccessible) {
              ReflectionHelper.makeAccessible(accessor);
            }

            // @SerializedName can be placed on accessor method, but it is not supported there
            // If field and method have annotation it is not easily possible to determine if accessor method
            // is implicit and has inherited annotation, or if it is explicitly declared with custom annotation
            if (accessor.getAnnotation(SerializedName.class) != null
                && field.getAnnotation(SerializedName.class) == null) {
              String methodDescription = ReflectionHelper.getAccessibleObjectDescription(accessor, false);
              throw new JsonIOException("@SerializedName on " + methodDescription + " is not supported");
            }
          }
        }

        // If blockInaccessible, skip and perform access check later
        // For Records if the accessor method is used the field does not have to be made accessible
        if (!blockInaccessible && accessor == null) {
          ReflectionHelper.makeAccessible(field);
        }
        Type fieldType = $Gson$Types.resolve(type.getType(), raw, field.getGenericType());
        List fieldNames = getFieldNames(field);
        BoundField previous = null;
        for (int i = 0, size = fieldNames.size(); i < size; ++i) {
          String name = fieldNames.get(i);
          if (i != 0) serialize = false; // only serialize the default name
          BoundField boundField = createBoundField(context, field, accessor, name,
              TypeToken.get(fieldType), serialize, deserialize, blockInaccessible);
          BoundField replaced = result.put(name, boundField);
          if (previous == null) previous = replaced;
        }
        if (previous != null) {
          throw new IllegalArgumentException("Class " + originalRaw.getName()
              + " declares multiple JSON fields named '" + previous.name + "'; conflict is caused"
              + " by fields " + ReflectionHelper.fieldToString(previous.field) + " and " + ReflectionHelper.fieldToString(field));
        }
      }
      type = TypeToken.get($Gson$Types.resolve(type.getType(), raw, raw.getGenericSuperclass()));
      raw = type.getRawType();
    }
    return result;
  }

  static abstract class BoundField {
    final String name;
    final Field field;
    /** Name of the underlying field */
    final String fieldName;
    final boolean serialized;
    final boolean deserialized;

    protected BoundField(String name, Field field, boolean serialized, boolean deserialized) {
      this.name = name;
      this.field = field;
      this.fieldName = field.getName();
      this.serialized = serialized;
      this.deserialized = deserialized;
    }

    /** Read this field value from the source, and append its JSON value to the writer */
    abstract void write(JsonWriter writer, Object source) throws IOException, IllegalAccessException;

    /** Read the value into the target array, used to provide constructor arguments for records */
    abstract void readIntoArray(JsonReader reader, int index, Object[] target) throws IOException, JsonParseException;

    /** Read the value from the reader, and set it on the corresponding field on target via reflection */
    abstract void readIntoField(JsonReader reader, Object target) throws IOException, IllegalAccessException;
  }

  /**
   * Base class for Adapters produced by this factory.
   *
   * 

The {@link RecordAdapter} is a special case to handle records for JVMs that support it, for * all other types we use the {@link FieldReflectionAdapter}. This class encapsulates the common * logic for serialization and deserialization. During deserialization, we construct an * accumulator A, which we use to accumulate values from the source JSON. After the object has been read in * full, the {@link #finalize(Object)} method is used to convert the accumulator to an instance * of T. * * @param type of objects that this Adapter creates. * @param type of accumulator used to build the deserialization result. */ // This class is public because external projects check for this class with `instanceof` (even though it is internal) public static abstract class Adapter extends TypeAdapter { final Map boundFields; Adapter(Map boundFields) { this.boundFields = boundFields; } @Override public void write(JsonWriter out, T value) throws IOException { if (value == null) { out.nullValue(); return; } out.beginObject(); try { for (BoundField boundField : boundFields.values()) { boundField.write(out, value); } } catch (IllegalAccessException e) { throw ReflectionHelper.createExceptionForUnexpectedIllegalAccess(e); } out.endObject(); } @Override public T read(JsonReader in) throws IOException { if (in.peek() == JsonToken.NULL) { in.nextNull(); return null; } A accumulator = createAccumulator(); try { in.beginObject(); while (in.hasNext()) { String name = in.nextName(); BoundField field = boundFields.get(name); if (field == null || !field.deserialized) { in.skipValue(); } else { readField(accumulator, in, field); } } } catch (IllegalStateException e) { throw new JsonSyntaxException(e); } catch (IllegalAccessException e) { throw ReflectionHelper.createExceptionForUnexpectedIllegalAccess(e); } in.endObject(); return finalize(accumulator); } /** Create the Object that will be used to collect each field value */ abstract A createAccumulator(); /** * Read a single BoundField into the accumulator. The JsonReader will be pointed at the * start of the value for the BoundField to read from. */ abstract void readField(A accumulator, JsonReader in, BoundField field) throws IllegalAccessException, IOException; /** Convert the accumulator to a final instance of T. */ abstract T finalize(A accumulator); } private static final class FieldReflectionAdapter extends Adapter { private final ObjectConstructor constructor; FieldReflectionAdapter(ObjectConstructor constructor, Map boundFields) { super(boundFields); this.constructor = constructor; } @Override T createAccumulator() { return constructor.construct(); } @Override void readField(T accumulator, JsonReader in, BoundField field) throws IllegalAccessException, IOException { field.readIntoField(in, accumulator); } @Override T finalize(T accumulator) { return accumulator; } } private static final class RecordAdapter extends Adapter { static final Map, Object> PRIMITIVE_DEFAULTS = primitiveDefaults(); // The canonical constructor of the record private final Constructor constructor; // Array of arguments to the constructor, initialized with default values for primitives private final Object[] constructorArgsDefaults; // Map from component names to index into the constructors arguments. private final Map componentIndices = new HashMap<>(); RecordAdapter(Class raw, Map boundFields, boolean blockInaccessible) { super(boundFields); constructor = ReflectionHelper.getCanonicalRecordConstructor(raw); if (blockInaccessible) { checkAccessible(null, constructor); } else { // Ensure the constructor is accessible ReflectionHelper.makeAccessible(constructor); } String[] componentNames = ReflectionHelper.getRecordComponentNames(raw); for (int i = 0; i < componentNames.length; i++) { componentIndices.put(componentNames[i], i); } Class[] parameterTypes = constructor.getParameterTypes(); // We need to ensure that we are passing non-null values to primitive fields in the constructor. To do this, // we create an Object[] where all primitives are initialized to non-null values. constructorArgsDefaults = new Object[parameterTypes.length]; for (int i = 0; i < parameterTypes.length; i++) { // This will correctly be null for non-primitive types: constructorArgsDefaults[i] = PRIMITIVE_DEFAULTS.get(parameterTypes[i]); } } private static Map, Object> primitiveDefaults() { Map, Object> zeroes = new HashMap<>(); zeroes.put(byte.class, (byte) 0); zeroes.put(short.class, (short) 0); zeroes.put(int.class, 0); zeroes.put(long.class, 0L); zeroes.put(float.class, 0F); zeroes.put(double.class, 0D); zeroes.put(char.class, '\0'); zeroes.put(boolean.class, false); return zeroes; } @Override Object[] createAccumulator() { return constructorArgsDefaults.clone(); } @Override void readField(Object[] accumulator, JsonReader in, BoundField field) throws IOException { // Obtain the component index from the name of the field backing it Integer componentIndex = componentIndices.get(field.fieldName); if (componentIndex == null) { throw new IllegalStateException( "Could not find the index in the constructor '" + ReflectionHelper.constructorToString(constructor) + "'" + " for field with name '" + field.fieldName + "'," + " unable to determine which argument in the constructor the field corresponds" + " to. This is unexpected behavior, as we expect the RecordComponents to have the" + " same names as the fields in the Java class, and that the order of the" + " RecordComponents is the same as the order of the canonical constructor parameters."); } field.readIntoArray(in, componentIndex, accumulator); } @Override T finalize(Object[] accumulator) { try { return constructor.newInstance(accumulator); } catch (IllegalAccessException e) { throw ReflectionHelper.createExceptionForUnexpectedIllegalAccess(e); } // Note: InstantiationException should be impossible because record class is not abstract; // IllegalArgumentException should not be possible unless a bad adapter returns objects of the wrong type catch (InstantiationException | IllegalArgumentException e) { throw new RuntimeException( "Failed to invoke constructor '" + ReflectionHelper.constructorToString(constructor) + "'" + " with args " + Arrays.toString(accumulator), e); } catch (InvocationTargetException e) { // TODO: JsonParseException ? throw new RuntimeException( "Failed to invoke constructor '" + ReflectionHelper.constructorToString(constructor) + "'" + " with args " + Arrays.toString(accumulator), e.getCause()); } } } }