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

de.bild.codec.PojoContext Maven / Gradle / Ivy

Go to download

A very fast POJO codec for MongoDB (used in conjunction with the Mongo Java Driver) that handles generic types as well as polymorphic class hierarchies

The newest version!
package de.bild.codec;


import de.bild.codec.annotations.Discriminator;
import de.bild.codec.annotations.Polymorphic;
import org.apache.commons.lang3.NotImplementedException;
import org.apache.commons.lang3.reflect.TypeUtils;
import org.bson.BsonReader;
import org.bson.BsonType;
import org.bson.BsonWriter;
import org.bson.Document;
import org.bson.codecs.Codec;
import org.bson.codecs.DecoderContext;
import org.bson.codecs.EncoderContext;
import org.bson.codecs.configuration.CodecConfigurationException;
import org.bson.codecs.configuration.CodecRegistry;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;
import java.lang.reflect.WildcardType;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

public class PojoContext {
    private static final Logger LOGGER = LoggerFactory.getLogger(PojoContext.class);

    private final Map> codecMap = new ConcurrentHashMap<>();
    private final TypesModel typesModel;
    private final List codecResolvers;
    private final List typeCodecProviders;
    private final CodecConfiguration codecConfiguration;


    /**
     * The TypeCodecProvider for all known standard types
     */
    //@SuppressWarnings("unchecked")
    private static final TypeCodecProvider DEFAULT_TYPE_CODEC_PROVIDER = new TypeCodecProvider() {
        @Override
        public  Codec get(Type type, TypeCodecRegistry typeCodecRegistry) {
            if (TypeUtils.isArrayType(type)) {
                PrimitiveArrayCodec primitiveArrayCodec = PrimitiveArrayCodec.get(ReflectionHelper.extractRawClass(type));
                if (primitiveArrayCodec != null) {
                    return primitiveArrayCodec;
                } else {
                    return new ArrayCodec<>(type, typeCodecRegistry);
                }
            } else if (type instanceof TypeVariable) {
                throw new IllegalArgumentException("This registry (and probably no other one as well) can not handle generic type variables.");
            } else if (type instanceof WildcardType) {
                LOGGER.error("WildcardTypes are not yet supported. {}", type);
                throw new NotImplementedException("WildcardTypes are not yet supported. " + type);
            } else if (TypeUtils.isAssignable(type, SpecialFieldsMap.class)) {
                return new SpecialFieldsMapCodec(type, typeCodecRegistry);
            }
            // default enum Codec if user did not register any
            else if (TypeUtils.isAssignable(type, Enum.class)) {
                return new EnumCodec(ReflectionHelper.extractRawClass(type));
            }

            return null;
        }
    };


    PojoContext(final TypesModel typesModel,
                List codecResolvers,
                List typeCodecProviders,
                CodecConfiguration codecConfiguration) {
        this.typesModel = typesModel;
        this.codecResolvers = codecResolvers;
        this.typeCodecProviders = typeCodecProviders;
        this.codecConfiguration = codecConfiguration;
    }

    /**
     * First the pojoContext is requested to return a valid codec, if this fails, the mongo codecregistry will be asked
     */
    private static class AnyTypeCodecRegistry implements TypeCodecRegistry {
        final CodecRegistry codecRegistry;
        final PojoContext pojoContext;

        public AnyTypeCodecRegistry(CodecRegistry codecRegistry, PojoContext pojoContext) {
            this.codecRegistry = codecRegistry;
            this.pojoContext = pojoContext;
        }

        @Override
        public  Codec getCodec(Type type) {
            Codec codec;
            //if type is a class, we ask the mongo codecregistry if it provides a specialized codec
            // side note: this PojoContext is chained within this codecRegistry and gets asked as well!!
            if (type instanceof Class) {
                codec = codecRegistry.get(ReflectionHelper.extractRawClass(type));
            } else {
                // if type is any other type than {@link Class}, there is no reason to ask codecRegistry
                // directly as anyway the class would need to be extracted from the type and we would loose type information
                // so in this case we do not ask the codecRegistry for potential more specialized codecs.
                // we ask the pojoContext directly
                codec = pojoContext.getCodec(type, this);
            }
            return codec;
        }

        @Override
        public CodecRegistry getRegistry() {
            return codecRegistry;
        }
    }


    public synchronized  Codec get(Class clazz, CodecRegistry registry) {
        return getCodec(clazz, new AnyTypeCodecRegistry(registry, this));
    }

    /**
     * Calculates and returns a codec for the given type, null otherwise
     *
     * @param type              type for which a codec is requested
     * @param typeCodecRegistry codec registry that can handle any type including parameterizd types, generic arrays, etc
     * @param                the value type
     * @return the codec responsible for the given type or null
     */
    public synchronized  Codec getCodec(Type type, TypeCodecRegistry typeCodecRegistry) {
        Codec codec = codecMap.get(type);
        if (codec != null) {
            return codec;
        }

        // we register within codecMap as LazyCodec for recursion detection
        LazyCodec lazyCodec = new LazyCodec(type, typeCodecRegistry);
        codecMap.put(type, lazyCodec);

        // calculate the codec for given type
        codec = calculateCodec(type, typeCodecRegistry);
        if (codec == null) {
            codecMap.remove(type);
        } else {
            codecMap.put(type, codec);
        }

        return codec;
    }


    /**
     * Iterates over the list of codecResolvers and returns a PolymorphicCodec if match is found.
     * Codecs eligible to encode/decode sub classes of polymorphic structures need to provide special functionality and
     * can be registered during setup of the PojoCodecProvider {@link PojoCodecProvider.Builder#registerCodecResolver(CodecResolver[])}
     *
     * @param type              the value type
     * @param typeCodecRegistry codec registry that can handle any type including parameterizd types, generic arrays, etc
     * @return PolymorphicCodec if responsible resolver is found
     */
    public synchronized  PolymorphicCodec resolve(Type type, TypeCodecRegistry typeCodecRegistry) {
        PolymorphicCodec codec;
        for (CodecResolver codecResolver : codecResolvers) {
            codec = codecResolver.getCodec(type, typeCodecRegistry, codecConfiguration);
            if (codec != null) {
                return codec;
            }
        }

        // if there is a standard codec provided for a type, that needs to be stored along with
        // polymorphic Type information "_t", we need to wrap that codec
        /** Alternatively a user could always provide a {@link CodecResolver} in order to override this default handling
         * Additionally a user can register a {@link PolymorphicCodec} within teh CodecRegistry chain.
         * That codec can be used to encode/decode a type in polymorphic contexts as well as non-polymorphic contexts
         * For an example: {@link AnyThingTest} **/
        // if codecMap contains a mapping for type (may be LazyCodec), this means, that within the chain of registered codecs,
        // there is no codec able to handle the type -> so we need to skip this part and create a reflection based codec
        if (!codecMap.containsKey(type)) {
            Codec standardCodec = typeCodecRegistry.getCodec(type);
            if (standardCodec instanceof PolymorphicCodec) {
                return (PolymorphicCodec) standardCodec; // lovely, user provided a PolymorphicCodec
            } else if (!(standardCodec instanceof TypeCodec)) {
                return new PolymorphicCodecWrapper<>(standardCodec);
            }
        }

        // if user has not registered a default Codec for Enums, use the internal one
        if (TypeUtils.isAssignable(type, Enum.class)) {
            return new PolymorphicCodecWrapper(new EnumCodec(ReflectionHelper.extractRawClass(type)));
        }


        // fallback is BasicReflectionCodec
        return new BasicReflectionCodec(type, typeCodecRegistry, codecConfiguration);
    }

    private static class PolymorphicCodecWrapper implements PolymorphicCodec {
        final Codec codec;

        public PolymorphicCodecWrapper(Codec codec) {
            this.codec = codec;
        }

        @Override
        public T decodeFields(BsonReader reader, DecoderContext decoderContext, T instance) {
            while (reader.readBsonType() != BsonType.END_OF_DOCUMENT) {
                String fieldName = reader.readName();
                if ("data".equals(fieldName)) {
                    return codec.decode(reader, decoderContext);
                } else {
                    reader.skipValue();
                }
            }
            return null;
        }

        @Override
        public void encodeFields(BsonWriter writer, T instance, EncoderContext encoderContext) {
            writer.writeName("data");
            codec.encode(writer, instance, encoderContext);
        }

        @Override
        public T newInstance() {
            return null;
        }

        @Override
        public void verifyFieldsNotNamedLikeAnyDiscriminatorKey(Set discriminatorKeys) throws IllegalArgumentException {
            if (discriminatorKeys.contains("data")) {
                throw new IllegalArgumentException("One of the discriminator keys equals the reserved word 'data' " + discriminatorKeys);
            }
        }

        @Override
        public Class getEncoderClass() {
            return codec.getEncoderClass();
        }

        @Override
        public String toString() {
            final StringBuilder sb = new StringBuilder("PolymorphicCodecWrapper{");
            sb.append("codec=").append(codec);
            sb.append('}');
            return sb.toString();
        }
    }


    /**
     * Will try to find an appropriate codec for the given type.
     *
     * @return Codec, if found or null, in case the type can or should not be handled by the pojo codec
     */
    private  Codec calculateCodec(Type type, TypeCodecRegistry typeCodecRegistry) {

        Codec codec;

        // first treat special types and custom codecs
        for (TypeCodecProvider typeCodecProvider : typeCodecProviders) {
            codec = typeCodecProvider.get(type, typeCodecRegistry);
            if (codec != null) {
                return codec;
            }
        }

        // standard codec available ?
        codec = DEFAULT_TYPE_CODEC_PROVIDER.get(type, typeCodecRegistry);
        if (codec != null) {
            return codec;
        }

        Class rawClass = ReflectionHelper.extractRawClass(type);
        if (rawClass != null && List.class.isAssignableFrom(rawClass)) {
            // List ?
            Type listInterface = ReflectionHelper.findInterface(type, List.class);
            if (listInterface instanceof ParameterizedType && !TypeUtils.containsTypeVariables(listInterface)) {
                ParameterizedType parameterizedType = (ParameterizedType) listInterface;
                try {
                    codec = new ListTypeCodec(rawClass, parameterizedType.getActualTypeArguments()[0], typeCodecRegistry);
                } catch (CodecConfigurationException cce) {
                    // unfortunately there is no elegant way to figure out, if a codec for the valueType can be build within the codecRegistry
                    // if all codecs have been tested an exception will be raised, this then indicates, that the mongo java driver codecs for lists need to be chosen
                    LOGGER.debug("Can't create advanced (generic list) codec for {}. Fall back to mongo db driver defaults.", parameterizedType, cce.getMessage());
                }
            }
        } else if (rawClass != null && Set.class.isAssignableFrom(rawClass)) {
            // Set ?
            Type setInterface = ReflectionHelper.findInterface(type, Set.class);
            if (setInterface instanceof ParameterizedType && !TypeUtils.containsTypeVariables(setInterface)) {
                ParameterizedType parameterizedType = (ParameterizedType) setInterface;
                try {
                    codec = new SetTypeCodec(rawClass, parameterizedType.getActualTypeArguments()[0], typeCodecRegistry);
                } catch (CodecConfigurationException cce) {
                    // unfortunately there is no elegant way to figure out, if a codec for the valueType can be build within the codecRegistry
                    // if all codecs have been tested an exception will be raised, this then indicates, that the mongo java driver codecs for sets need to be chosen
                    LOGGER.debug("Can't create advanced (generic set) codec for {}. Fall back to mongo db driver defaults.", parameterizedType, cce.getMessage());
                }
            }
        } else if (Document.class.isAssignableFrom(rawClass)) {
            // if Document.class let mongo java driver handle this class directly
            codec = null;
        } else if (rawClass != null && Map.class.isAssignableFrom(rawClass)) {
            // Map ?
            Type mapInterface = ReflectionHelper.findInterface(type, Map.class);
            if (mapInterface instanceof ParameterizedType && !TypeUtils.containsTypeVariables(mapInterface)) {
                ParameterizedType parameterizedType = (ParameterizedType) mapInterface;
                Type keyType = parameterizedType.getActualTypeArguments()[0];
                Type valueType = parameterizedType.getActualTypeArguments()[1];
                try {
                    if (keyType.equals(String.class)) {
                        codec = new SimpleMapTypeCodec(rawClass, valueType, typeCodecRegistry);
                    } else {
                        codec = new ComplexMapTypeCodec(rawClass, keyType, valueType, typeCodecRegistry);
                    }
                } catch (CodecConfigurationException cce) {
                    // unfortunately there is no elegant way to figure out, if a codec for the valueType can be build within the codecRegistry
                    // if all codecs have been tested an exception will be raised, this then indicates, that the mongo java driver codecs for maps need to be chosen
                    LOGGER.debug("Can't create advanced (generic map) codec for {}. Fall back to mongo db driver defaults.", parameterizedType, cce.getMessage());
                }
            }
        } else {
            /*
             * now try pojo codec with potentially polymorphic structures
             */
            try {
                Set validTypesForType = typesModel.getAssignableTypesWithinClassHierarchy(type);

                if (validTypesForType == null || validTypesForType.isEmpty()) {
                    LOGGER.debug("Could not find codec for type {}. Maybe another codec or codec provider is able to handle te class?!", type);
                    return null;
                } else if (validTypesForType.size() > 1 || isPolymorphic(type, validTypesForType.iterator().next())) {
                    LOGGER.debug("Creating polymorphic codec for type {} with valid types {}", type, validTypesForType);
                    return new PolymorphicReflectionCodec<>(type, validTypesForType, typeCodecRegistry, this);
                } else {
                    LOGGER.debug("Creating simple reflection based codec for type {} (generic type {}) as only one concrete implementation known.", type, validTypesForType);
                    Type singleType = validTypesForType.iterator().next();
                    codec = resolve(singleType, typeCodecRegistry);
                }
            } catch (Exception e) {
                LOGGER.warn("Could not create codec for type {} reason {}", type, e.getMessage());
                return null;
            }
        }
        return codec;
    }


    /**
     * @param type            to be checked for polymorphic structure
     * @param singleValidType
     * @return true, if type represents a polymorphic type structure
     */
    private boolean isPolymorphic(Type type, Type singleValidType) {
        Class typeClazz = ReflectionHelper.extractRawClass(type);
        Class singleValidClass = ReflectionHelper.extractRawClass(singleValidType);
        return typeClazz.isInterface() || //if type is an interface and there is only one singleValidType -> polymorphic
                singleValidClass.getAnnotation(Polymorphic.class) != null ||    // marked as polymorphic (@inherited annotation)
                singleValidClass.getDeclaredAnnotation(Discriminator.class) != null || // explicit discriminator annotation (non @inherited)
                isClassPartOfPolymorphicStructureWithinTypesModel(typeClazz);
    }

    private boolean isClassPartOfPolymorphicStructureWithinTypesModel(Class clazz) {
        Class superclass = clazz.getSuperclass();
        // first check if superclass is part of typesModel, if so return true
        // Reason: for the current field, since only one valid type exists, non-polymorphic structure could be assumed, but since
        // there might be pojos, that declare its type further up the type hierarchy and want to decode the same data from the database, discriminators are needed
        if (typesModel.getClassHierarchyNodeForType(superclass) != null) {
            return true;
        }
        // now check if any interface is part of the typesModel, if so, return true
        for (Class anInterface : clazz.getInterfaces()) {
            if (typesModel.getClassHierarchyNodeForType(anInterface) != null) {
                return true;
            }
        }
        return false;
    }

    /**
     * Class is used internally to detect cycles.
     */
    private static class LazyCodec implements TypeCodec, DelegatingCodec {
        private final Type type;
        private volatile Codec wrapped;
        private final TypeCodecRegistry typeCodecRegistry;

        LazyCodec(final Type type, TypeCodecRegistry typeCodecRegistry) {
            this.type = type;
            this.typeCodecRegistry = typeCodecRegistry;
        }

        @Override
        public void encode(final BsonWriter writer, final T value, final EncoderContext encoderContext) {
            getWrapped().encode(writer, value, encoderContext);
        }

        @Override
        public Class getEncoderClass() {
            return getWrapped().getEncoderClass();
        }

        @Override
        public T decode(final BsonReader reader, final DecoderContext decoderContext) {
            return getWrapped().decode(reader, decoderContext);
        }

        private Codec getWrapped() {
            if (wrapped == null) {
                wrapped = typeCodecRegistry.getCodec(type);
            }

            return wrapped;
        }

        @Override
        public Codec getDelegate() {
            return getWrapped();
        }
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy