de.bild.codec.PolymorphicReflectionCodec Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of polymorphia Show documentation
Show all versions of polymorphia Show documentation
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
package de.bild.codec;
import com.mongodb.client.model.Filters;
import de.bild.codec.annotations.Discriminator;
import de.bild.codec.annotations.DiscriminatorFallback;
import de.bild.codec.annotations.DiscriminatorKey;
import org.bson.*;
import org.bson.codecs.DecoderContext;
import org.bson.codecs.EncoderContext;
import org.bson.conversions.Bson;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.lang.reflect.Modifier;
import java.lang.reflect.Type;
import java.util.*;
public class PolymorphicReflectionCodec implements TypeCodec {
private static final Logger LOGGER = LoggerFactory.getLogger(PolymorphicReflectionCodec.class);
final Class clazz;
final Map> discriminatorToCodec = new HashMap<>();
final Map, PolymorphicCodec> classToCodec = new HashMap<>();
final Map, String> mainDiscriminators = new HashMap<>();
final Map, String> discriminatorKeys = new HashMap<>();
final Set allDiscriminatorKeys = new HashSet<>();
final Bson typeFilter;
PolymorphicCodec fallBackCodec;
final boolean isCollectible;
public PolymorphicReflectionCodec(Type type, Set validTypes, TypeCodecRegistry typeCodecRegistry, PojoContext pojoContext) {
this.clazz = AbstractTypeCodec.extractClass(type);
boolean isAnyCodecCollectible = false;
List allDiscriminatorKeyValueFilters = new ArrayList<>();
for (Type validType : validTypes) {
Class clazz = AbstractTypeCodec.extractClass(validType);
// ignore interfaces and also ignore abstract classes
if (!clazz.isInterface() && ! Modifier.isAbstract(clazz.getModifiers())) {
String discriminatorKey = getDiscriminatorKeyForClass(clazz);
boolean isFallBack = clazz.getDeclaredAnnotation(DiscriminatorFallback.class) != null;
discriminatorKeys.putIfAbsent(clazz, discriminatorKey);
allDiscriminatorKeys.add(discriminatorKey);
PolymorphicCodec codecFor = pojoContext.resolve(validType, typeCodecRegistry);
if (isFallBack) {
if (fallBackCodec != null) {
LOGGER.error("It is not allowed to declare more han one class within hierarchy as fallback. {} found already {}", clazz, codecFor.getEncoderClass());
throw new IllegalArgumentException("It is not allowed to declare more han one class within hierarchy as fallback." + clazz);
} else {
fallBackCodec = codecFor;
LOGGER.debug("Found fallback discriminator at class {}", clazz);
}
}
isAnyCodecCollectible |= codecFor.isCollectible();
classToCodec.put(clazz, codecFor);
Discriminator discriminatorAnnotation = clazz.getDeclaredAnnotation(Discriminator.class);
String mainDiscriminator = clazz.getSimpleName();
List allDiscriminators = new ArrayList<>();
if (discriminatorAnnotation != null) {
if (discriminatorAnnotation.value() != null) {
mainDiscriminator = discriminatorAnnotation.value();
}
allDiscriminators.add(mainDiscriminator);
for (String alias : discriminatorAnnotation.aliases()) {
allDiscriminators.add(alias);
}
} else {
allDiscriminators.add(mainDiscriminator);
}
for (String discriminator : allDiscriminators) {
allDiscriminatorKeyValueFilters.add(Filters.eq(discriminatorKey, discriminator));
PolymorphicCodec registeredCodec = this.discriminatorToCodec.putIfAbsent(discriminator, codecFor);
if (registeredCodec != null) {
LOGGER.warn("Cannot register multiple classes ({}, {}) for the same discriminator {} ", clazz, registeredCodec.getEncoderClass(), discriminator);
throw new IllegalArgumentException("Cannot register multiple classes (" + clazz + ", " + registeredCodec.getEncoderClass() + ") for the same discriminator " + discriminator);
}
}
mainDiscriminators.put(clazz, mainDiscriminator);
}
}
//check for properties within classes that are named exactly like one of the used main discrimimnator keys
for (PolymorphicCodec typeCodec : classToCodec.values()) {
typeCodec.verifyFieldsNotNamedLikeAnyDiscriminatorKey(allDiscriminatorKeys);
}
this.typeFilter = Filters.or(allDiscriminatorKeyValueFilters);
// if any of the subclass codecs need application id generation, mark this codec as being collectible
this.isCollectible = isAnyCodecCollectible;
LOGGER.debug("Type {} -> Found the following matching types {}", type, discriminatorToCodec);
}
private String getDiscriminatorKeyForClass(Class> clazz) {
DiscriminatorKey discriminatorKey = clazz.getAnnotation(DiscriminatorKey.class);
if (discriminatorKey != null && discriminatorKey.value() != null && discriminatorKey.value().length() > 0) {
return discriminatorKey.value();
}
return "_t";
}
@Override
public T decode(BsonReader reader, DecoderContext decoderContext) {
if (reader.getCurrentBsonType() == BsonType.NULL) {
reader.readNull();
return null;
}
String discriminator = null;
BsonReaderMark mark = reader.getMark();
reader.readStartDocument();
PolymorphicCodec codec = null;
while (reader.readBsonType() != BsonType.END_OF_DOCUMENT) {
String fieldName = reader.readName();
if (allDiscriminatorKeys.contains(fieldName)) {
discriminator = reader.readString();
codec = getCodecForDiscriminator(discriminator);
if (codec != null) {
//now check that the codec found actually has the correct
String discriminatorKeyForClass = discriminatorKeys.get(codec.getEncoderClass());
if (fieldName.equals(discriminatorKeyForClass)) {
break;
} else {
discriminator = null;
codec = null;
LOGGER.warn("Confusing. Skipping discriminator {} encoded in discriminator key {} since the " +
"destination class is declaring a different discriminator key {}.",
discriminator, fieldName, discriminatorKeyForClass);
}
}
} else {
reader.skipValue();
}
}
mark.reset();
// try fallback and legacy handling
if (codec == null) {
if (discriminator != null) {
LOGGER.warn("At least one valid discriminator {} was found in database, but no matching codec found at all.", discriminator);
reader.skipValue();
return null; // todo: when switching to mongo db 3.6 an exception should be thrown instead of returning null
}
LOGGER.debug("No discriminator found in db for entity. Trying fallback. Fallback is {}", fallBackCodec);
codec = fallBackCodec;
if (codec == null) {
LOGGER.debug("FallbackCodec is null. Still no matching codec found for discriminator {} within discriminatorToCodec {}", discriminator, discriminatorToCodec);
if (classToCodec.values().size() == 1) {
codec = classToCodec.values().iterator().next();
LOGGER.debug("Found single possible codec {} for type {}", codec, getEncoderClass());
}
else {
LOGGER.info("Legacy handling to resolve entities in db without discriminator failed as there are (now?) more than one codecs available {}. One option is to use @DiscriminatrFallback at the legacy class or to add discriminators to the entities within the database. For now, return least specific codec.", classToCodec);
codec = classToCodec.get(getEncoderClass());
// if codec is still null at this point, something is broken -> should not happen
if (codec == null) {
LOGGER.warn("Skipping value. Can not determine codec for class {} from available codecs {}", getEncoderClass(), classToCodec);
reader.skipValue();
return null;// todo: when switching to mongo db 3.6 an exception should be thrown instead of returning null, so this entity can be skipped
}
}
}
}
return decodeWithType(reader, decoderContext, codec);
}
protected T decodeWithType(BsonReader reader, DecoderContext decoderContext, PolymorphicCodec polymorphicCodec) {
return polymorphicCodec.decode(reader, decoderContext);
}
@Override
public void encode(BsonWriter writer, T value, EncoderContext encoderContext) {
if (value == null) {
writer.writeNull();
}
else {
writer.writeStartDocument();
PolymorphicCodec codecForValue = getCodecForClass(value.getClass());
if (codecForValue != null) {
writer.writeName(discriminatorKeys.get(codecForValue.getEncoderClass()));
writer.writeString(mainDiscriminators.get(codecForValue.getEncoderClass()));
codecForValue.encodeFields(writer, value, encoderContext);
} else {
LOGGER.warn("The value to be encoded has the wrong type {}. This codec can only handle {}", value.getClass(), discriminatorToCodec);
}
writer.writeEndDocument();
}
}
private PolymorphicCodec getCodecForDiscriminator(String discriminator) {
if (discriminator == null) {
LOGGER.warn("Discriminator key cannot be null.");
return null;
}
return discriminatorToCodec.get(discriminator);
}
/**
* Walks up class hierarchy until a registered codec (in the context of registered model classes) is found
*
* @return a codec responsible for a valid class within the class hierarchy
*/
public PolymorphicCodec getCodecForClass(Class> clazz) {
if (clazz == null) {
return null;
}
PolymorphicCodec codec = classToCodec.get(clazz);
if (codec != null) {
return codec;
}
return getCodecForClass(clazz.getSuperclass());
}
private PolymorphicCodec getCodecForValue(T document) {
return getCodecForClass(document.getClass());
}
@Override
public Class getEncoderClass() {
return clazz;
}
@Override
public boolean isCollectible() {
return isCollectible;
}
@Override
public T generateIdIfAbsentFromDocument(T document) {
PolymorphicCodec codecForValue = getCodecForValue(document);
if (codecForValue != null) {
codecForValue.generateIdIfAbsentFromDocument(document);
}
return document;
}
@Override
public boolean documentHasId(T document) {
PolymorphicCodec codecForValue = getCodecForValue(document);
if (codecForValue != null) {
return codecForValue.documentHasId(document);
}
return false;
}
@Override
public BsonValue getDocumentId(T document) {
PolymorphicCodec codecForValue = getCodecForValue(document);
if (codecForValue != null) {
return codecForValue.getDocumentId(document);
}
return null;
}
/**
* Example:
*
* Or Filter{
filters=[
Filter{fieldName='_t', value=Square},
Filter{fieldName='_discriminatorKey', value=Circle}
]
}
*
*
* @return the filter needed to find all types this codec is responsible for
*/
@Override
public Bson getTypeFilter() {
return typeFilter;
}
}