7 votes

gson - Comment inclure la propriété class name lors de la sérialisation d'un objet de n'importe quel type

Je me suis rendu compte que je devais inclure le nom de la classe comme propriété lors de la sérialisation d'un objet dans mon application. Il serait probablement préférable que j'ajoute la propriété du nom de la classe pour tout objet non primitif qui est sérialisé.

J'ai vu qu'il s'agit d'une fonctionnalité intégrée dans Genson avec la fonction useClassMetadata méthode. Mais j'utilise déjà gson dans mon projet, il serait donc bénéfique que je m'en tienne à cette méthode.

Voici ma tentative actuelle :

package com.mycompany.javatest;

import com.google.gson.*;
import java.lang.reflect.*;

public class JavaTest {

    public static class GenericSerializer implements JsonSerializer<Object>, JsonDeserializer<Object> {

        private static final String CLASS_PROPERTY_NAME = "class";

        @Override
        public JsonElement serialize(Object src, Type typeOfSrc,
                                     JsonSerializationContext context) {

            JsonElement retValue = context.serialize(src);
            if (retValue.isJsonObject()) {
                retValue.getAsJsonObject().addProperty(CLASS_PROPERTY_NAME, src.getClass().getName());
            }
            return retValue;
        }

        @Override
        public Object deserialize(JsonElement json, Type typeOfT,
                                  JsonDeserializationContext context) throws JsonParseException {

            Class actualClass;
            if (json.isJsonObject()) {
                JsonObject jsonObject = json.getAsJsonObject();
                String className = jsonObject.get(CLASS_PROPERTY_NAME).getAsString();

                try {
                    actualClass = Class.forName(className);
                }
                catch (ClassNotFoundException e) {
                    e.printStackTrace();
                    throw new JsonParseException(e.getMessage());
                }
            }
            else {
                actualClass = typeOfT.getClass();
            }
            return context.deserialize(json, actualClass);
        }
    }

    public static class MyClass {

        private final String name = "SpongePants SquareBob";

    }

    public static void main(String[] args) {

        MyClass obj = new MyClass();

        GsonBuilder gb = new GsonBuilder();
        gb.registerTypeAdapter(Object.class, new GenericSerializer());
        Gson gson = gb.create();

        System.out.println(gson.toJson(obj, Object.class));

    }
}

Impressions

{"name":"SpongePants SquareBob"}

Je veux qu'il s'imprime

{"name":"SpongePants SquareBob","class":"com.mycompany.javatest$MyClass"}

EDITAR: Une autre tentative (cette fois-ci en utilisant GsonFire)

package com.mycompany.javatest;

import com.google.gson.*;
import io.gsonfire.*;

public class JavaTest {

    public static class DummyData {

        private final String someData = "1337";

    }

    private static final String CLASS_PROPERTY_NAME = "class";

    public static void main(String[] args) {

        GsonFireBuilder gfb = new GsonFireBuilder();
        gfb.registerPostProcessor(Object.class, new PostProcessor<Object>() {

                              @Override
                              public void postDeserialize(Object t, JsonElement je, Gson gson) {
                                  // Ignore
                              }

                              @Override
                              public void postSerialize(JsonElement je, Object t, Gson gson) {
                                  if (je.isJsonObject()) {
                                      je.getAsJsonObject().add(CLASS_PROPERTY_NAME, new JsonPrimitive(t.getClass().getTypeName()));
                                  }
                              }

                          });

        gfb.registerTypeSelector(Object.class, (JsonElement je) -> {
            System.out.println(je);
                             if (je.isJsonObject()) {
                                 try {
                                     return Class.forName(je.getAsJsonObject().get(CLASS_PROPERTY_NAME).getAsString());
                                 }
                                 catch (ClassNotFoundException ex) {
                                     ex.printStackTrace();
                                 }
                             }

                             return null;
                         });

        Gson gson = gfb.createGson();

        DummyData dd = new DummyData();
        String json = gson.toJson(dd);
        System.out.println(json);

        DummyData dd2 = (DummyData) gson.fromJson(json, Object.class); // <-- gives me a ClassCastException

    }

}

4voto

pandaadb Points 4391

Autre réponse. Cela a pris un peu plus de temps.

Commentaire en marge : La solution ci-dessus fonctionnerait si vous utilisiez récursivement la réflexion pour travailler sur les champs de votre classe. Ensuite, vous sérialisez ces champs avec le sérialiseur spécial, tout en utilisant un sérialiseur séparé pour l'objet parent. Cela éviterait le débordement de la pile.

Cela dit, je suis un développeur paresseux, et j'aime donc faire les choses paresseusement. Je suis en train d'adapter une solution de Google pour vous.

NOTE : VEUILLEZ TESTER CECI ET L'ADAPTER À VOS BESOINS. IL S'AGIT D'UN PROTOTYPE ET JE N'AI PAS NETTOYÉ LE CODE INUTILE NI VÉRIFIÉ LES PROBLÈMES ÉVENTUELS.

La source originale du code :

https://github.com/google/gson/blob/master/extras/src/main/java/com/google/gson/typeadapters/RuntimeTypeAdapterFactory.java

Il s'agit donc d'un projet basé sur la RuntimeTypeAdapterFactory . Cette fabrique est fournie par Google et son but est de supporter la désérialisation hiérarchique. Pour ce faire, vous devez enregistrer une classe de base et TOUTES les sous-classes, avec une propriété que vous souhaitez ajouter comme identifiant. Si vous lisez les javadocs, ce sera beaucoup plus clair.

Cela nous offre évidemment ce que nous voulons : enregistrer récursivement différents adaptateurs pour les types de classes qui peuvent les gérer, tout en ne tournant PAS en rond et en ne provoquant pas de débordement de pile. Avec un problème important : vous devez enregistrer TOUS sous-classes. Ceci n'est évidemment pas approprié (bien que l'on puisse argumenter que vous pourriez résoudre le chemin de classe et simplement ajouter toutes vos classes au démarrage pour être capable d'utiliser ceci partout). J'ai donc regardé dans la source et j'ai modifié le code pour le faire dynamiquement. Notez que google vous déconseille de le faire - utilisez-le selon vos propres conditions :)

Voici mon usine :

import java.io.IOException;
import java.util.LinkedHashMap;
import java.util.Map;

import com.google.gson.Gson;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParseException;
import com.google.gson.JsonPrimitive;
import com.google.gson.TypeAdapter;
import com.google.gson.TypeAdapterFactory;
import com.google.gson.internal.Streams;
import com.google.gson.reflect.TypeToken;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonWriter;

/**
 * Adapts values whose runtime type may differ from their declaration type. This
 * is necessary when a field's type is not the same type that GSON should create
 * when deserializing that field. For example, consider these types:
 * <pre>   {@code
 *   abstract class Shape {
 *     int x;
 *     int y;
 *   }
 *   class Circle extends Shape {
 *     int radius;
 *   }
 *   class Rectangle extends Shape {
 *     int width;
 *     int height;
 *   }
 *   class Diamond extends Shape {
 *     int width;
 *     int height;
 *   }
 *   class Drawing {
 *     Shape bottomShape;
 *     Shape topShape;
 *   }
 * }</pre>
 * <p>Without additional type information, the serialized JSON is ambiguous. Is
 * the bottom shape in this drawing a rectangle or a diamond? <pre>   {@code
 *   {
 *     "bottomShape": {
 *       "width": 10,
 *       "height": 5,
 *       "x": 0,
 *       "y": 0
 *     },
 *     "topShape": {
 *       "radius": 2,
 *       "x": 4,
 *       "y": 1
 *     }
 *   }}</pre>
 * This class addresses this problem by adding type information to the
 * serialized JSON and honoring that type information when the JSON is
 * deserialized: <pre>   {@code
 *   {
 *     "bottomShape": {
 *       "type": "Diamond",
 *       "width": 10,
 *       "height": 5,
 *       "x": 0,
 *       "y": 0
 *     },
 *     "topShape": {
 *       "type": "Circle",
 *       "radius": 2,
 *       "x": 4,
 *       "y": 1
 *     }
 *   }}</pre>
 * Both the type field name ({@code "type"}) and the type labels ({@code
 * "Rectangle"}) are configurable.
 *
 * <h3>Registering Types</h3>
 * Create a {@code RuntimeTypeAdapterFactory} by passing the base type and type field
 * name to the {@link #of} factory method. If you don't supply an explicit type
 * field name, {@code "type"} will be used. <pre>   {@code
 *   RuntimeTypeAdapterFactory<Shape> shapeAdapterFactory
 *       = RuntimeTypeAdapterFactory.of(Shape.class, "type");
 * }</pre>
 * Next register all of your subtypes. Every subtype must be explicitly
 * registered. This protects your application from injection attacks. If you
 * don't supply an explicit type label, the type's simple name will be used.
 * <pre>   {@code
 *   shapeAdapter.registerSubtype(Rectangle.class, "Rectangle");
 *   shapeAdapter.registerSubtype(Circle.class, "Circle");
 *   shapeAdapter.registerSubtype(Diamond.class, "Diamond");
 * }</pre>
 * Finally, register the type adapter factory in your application's GSON builder:
 * <pre>   {@code
 *   Gson gson = new GsonBuilder()
 *       .registerTypeAdapterFactory(shapeAdapterFactory)
 *       .create();
 * }</pre>
 * Like {@code GsonBuilder}, this API supports chaining: <pre>   {@code
 *   RuntimeTypeAdapterFactory<Shape> shapeAdapterFactory = RuntimeTypeAdapterFactory.of(Shape.class)
 *       .registerSubtype(Rectangle.class)
 *       .registerSubtype(Circle.class)
 *       .registerSubtype(Diamond.class);
 * }</pre>
 */
public final class RuntimeClassNameTypeAdapterFactory<T> implements TypeAdapterFactory {
  private final Class<?> baseType;
  private final String typeFieldName;
  private final Map<String, Class<?>> labelToSubtype = new LinkedHashMap<String, Class<?>>();
  private final Map<Class<?>, String> subtypeToLabel = new LinkedHashMap<Class<?>, String>();

  private RuntimeClassNameTypeAdapterFactory(Class<?> baseType, String typeFieldName) {
    if (typeFieldName == null || baseType == null) {
      throw new NullPointerException();
    }
    this.baseType = baseType;
    this.typeFieldName = typeFieldName;
  }

  /**
   * Creates a new runtime type adapter using for {@code baseType} using {@code
   * typeFieldName} as the type field name. Type field names are case sensitive.
   */
  public static <T> RuntimeClassNameTypeAdapterFactory<T> of(Class<T> baseType, String typeFieldName) {
    return new RuntimeClassNameTypeAdapterFactory<T>(baseType, typeFieldName);
  }

  /**
   * Creates a new runtime type adapter for {@code baseType} using {@code "type"} as
   * the type field name.
   */
  public static <T> RuntimeClassNameTypeAdapterFactory<T> of(Class<T> baseType) {
    return new RuntimeClassNameTypeAdapterFactory<T>(baseType, "class");
  }

  /**
   * Registers {@code type} identified by {@code label}. Labels are case
   * sensitive.
   *
   * @throws IllegalArgumentException if either {@code type} or {@code label}
   *     have already been registered on this type adapter.
   */
  public RuntimeClassNameTypeAdapterFactory<T> registerSubtype(Class<? extends T> type, String label) {
    if (type == null || label == null) {
      throw new NullPointerException();
    }
    if (subtypeToLabel.containsKey(type) || labelToSubtype.containsKey(label)) {
      throw new IllegalArgumentException("types and labels must be unique");
    }
    labelToSubtype.put(label, type);
    subtypeToLabel.put(type, label);
    return this;
  }

  /**
   * Registers {@code type} identified by its {@link Class#getSimpleName simple
   * name}. Labels are case sensitive.
   *
   * @throws IllegalArgumentException if either {@code type} or its simple name
   *     have already been registered on this type adapter.
   */
  public RuntimeClassNameTypeAdapterFactory<T> registerSubtype(Class<? extends T> type) {
    return registerSubtype(type, type.getSimpleName());
  }

  public <R> TypeAdapter<R> create(Gson gson, TypeToken<R> type) {

    final Map<String, TypeAdapter<?>> labelToDelegate
        = new LinkedHashMap<String, TypeAdapter<?>>();
    final Map<Class<?>, TypeAdapter<?>> subtypeToDelegate
        = new LinkedHashMap<Class<?>, TypeAdapter<?>>();

//    && !String.class.isAssignableFrom(type.getRawType())

    if(Object.class.isAssignableFrom(type.getRawType()) ) {
        TypeAdapter<?> delegate = gson.getDelegateAdapter(this, type);
        labelToDelegate.put("class", delegate);
        subtypeToDelegate.put(type.getRawType(), delegate);
    }

//    for (Map.Entry<String, Class<?>> entry : labelToSubtype.entrySet()) {
//      TypeAdapter<?> delegate = gson.getDelegateAdapter(this, TypeToken.get(entry.getValue()));
//      labelToDelegate.put(entry.getKey(), delegate);
//      subtypeToDelegate.put(entry.getValue(), delegate);
//    }

    return new TypeAdapter<R>() {
      @Override public R read(JsonReader in) throws IOException {
        JsonElement jsonElement = Streams.parse(in);
        JsonElement labelJsonElement = jsonElement.getAsJsonObject().remove(typeFieldName);
        if (labelJsonElement == null) {
          throw new JsonParseException("cannot deserialize " + baseType
              + " because it does not define a field named " + typeFieldName);
        }
        String label = labelJsonElement.getAsString();
        @SuppressWarnings("unchecked") // registration requires that subtype extends T
        TypeAdapter<R> delegate = (TypeAdapter<R>) labelToDelegate.get(label);
        if (delegate == null) {
          throw new JsonParseException("cannot deserialize " + baseType + " subtype named "
              + label + "; did you forget to register a subtype?");
        }
        return delegate.fromJsonTree(jsonElement);
      }

      @Override public void write(JsonWriter out, R value) throws IOException {
        Class<?> srcType = value.getClass();
        String label = srcType.getName();
        @SuppressWarnings("unchecked") // registration requires that subtype extends T
        TypeAdapter<R> delegate = (TypeAdapter<R>) subtypeToDelegate.get(srcType);
        if (delegate == null) {
          throw new JsonParseException("cannot serialize " + srcType.getName()
              + "; did you forget to register a subtype?");
        }
        JsonElement jsonTree = delegate.toJsonTree(value);
        if(jsonTree.isJsonPrimitive()) {
            Streams.write(jsonTree, out);
        } else {
            JsonObject jsonObject = jsonTree.getAsJsonObject();
            if (jsonObject.has(typeFieldName)) {
              throw new JsonParseException("cannot serialize " + srcType.getName()
                  + " because it already defines a field named " + typeFieldName);
            }
            JsonObject clone = new JsonObject();
            clone.add(typeFieldName, new JsonPrimitive(label));
            for (Map.Entry<String, JsonElement> e : jsonObject.entrySet()) {
              clone.add(e.getKey(), e.getValue());
            }
            Streams.write(clone, out);
        }
      }
    }.nullSafe();
  }
}

J'ai ajouté toutes les importations pour vous. Ceci n'est pas (vraiment) publié dans maven central, bien que vous puissiez le trouver ici : https://mvnrepository.com/artifact/org.danilopianini/gson-extras/0.1.0

Quoi qu'il en soit, vous devrez faire des adaptations pour que cela fonctionne pour vous, alors j'ai fait une copie. La copie compile complètement et vous pouvez simplement la coller dans votre code et vous épargner la dépendance supplémentaire.

Les éléments importants de ce code sont les suivants : (et je les ai volontairement laissés dans le code mais commentés pour que vous puissiez vous en rendre compte)

en create(Gson gson, TypeToken<R> type)

Vérifier si le type brut est assignable à partir de la classe String. Vous voulez que cette vérification soit appliquée à chaque objet de la classe, c'est donc ce qui est fait. Notez que le code précédent vérifie si le type est enregistré dans la classe - ce n'est plus nécessaire (en conséquence, les variables ne seraient pas nécessaires ; vous devriez nettoyer le code).

en @Override public void write(JsonWriter out, R value) throws IOException { :

Tout d'abord, nous nous débarrassons de l'étiquette. Notre étiquette est et sera toujours le nom du type de source. C'est ce qui est fait dans :

String label = srcType.getName();

Deuxièmement, nous devons faire une distinction entre les types primitifs et les types objets. Les types primitifs sont les chaînes de caractères, les nombres entiers, etc. dans le monde Gson. Cela signifie que notre vérification ci-dessus (ajouter un adaptateur) ne tient pas compte du fait que ces types objets sont en fait des types primitifs. C'est ce que nous faisons :

if(jsonTree.isJsonPrimitive()) {
            Streams.write(jsonTree, out);

Ceci permet d'y remédier. S'il est primitif, il suffit d'écrire l'arbre dans le flux. Si ce n'est pas le cas, nous écrivons alors tous les autres champs ET le champ de la classe.

JsonObject clone = new JsonObject();
            clone.add(typeFieldName, new JsonPrimitive(label));
            for (Map.Entry<String, JsonElement> e : jsonObject.entrySet()) {
              clone.add(e.getKey(), e.getValue());
            }
            Streams.write(clone, out);

Fewww - enfin, cela permet de régler ce problème. Et voici l'exemple qui prouve que mon code fait ce que (je crois) vous voulez qu'il fasse ;)

public class GsonClassNameTest {
    static Gson create = new GsonBuilder().registerTypeAdapterFactory(RuntimeClassNameTypeAdapterFactory.of(Object.class)).create();
    public static void main(String[] args) {
        String json = create.toJson(new X());
        System.out.println(json);
    }
    public static class X {
        public String test = "asd";
        public int xyz = 23;
        public Y y_class = new Y();
    }
    public static class Y {
        String yTest = "asd2";

        Z zTest = new Z();
    }
    public static class Z {
        long longVal = 25;
        double doubleTest = 2.4;
    }
}

Cela produit maintenant ce json pour vous :

{  
   "class":"google.GsonClassNameTest$X",
   "test":"asd",
   "xyz":23,
   "y_class":{  
      "class":"google.GsonClassNameTest$Y",
      "yTest":"asd2",
      "zTest":{  
         "class":"google.GsonClassNameTest$Z",
         "longVal":25,
         "doubleTest":2.4
      }
   }
}

Comme vous pouvez le voir, les chaînes, les longueurs et les entiers sont correctement créés. Chaque objet de classe a reçu son nom de classe de manière récurrente.

Il s'agit d'une approche générique qui devrait fonctionner avec tout ce que vous créez. Cependant, si vous décidez d'adopter cette approche, faites-moi une faveur et écrivez quelques tests unitaires ;) Comme je l'ai mentionné précédemment, j'ai prototypé cette implémentation.

J'espère que cela me permettra d'obtenir une note :)

Voir aussi,

Artur

3voto

gromit190 Points 1132

J'ai accepté la réponse de @padaadb mais je voulais juste coller le code que j'utilise. Il s'occupe de la sérialisation avec le type et de la dé-sérialisation dans le sous-type approprié :

package com.mycompany.javatest;

import com.google.gson.*;
import java.lang.reflect.*;
import org.junit.*;

public class JavaTest {

    public static class GenericSerializer implements JsonSerializer<Object>, JsonDeserializer<Object> {

        private static final String CLASS_PROPERTY_NAME = "class";
        private final Gson gson;

        public GenericSerializer() {
            gson = new Gson();
        }

        public GenericSerializer(Gson gson) {
            this.gson = gson;
        }

        @Override
        public Object deserialize(JsonElement json, Type typeOfT,
                                  JsonDeserializationContext context) throws JsonParseException {

            Class actualClass;
            if (json.isJsonObject()) {
                JsonObject jsonObject = json.getAsJsonObject();
                String className = jsonObject.get(CLASS_PROPERTY_NAME).getAsString();
                try {
                    actualClass = Class.forName(className);
                }
                catch (ClassNotFoundException e) {
                    e.printStackTrace();
                    throw new JsonParseException(e.getMessage());
                }
            }
            else {
                actualClass = typeOfT.getClass();
            }

            return gson.fromJson(json, actualClass);
        }

        @Override
        public JsonElement serialize(Object src, Type typeOfSrc,
                                     JsonSerializationContext context) {
            JsonElement retValue = gson.toJsonTree(src);
            if (retValue.isJsonObject()) {
                retValue.getAsJsonObject().addProperty(CLASS_PROPERTY_NAME, src.getClass().getName());
            }
            return retValue;
        }

    }

    public static void main(String[] args) {

        GsonBuilder builder = new GsonBuilder();
        builder.registerTypeHierarchyAdapter(Object.class, new GenericSerializer());
        Gson gson = builder.create();

        SomeSuperClass x = new SomeSubClass();
        String json = gson.toJson(x);

        SomeSuperClass y = gson.fromJson(json, SomeSuperClass.class); // Usually, y would now be of type SomeSuperClass
        Assert.assertEquals(x.getClass(), y.getClass()); // y is actually of type SomeSubClass (!)

        System.out.println("y.getClass()= " + y.getClass());
    }

    public static class SomeSuperClass {
    }

    public static class SomeSubClass extends SomeSuperClass {

        private final String someMember = "12345";
    }
}

2voto

pandaadb Points 4391

Je viens d'essayer moi-même et cela semble fonctionner :

public class GsonClassNameTest {

    public static void main(String[] args) {

        Gson create = new GsonBuilder().registerTypeHierarchyAdapter(Object.class, new ODeserialiser()).create();
        String json = create.toJson(new X());
        System.out.println(json);

    }

    public static class ODeserialiser implements JsonSerializer<Object> {

        @Override
        public JsonElement serialize(Object src, Type typeOfSrc, JsonSerializationContext context) {
            Gson gson = new Gson();
            JsonElement serialize = gson.toJsonTree(src);
            JsonObject o = (JsonObject) serialize;
            o.addProperty("class", src.getClass().getName());
            return serialize;
        }
    }

    public static class X {
        public String test = "asd";
    }
}

Cette empreinte :

{"test":"asd","class":"google.GsonClassNameTest$X"}

détails :

Vous devez enregistrer un adaptateur de hiérarchie, de sorte que si vous l'enregistrez avec la classe Object, il sera appelé pour n'importe quel type que vous lui passez.

Vous devez également utiliser une instance Gson différente dans le sérialiseur personnalisé, sinon vous continuez à tourner en rond et vous obtenez un Stackoverflow.

A part cela, c'est assez simple :)

Note : J'ai peu d'expérience avec gson, il y a peut-être une solution plus simple.

Voir aussi,

Artur

0voto

Alix Points 281

pandaadb ne fonctionnait pas complètement pour moi car elle ne gère pas les tableaux/listes et il y avait un problème avec la désérialisation, j'ai donc fait quelques changements :

package org.ctbto.osi.fieldapp.util.gson;

import com.google.gson.Gson;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParseException;
import com.google.gson.JsonPrimitive;
import com.google.gson.TypeAdapter;
import com.google.gson.TypeAdapterFactory;
import com.google.gson.internal.Streams;
import com.google.gson.reflect.TypeToken;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonWriter;

import java.io.IOException;
import java.util.LinkedHashMap;
import java.util.Map;

/**
 * <p>
*  Disclaimer: taken from here https://stackoverflow.com/a/40133286/285091 with some modifications
 * </p>
 *
 * Adapts values whose runtime type may differ from their declaration type. This
 * is necessary when a field's type is not the same type that GSON should create
 * when deserializing that field. For example, consider these types:
 * <pre>   {@code
 *   abstract class Shape {
 *     int x;
 *     int y;
 *   }
 *   class Circle extends Shape {
 *     int radius;
 *   }
 *   class Rectangle extends Shape {
 *     int width;
 *     int height;
 *   }
 *   class Diamond extends Shape {
 *     int width;
 *     int height;
 *   }
 *   class Drawing {
 *     Shape bottomShape;
 *     Shape topShape;
 *   }
 * }</pre>
 * <p>Without additional type information, the serialized JSON is ambiguous. Is
 * the bottom shape in this drawing a rectangle or a diamond? <pre>   {@code
 *   {
 *     "bottomShape": {
 *       "width": 10,
 *       "height": 5,
 *       "x": 0,
 *       "y": 0
 *     },
 *     "topShape": {
 *       "radius": 2,
 *       "x": 4,
 *       "y": 1
 *     }
 *   }}</pre>
 * This class addresses this problem by adding type information to the
 * serialized JSON and honoring that type information when the JSON is
 * deserialized: <pre>   {@code
 *   {
 *     "bottomShape": {
 *       "type": "Diamond",
 *       "width": 10,
 *       "height": 5,
 *       "x": 0,
 *       "y": 0
 *     },
 *     "topShape": {
 *       "type": "Circle",
 *       "radius": 2,
 *       "x": 4,
 *       "y": 1
 *     }
 *   }}</pre>
 * Both the type field name ({@code "type"}) and the type labels ({@code
 * "Rectangle"}) are configurable.
 * <p>
 * <h3>Registering Types</h3>
 * Create a {@code RuntimeTypeAdapterFactory} by passing the base type and type field
 * name to the {@link #of} factory method. If you don't supply an explicit type
 * field name, {@code "type"} will be used. <pre>   {@code
 *   RuntimeTypeAdapterFactory<Shape> shapeAdapterFactory
 *       = RuntimeTypeAdapterFactory.of(Shape.class, "type");
 * }</pre>
 * Next register all of your subtypes. Every subtype must be explicitly
 * registered. This protects your application from injection attacks. If you
 * don't supply an explicit type label, the type's simple name will be used.
 * <pre>   {@code
 *   shapeAdapter.registerSubtype(Rectangle.class, "Rectangle");
 *   shapeAdapter.registerSubtype(Circle.class, "Circle");
 *   shapeAdapter.registerSubtype(Diamond.class, "Diamond");
 * }</pre>
 * Finally, register the type adapter factory in your application's GSON builder:
 * <pre>   {@code
 *   Gson gson = new GsonBuilder()
 *       .registerTypeAdapterFactory(shapeAdapterFactory)
 *       .create();
 * }</pre>
 * Like {@code GsonBuilder}, this API supports chaining: <pre>   {@code
 *   RuntimeTypeAdapterFactory<Shape> shapeAdapterFactory = RuntimeTypeAdapterFactory.of(Shape.class)
 *       .registerSubtype(Rectangle.class)
 *       .registerSubtype(Circle.class)
 *       .registerSubtype(Diamond.class);
 * }</pre>
 */
public final class RuntimeClassNameTypeAdapterFactory<T> implements TypeAdapterFactory {
    private final Class<?> baseType;
    private final String typeFieldName;
    private final Map<String, Class<?>> labelToSubtype = new LinkedHashMap<String, Class<?>>();
    private final Map<Class<?>, String> subtypeToLabel = new LinkedHashMap<Class<?>, String>();

    private RuntimeClassNameTypeAdapterFactory(Class<?> baseType, String typeFieldName) {
        if (typeFieldName == null || baseType == null) {
            throw new NullPointerException();
        }
        this.baseType = baseType;
        this.typeFieldName = typeFieldName;
    }

    /**
     * Creates a new runtime type adapter using for {@code baseType} using {@code
     * typeFieldName} as the type field name. Type field names are case sensitive.
     */
    public static <T> RuntimeClassNameTypeAdapterFactory<T> of(Class<T> baseType, String typeFieldName) {
        return new RuntimeClassNameTypeAdapterFactory<T>(baseType, typeFieldName);
    }

    /**
     * Creates a new runtime type adapter for {@code baseType} using {@code "type"} as
     * the type field name.
     */
    public static <T> RuntimeClassNameTypeAdapterFactory<T> of(Class<T> baseType) {
        return new RuntimeClassNameTypeAdapterFactory<T>(baseType, "class");
    }

    /**
     * Registers {@code type} identified by {@code label}. Labels are case
     * sensitive.
     *
     * @throws IllegalArgumentException if either {@code type} or {@code label}
     *                                  have already been registered on this type adapter.
     */
    public RuntimeClassNameTypeAdapterFactory<T> registerSubtype(Class<? extends T> type, String label) {
        if (type == null || label == null) {
            throw new NullPointerException();
        }
        if (subtypeToLabel.containsKey(type) || labelToSubtype.containsKey(label)) {
            throw new IllegalArgumentException("types and labels must be unique");
        }
        labelToSubtype.put(label, type);
        subtypeToLabel.put(type, label);
        return this;
    }

    /**
     * Registers {@code type} identified by its {@link Class#getSimpleName simple
     * name}. Labels are case sensitive.
     *
     * @throws IllegalArgumentException if either {@code type} or its simple name
     *                                  have already been registered on this type adapter.
     */
    public RuntimeClassNameTypeAdapterFactory<T> registerSubtype(Class<? extends T> type) {
        return registerSubtype(type, type.getSimpleName());
    }

    public <R> TypeAdapter<R> create(Gson gson, TypeToken<R> type) {

        final Map<String, TypeAdapter<?>> labelToDelegate
                = new LinkedHashMap<String, TypeAdapter<?>>();
        final Map<Class<?>, TypeAdapter<?>> subtypeToDelegate
                = new LinkedHashMap<Class<?>, TypeAdapter<?>>();

//    && !String.class.isAssignableFrom(type.getRawType())

        if (Object.class.isAssignableFrom(type.getRawType())) {
            TypeAdapter<?> delegate = gson.getDelegateAdapter(this, type);
            labelToDelegate.put(type.getRawType().getName(), delegate);
            subtypeToDelegate.put(type.getRawType(), delegate);
        }

//    for (Map.Entry<String, Class<?>> entry : labelToSubtype.entrySet()) {
//      TypeAdapter<?> delegate = gson.getDelegateAdapter(this, TypeToken.get(entry.getValue()));
//      labelToDelegate.put(entry.getKey(), delegate);
//      subtypeToDelegate.put(entry.getValue(), delegate);
//    }

        return new TypeAdapter<R>() {
            @SuppressWarnings("unchecked")
            @Override
            public R read(JsonReader in) throws IOException {
                JsonElement jsonElement = Streams.parse(in);
                if (jsonElement.isJsonObject()) {
                    JsonElement labelJsonElement = jsonElement.getAsJsonObject().remove(typeFieldName);
                    if (labelJsonElement == null) {
                        throw new JsonParseException("cannot deserialize " + baseType
                                + " because it does not define a field named " + typeFieldName);
                    }
                    String label = labelJsonElement.getAsString();
                    TypeAdapter<R> delegate = (TypeAdapter<R>) labelToDelegate.get(label);
                    if (delegate == null) {
                        Class<R> aClass;
                        try {
                            aClass = (Class<R>) Class.forName(label);
                        } catch (ClassNotFoundException e) {
                            throw new JsonParseException("Cannot find class " + label, e);
                        }

                        TypeToken<R> subClass = TypeToken.get(aClass);
                        delegate = gson.getDelegateAdapter(RuntimeClassNameTypeAdapterFactory.this, subClass);
                        if (delegate == null) {
                            throw new JsonParseException("cannot deserialize " + baseType + " subtype named "
                                    + label + "; did you forget to register a subtype?");
                        }
                    }
                    return delegate.fromJsonTree(jsonElement);
                } else if (jsonElement.isJsonNull()) {
                    return null;
                } else {
                    TypeAdapter<R> delegate = gson.getDelegateAdapter(RuntimeClassNameTypeAdapterFactory.this, type);
                    if (delegate == null) {
                        throw new JsonParseException("cannot deserialize " + baseType + "; did you forget to register a subtype?");
                    }
                    return delegate.fromJsonTree(jsonElement);
                }
            }

            @Override
            public void write(JsonWriter out, R value) throws IOException {
                Class<?> srcType = value.getClass();
                String label = srcType.getName();
                TypeAdapter<R> delegate = getDelegate(srcType);
                if (delegate == null) {
                    throw new JsonParseException("cannot serialize " + srcType.getName()
                            + "; did you forget to register a subtype?");
                }
                JsonElement jsonTree = delegate.toJsonTree(value);
                if (!jsonTree.isJsonObject()) {
                    Streams.write(jsonTree, out);
                } else {
                    JsonObject jsonObject = jsonTree.getAsJsonObject();
                    if (jsonObject.has(typeFieldName)) {
                        throw new JsonParseException("cannot serialize " + srcType.getName()
                                + " because it already defines a field named " + typeFieldName);
                    }
                    JsonObject clone = new JsonObject();
                    clone.add(typeFieldName, new JsonPrimitive(label));
                    for (Map.Entry<String, JsonElement> e : jsonObject.entrySet()) {
                        clone.add(e.getKey(), e.getValue());
                    }
                    Streams.write(clone, out);
                }
            }

            @SuppressWarnings("unchecked")
            private TypeAdapter<R> getDelegate(Class<?> srcType) {
                TypeAdapter<?> typeAdapter = subtypeToDelegate.get(srcType);
                if (typeAdapter != null) {
                    return (TypeAdapter<R>) typeAdapter;
                }

                for (Map.Entry<Class<?>, TypeAdapter<?>> classTypeAdapterEntry : subtypeToDelegate.entrySet()) {
                    if (classTypeAdapterEntry.getKey().isAssignableFrom(srcType)) {
                        return (TypeAdapter<R>) classTypeAdapterEntry.getValue();
                    }
                }
                return null;
            }
        }.nullSafe();
    }
}

Tout le mérite lui revient cependant. Comme il le dit, veuillez tester ce code avant de l'utiliser !

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X