PolymorphicDeserializer.java

/*
 * The coLAB project
 * Copyright (C) 2021-2023 AlbaSim, MEI, HEIG-VD, HES-SO
 *
 * Licensed under the MIT License
 */
package ch.colabproject.colab.generator.model.tools;

import ch.colabproject.colab.generator.model.interfaces.WithJsonDiscriminator;
import java.lang.reflect.Type;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import javax.json.JsonObject;
import javax.json.bind.serializer.DeserializationContext;
import javax.json.bind.serializer.JsonbDeserializer;
import javax.json.stream.JsonParser;
import org.reflections.Reflections;

/**
 * Custom deserializer which can handle polymorphic objects. By default, this deserializer handles
 * all {@link WithJsonDiscriminator} implementations defined if the
 * <code>ch.colabproject.colab.generator.model</code> package.
 * <p>
 * The {@link #includePackage(java.lang.String) } method can be used to register implementations
 * from other packages.
 * </p>
 * All WithJsonDiscriminator abstract implementation MUST register this deserializer with the
 * {@link javax.json.bind.annotation.JsonbTypeDeserializer  JsonbTypeDeserializer} annotation.
 * <p>
 * Yasson/JSON-b polymorphic deserializer is a "vieux serpent de mer". Some inputs:
 * <ul>
 * <li>https://github.com/eclipse-ee4j/yasson/issues/279
 * <li>https://github.com/eclipse-ee4j/jsonb-api/issues/147
 * <li>https://stackoverflow.com/questions/62398858/deserialize-json-into-polymorphic-pojo-with-json-b-yasson
 * </ul>
 * <p>
 * This implementation does not work if the deserializer is registered with <code>
 * JsonbConfig config = new JsonbConfig().withDeserializers(new PolymorphicDeserializer());
 * </code>. Such a config leads to infinite recusions. See
 * <a href="https://github.com/maxencelaurent/YassonPolymorphicDeserializer/blob/main/src/main/java/com/github/maxencelaurent/yasson/polymorphic/InternalHackDeserializer.java">here</a>
 * for a "global-config" compliant implementation.
 *
 * @author Maxence
 */
public class PolymorphicDeserializer implements JsonbDeserializer<WithJsonDiscriminator> {

    /**
     * Store class references
     */
    private static final Map<String, Class<? extends WithJsonDiscriminator>> CLASSES_MAP
        = new HashMap<>();

    /**
     * Reflections allow to find, for instance, all implementations of an interface
     */
    private static final Reflections REFLECTIONS;

    static {
        // analyse classes in the model package
        REFLECTIONS = new Reflections(
            "ch.colabproject.colab.generator.model"
        );
    }

    /**
     * Include all {@link WithJsonDiscriminator} implementations defined in the given package.
     *
     * @param packageName name of the package
     * @return the number of new types
     */
    public static int includePackage(String packageName) {
        int size = REFLECTIONS.getSubTypesOf(WithJsonDiscriminator.class).size();
        REFLECTIONS.merge(new Reflections(packageName));
        return REFLECTIONS.getSubTypesOf(WithJsonDiscriminator.class).size() -  size;
    }

    /**
     * {@inheritDoc }
     */
    @Override
    public WithJsonDiscriminator deserialize(JsonParser parser, DeserializationContext ctx,
        Type rtType) {
        // find @class discriminant from object to deserialize
        JsonObject value = parser.getObject();
        String atClass = value.getString("@class", null);

        // is this @class already known?
        Class<? extends WithJsonDiscriminator> theClass = CLASSES_MAP.get(atClass);

        if (theClass == null) {
            // nope -> let's resolve it with the help of reflections
            Optional<Class<? extends WithJsonDiscriminator>> concreteClass
                = REFLECTIONS.getSubTypesOf(WithJsonDiscriminator.class)
                    .stream()
                    .filter(cl -> cl.getSimpleName().equals(atClass))
                    .findFirst();
            if (concreteClass.isPresent()) {
                theClass = concreteClass.get();
                CLASSES_MAP.put(atClass, theClass);
            }
        }

        return JsonbProvider.getJsonb().fromJson(value.toString(), theClass);
    }
}