From 9633847a1703a658a042ce7cb35605d9cc5490f9 Mon Sep 17 00:00:00 2001 From: Michael Weber Date: Mon, 15 Jan 2018 15:51:38 +0100 Subject: [PATCH] ##2.0.0 * Improved error checking while application id generation * add support to register codecs for any arbitrary type -> de.bild.codec.TypeCodecProvider * support for SortedSet added * support for polymorphic codecs that do not need to be ReflectionCodecs -> PolymorphicCodec CodecResolver.getCodec(...) * improved null-value handling: now nulls can be encoded as nulls or nulls are not written at all -> de.bild.codec.annotations.EncodeNulls * added annotation driven null value handling while encoding -> de.bild.codec.annotations.EncodeNullHandlingStrategy * added annotation driven undefined value handling while decoding -> de.bild.codec.annotations.DecodeUndefinedHandlingStrategy --- README.md | 79 +++++- pom.xml | 4 +- release_notes.md | 11 + .../java/de/bild/codec/AbstractTypeCodec.java | 1 - .../de/bild/codec/BasicReflectionCodec.java | 71 ++--- .../de/bild/codec/CodecConfiguration.java | 19 ++ .../java/de/bild/codec/CodecResolver.java | 7 +- .../de/bild/codec/CollectionTypeCodec.java | 1 - .../de/bild/codec/IdGenerationException.java | 11 + src/main/java/de/bild/codec/MapTypeCodec.java | 1 - src/main/java/de/bild/codec/MappedField.java | 141 ++++++---- .../java/de/bild/codec/PojoCodecProvider.java | 53 +++- src/main/java/de/bild/codec/PojoContext.java | 222 +++++++-------- .../java/de/bild/codec/PolymorphicCodec.java | 74 +++++ .../codec/PolymorphicReflectionCodec.java | 80 +++--- .../java/de/bild/codec/ReflectionCodec.java | 53 ++-- .../java/de/bild/codec/ReflectionHelper.java | 16 ++ src/main/java/de/bild/codec/SetTypeCodec.java | 5 + .../de/bild/codec/SpecialFieldsMapCodec.java | 31 ++- src/main/java/de/bild/codec/TypeCodec.java | 12 +- .../java/de/bild/codec/TypeCodecProvider.java | 18 ++ .../DecodeUndefinedHandlingStrategy.java | 40 +++ .../EncodeNullHandlingStrategy.java | 42 +++ .../bild/codec/annotations/EncodeNulls.java | 19 ++ .../java/de/bild/codec/annotations/Id.java | 1 - .../java/de/bild/codec/CodecResolverTest.java | 12 +- .../java/de/bild/codec/ListTypeCodecTest.java | 18 +- .../java/de/bild/codec/NullHandlingTest.java | 136 +++++++++ .../ExternalIdCodecProviderTest.java | 13 +- .../ExternalIdCodecProviderTest.java | 112 ++++++++ .../codec/idtypemismatch/model/CustomId.java | 15 + .../bild/codec/idtypemismatch/model/Pojo.java | 17 ++ .../TypeCodecProviderTest.java | 258 ++++++++++++++++++ 33 files changed, 1262 insertions(+), 331 deletions(-) create mode 100644 src/main/java/de/bild/codec/CodecConfiguration.java create mode 100644 src/main/java/de/bild/codec/IdGenerationException.java create mode 100644 src/main/java/de/bild/codec/PolymorphicCodec.java create mode 100644 src/main/java/de/bild/codec/TypeCodecProvider.java create mode 100644 src/main/java/de/bild/codec/annotations/DecodeUndefinedHandlingStrategy.java create mode 100644 src/main/java/de/bild/codec/annotations/EncodeNullHandlingStrategy.java create mode 100644 src/main/java/de/bild/codec/annotations/EncodeNulls.java create mode 100644 src/test/java/de/bild/codec/NullHandlingTest.java create mode 100644 src/test/java/de/bild/codec/idtypemismatch/ExternalIdCodecProviderTest.java create mode 100644 src/test/java/de/bild/codec/idtypemismatch/model/CustomId.java create mode 100644 src/test/java/de/bild/codec/idtypemismatch/model/Pojo.java create mode 100644 src/test/java/de/bild/codec/typecodecprovider/TypeCodecProviderTest.java diff --git a/README.md b/README.md index 0340d2d..1443f7e 100644 --- a/README.md +++ b/README.md @@ -138,6 +138,16 @@ Alternatively you could mark those properties with [@Transient](src/main/java/de Note that decoding into POJOs that are not registered within the codec does not work. A [NonRegisteredModelClassException](src/main/java/de/bild/codec/NonRegisteredModelClassException.java) will be thrown. + +## Application Id generation +The mongo java driver allows for application id generation. In general to enable this feature, the mongo java driver requests the codec responsible for an entity to implement org.bson.codecs.CollectibleCodec. This is handled transparently within Polymorphia and you do not need to care about. + +Within your pojo class you can annotate a **single** property with [@Id(collectible=true, value = CustomIdGenerator.class)](src/main/java/de/bild/codec/annotations/Id.java) +When the codec for this pojo is being built and such an Id annotated field is found, the final codec will be wrapped into a java.lang.reflect.Proxy that implements the org.bson.codecs.CollectibleCodec interface. +If your Pojo is part of a polymorphic structure, one or all of your subclasses within that structure can allow for application id generation. Each subclass can generate individual Ids of different types. The mongo database is capable of handling different ids in one collection. +The PolymorphicReflectionCodec takes care of this and will implement the CollectibleCodec interface if (and only if) at least one PolymorphicCodec (one for each sub type) is found to be collectible. + + ## Updating entities Updating entities is straight forward. @@ -177,8 +187,6 @@ You may wonder, why different annotations can be defined to ignore types. As ou ``` - - ## Advanced usage If you have a data structure within your mongo whereby you are not exactly sure what fields are declared, but you know @@ -198,7 +206,20 @@ public class MapWithSpecialFieldsPojo extends Document implements SpecialFieldsM } ``` -If you need fine grained control over deserialization and serialization register a CodecResolver. + +## Hooks for custom codecs + +If you want to provide your own serialization and deserialization codecs but at the same time want to benefit from Polymorphias capabilities +to map polymorphic structures, you can chain your custom codecs by registering a [CodecResolver](src/main/java/de/bild/codec/CodecResolver.java) when building +the [PojoCodecProvider](src/main/java/de/bild/codec/PojoCodecProvider.java) +These codecs however need to implement a specialization of org.bson.codecs.Codec namely [PolymorphicCodec](src/main/java/de/bild/codec/PolymorphicCodec.java) +This interface adds the following features: + * handles discriminator issues like check for fields names that must not be equal to any discriminator key + * provides methods to encode any additional fields besides the discriminator + * copies the method signatures of org.bson.codecs.CollectibleCodec but without implementing CollectibleCodec (The reason is explained in the section [Application Id generation]) + * this enables your codec to generate application ids + * provides methods to instantiate your entities and set default values + For an example of usage @see [CodecResolverTest](src/test/java/de/bild/codec/CodecResolverTest.java) ```java @@ -213,19 +234,49 @@ PojoCodecProvider.builder() .build() ``` +### [TypeCodecProvider](src/main/java/de/bild/codec/TypeCodecProvider.java) +If you desire to register a codec provider that can provide a codec for any given type (not just for java.lang.Class) you may register a [TypeCodecProvider](src/main/java/de/bild/codec/TypeCodecProvider.java) +For an example of usage have a look at [TypeCodecProviderTest](src/test/java/de/bild/codec/typecodecprovider/TypeCodecProviderTest.java) +You can override any fully specified type or generic type. You can e.g. register alternative codecs for Set or List or Map. +In contrast to org.bson.codecs.configuration.CodecProvider the registered [TypeCodecProvider](src/main/java/de/bild/codec/TypeCodecProvider.java) accepts any java.lang.reflect.Type +Please be aware of the fact, that these TypeCodecProviders will only take effect when resolved within PojoCodecProvider. + + ## Default values -For now the codecs that encode collections(set, list) and maps encode __null__ values as empty collections or empty maps. -It might be a future improvement to better control this behaviour as it might be undesired to have empty (null) fields persisted at all. -The idea is to provide an annotation that describes the default value in case the field value is null. -Feel free to add this functionality. +As of Polymoprhia version 2.0.0 the developer has better control over null-handling while encoding to the database. Additionally a developer can control default values when decoding undefined fields. +Use [EncodeNulls](de.bild.codec.annotations.EncodeNulls) to decide whether you need nulls written to the database. +You can convert null values into defaults before the encoder kicks in. Use [EncodeNullHandlingStrategy](de.bild.codec.annotations.EncodeNullHandlingStrategy) to assign values to null fields if desired. + + +While decoding fields that are present in the pojo but have undefined values in the database (evolution of pojos!) you can assign a [DecodeUndefinedHandlingStrategy](de.bild.codec.annotations.DecodeUndefinedHandlingStrategy). + +The mentioned annotations can be used at class level and field level. Field level annotations overrule class annotations. It is also possible to set global behaviour for these configurations. When building your [PojoCodecProvider](src/main/java/de/bild/codec/PojoCodecProvider.java) use the Builder methods to control the global defaults. +* de.bild.codec.PojoCodecProvider.Builder#encodeNullHandlingStrategy(Strategy) -> defaults to CODEC (historical reasons) +* de.bild.codec.PojoCodecProvider.Builder#decodeUndefinedHandlingStrategy(Strategy) -> defaults to KEEP_POJO_DEFAULT +* de.bild.codec.PojoCodecProvider.Builder#encodeNulls(boolean) -> defaluts to false + +Have a look at [NullHandlingTest](src/test/java/de/bild/codec/NullHandlingTest.java) for an example. + ```java -public class Pojo { - //attention: this code needs to be written - @Default(DefaultValueProvider.class) - Integer aField; -} -``` +PojoCodecProvider.builder() + .register(NullHandlingTest.class) + .encodeNulls(false) + .decodeUndefinedHandlingStrategy(DecodeUndefinedHandlingStrategy.Strategy.KEEP_POJO_DEFAULT) + .encodeNullHandlingStrategy(EncodeNullHandlingStrategy.Strategy.KEEP_NULL) + .build() + + + @EncodeNulls(false) + public class Pojo { + @DecodeUndefinedHandlingStrategy(DecodeUndefinedHandlingStrategy.Strategy.CODEC) + Integer aField; + + @DecodeUndefinedHandlingStrategy(DecodeUndefinedHandlingStrategy.Strategy.SET_TO_NULL) + @EncodeNulls + String anotherField; + } +``` ## Release Notes @@ -235,7 +286,7 @@ Release notes are available [release_notes.md](release_notes.md). de.bild.backend polymorphia - 1.7.0 + 2.0.0 ``` diff --git a/pom.xml b/pom.xml index 4c7fd7e..bdd7c52 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ de.bild.backend polymorphia - 1.7.0 + 2.0.0 ${project.groupId}:${project.artifactId} @@ -124,7 +124,7 @@ org.projectlombok lombok - test + compile diff --git a/release_notes.md b/release_notes.md index adaeb25..1a7b3b7 100644 --- a/release_notes.md +++ b/release_notes.md @@ -1,6 +1,17 @@ Release Notes ======= +##2.0.0 +* Improved error checking while application id generation +* add support to register codecs for any arbitrary type -> de.bild.codec.TypeCodecProvider +* support for SortedSet added +* support for polymorphic codecs that do not need to be ReflectionCodecs -> PolymorphicCodec CodecResolver.getCodec(...) +* improved null-value handling: now nulls can be encoded as nulls or nulls are not written at all -> de.bild.codec.annotations.EncodeNulls +* added annotation driven null value handling while encoding -> de.bild.codec.annotations.EncodeNullHandlingStrategy +* added annotation driven undefined value handling while decoding -> de.bild.codec.annotations.DecodeUndefinedHandlingStrategy + + + ##1.7.0 added support to ignore model classes in scanned packages diff --git a/src/main/java/de/bild/codec/AbstractTypeCodec.java b/src/main/java/de/bild/codec/AbstractTypeCodec.java index 33615fa..f41acab 100644 --- a/src/main/java/de/bild/codec/AbstractTypeCodec.java +++ b/src/main/java/de/bild/codec/AbstractTypeCodec.java @@ -54,7 +54,6 @@ protected Constructor getDefaultConstructor(Class clazz) { } } - @Override public T newInstance() { try { return defaultConstructor.newInstance(); diff --git a/src/main/java/de/bild/codec/BasicReflectionCodec.java b/src/main/java/de/bild/codec/BasicReflectionCodec.java index 8d3e5e0..9b75eef 100644 --- a/src/main/java/de/bild/codec/BasicReflectionCodec.java +++ b/src/main/java/de/bild/codec/BasicReflectionCodec.java @@ -4,6 +4,7 @@ import de.bild.codec.annotations.PostLoad; import de.bild.codec.annotations.PreSave; import de.bild.codec.annotations.Transient; +import org.apache.commons.lang3.reflect.TypeUtils; import org.bson.BsonReader; import org.bson.BsonType; import org.bson.BsonValue; @@ -19,7 +20,7 @@ public class BasicReflectionCodec extends AbstractTypeCodec implements ReflectionCodec { private static final Logger LOGGER = LoggerFactory.getLogger(BasicReflectionCodec.class); - MappedField idField; + MappedField idField; /** * a list of the fields to map @@ -30,13 +31,13 @@ public class BasicReflectionCodec extends AbstractTypeCodec implements Ref IdGenerator idGenerator; boolean isCollectible; - public BasicReflectionCodec(Type type, TypeCodecRegistry typeCodecRegistry) { + public BasicReflectionCodec(Type type, TypeCodecRegistry typeCodecRegistry, CodecConfiguration codecConfiguration) { super(type, typeCodecRegistry); // resolve all persistable fields for (final FieldTypePair fieldTypePair : ReflectionHelper.getDeclaredAndInheritedFieldTypePairs(type, true)) { Field field = fieldTypePair.getField(); if (!isIgnorable(field)) { - MappedField mappedField = new MappedField(fieldTypePair, encoderClass, typeCodecRegistry); + MappedField mappedField = new MappedField<>(fieldTypePair, encoderClass, typeCodecRegistry, codecConfiguration); persistenceFields.put(mappedField.getMappedFieldName(), mappedField); if (mappedField.isIdField()) { if (idField == null) { @@ -53,7 +54,7 @@ public BasicReflectionCodec(Type type, TypeCodecRegistry typeCodecRegistry) { throw new IllegalArgumentException("Could not create instance of IdGenerator for class " + type + " Generator class: " + idGeneratorClass, e); } } else { - throw new IllegalArgumentException("Id field is used again in class hierarchy! Class " + encoderClass); + throw new IllegalArgumentException("Id field is annotated multiple times in class hierarchy! Class " + encoderClass); } } } @@ -82,41 +83,31 @@ protected boolean isIgnorable(final Field field) { || Modifier.isTransient(field.getModifiers()); } - - @Override - public T decode(BsonReader reader, DecoderContext decoderContext) { - T newInstance; - //if reader is in initial state (reader.getCurrentBsonType() == null) or DOCUMENT state - if (reader.getCurrentBsonType() == null || reader.getCurrentBsonType() == BsonType.DOCUMENT) { - reader.readStartDocument(); - newInstance = decodeFields(reader, decoderContext, newInstance()); - reader.readEndDocument(); - return newInstance; - } else { - LOGGER.error("Expected to read document but reader is in state {}. Skipping value!", reader.getCurrentBsonType()); - reader.skipValue(); - return null; - } - } - @Override public T decodeFields(BsonReader reader, DecoderContext decoderContext, T instance) { + Set fieldNames = new HashSet<>(persistenceFields.keySet()); + while (reader.readBsonType() != BsonType.END_OF_DOCUMENT) { String fieldName = reader.readName(); MappedField mappedField = persistenceFields.get(fieldName); if (mappedField != null) { + fieldNames.remove(fieldName); mappedField.decode(reader, instance, decoderContext); } else { reader.skipValue(); } } + + // for all non-found (undefined) fields, run initialization + for (String fieldName : fieldNames) { + persistenceFields.get(fieldName).initializeUndefinedValue(instance); + } postDecode(instance); return instance; } @Override public void postDecode(T instance) { - initializeDefaults(instance); for (Method postLoadMethod : postLoadMethods) { try { postLoadMethod.invoke(instance); @@ -126,20 +117,6 @@ public void postDecode(T instance) { } } - @Override - public void initializeDefaults(T instance) { - for (MappedField persistenceField : persistenceFields.values()) { - persistenceField.initializeDefault(instance); - } - } - - @Override - public void encode(BsonWriter writer, T instance, EncoderContext encoderContext) { - writer.writeStartDocument(); - encodeFields(writer, instance, encoderContext); - writer.writeEndDocument(); - } - @Override public void encodeFields(BsonWriter writer, T instance, EncoderContext encoderContext) { preEncode(instance); @@ -164,11 +141,6 @@ public MappedField getMappedField(String mappedFieldName) { return persistenceFields.get(mappedFieldName); } - @Override - public MappedField getIdField() { - return idField; - } - @Override public boolean isCollectible() { return isCollectible; @@ -177,9 +149,20 @@ public boolean isCollectible() { @Override public T generateIdIfAbsentFromDocument(T document) { if (idGenerator != null && !documentHasId(document)) { - boolean couldGenerate = idField.setFieldValue(document, idGenerator.generate()); - if (!couldGenerate) { - LOGGER.error("Could not set id!"); + Object generatedId = idGenerator.generate(); + try { + if (!idField.setFieldValue(document, generatedId)) { + LOGGER.error("Id {} for pojo {} could not be set. Please watch the logs.", generatedId, document); + throw new IdGenerationException("Id could not be generated for pojo. See logs for details."); + } + } catch (TypeMismatchException e) { + if (generatedId != null && !TypeUtils.isAssignable(generatedId.getClass(), idField.fieldTypePair.realType)) { + LOGGER.error("Your set id generator {} for the id field {} produces non-assignable values.", idGenerator, idField, e); + } + else { + LOGGER.error("Some unspecified error occurred while generating an id {} for your pojo {}", generatedId, document); + } + throw new IdGenerationException("Id could not be generated for pojo. See logs for details.", e); } } return document; diff --git a/src/main/java/de/bild/codec/CodecConfiguration.java b/src/main/java/de/bild/codec/CodecConfiguration.java new file mode 100644 index 0000000..dc887d8 --- /dev/null +++ b/src/main/java/de/bild/codec/CodecConfiguration.java @@ -0,0 +1,19 @@ +package de.bild.codec; + +import de.bild.codec.annotations.DecodeUndefinedHandlingStrategy; +import de.bild.codec.annotations.EncodeNullHandlingStrategy; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; + +/** + * Helper class holding configurations for the PojoCodecProvider + */ +@Builder +@AllArgsConstructor +@Getter +public class CodecConfiguration { + private boolean encodeNulls; + private EncodeNullHandlingStrategy.Strategy encodeNullHandlingStrategy; + private DecodeUndefinedHandlingStrategy.Strategy decodeUndefinedHandlingStrategy; +} diff --git a/src/main/java/de/bild/codec/CodecResolver.java b/src/main/java/de/bild/codec/CodecResolver.java index 5cea53c..2e313f7 100644 --- a/src/main/java/de/bild/codec/CodecResolver.java +++ b/src/main/java/de/bild/codec/CodecResolver.java @@ -3,15 +3,16 @@ import java.lang.reflect.Type; /** - * This interface can be used to add special handling pojo of certain types. + * This interface can be used to add special handling for pojos of certain types. * Simply register a CodecResolver when you build the {@link PojoCodecProvider} */ public interface CodecResolver { /** * - * @param typeCodecRegistry codec registry for any type * @param type the type to be handled + * @param typeCodecRegistry codec registry for any type + * @param codecConfiguration * @return null, if resolver cannot handle type or a codec that is able to handle the type */ - ReflectionCodec getCodec(Type type, TypeCodecRegistry typeCodecRegistry); + PolymorphicCodec getCodec(Type type, TypeCodecRegistry typeCodecRegistry, CodecConfiguration codecConfiguration); } diff --git a/src/main/java/de/bild/codec/CollectionTypeCodec.java b/src/main/java/de/bild/codec/CollectionTypeCodec.java index 1d44966..d258fe4 100644 --- a/src/main/java/de/bild/codec/CollectionTypeCodec.java +++ b/src/main/java/de/bild/codec/CollectionTypeCodec.java @@ -58,7 +58,6 @@ public void encode(BsonWriter writer, C value, EncoderContext encoderContext) { writer.writeEndArray(); } - @Override public C defaultInstance() { return newInstance(); } diff --git a/src/main/java/de/bild/codec/IdGenerationException.java b/src/main/java/de/bild/codec/IdGenerationException.java new file mode 100644 index 0000000..35643a8 --- /dev/null +++ b/src/main/java/de/bild/codec/IdGenerationException.java @@ -0,0 +1,11 @@ +package de.bild.codec; + +public class IdGenerationException extends RuntimeException { + public IdGenerationException(String message) { + super(message); + } + + public IdGenerationException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/src/main/java/de/bild/codec/MapTypeCodec.java b/src/main/java/de/bild/codec/MapTypeCodec.java index a380284..6e32e04 100644 --- a/src/main/java/de/bild/codec/MapTypeCodec.java +++ b/src/main/java/de/bild/codec/MapTypeCodec.java @@ -28,7 +28,6 @@ protected Constructor> getDefaultConstructor(Class> clazz) { return super.getDefaultConstructor(clazz); } - @Override public Map defaultInstance() { return newInstance(); } diff --git a/src/main/java/de/bild/codec/MappedField.java b/src/main/java/de/bild/codec/MappedField.java index b54746c..c558c9c 100644 --- a/src/main/java/de/bild/codec/MappedField.java +++ b/src/main/java/de/bild/codec/MappedField.java @@ -1,9 +1,6 @@ package de.bild.codec; -import de.bild.codec.annotations.CodecToBeUsed; -import de.bild.codec.annotations.Id; -import de.bild.codec.annotations.LockingVersion; -import org.apache.commons.lang3.reflect.TypeUtils; +import de.bild.codec.annotations.*; import org.bson.BsonReader; import org.bson.BsonType; import org.bson.BsonWriter; @@ -20,7 +17,11 @@ import java.lang.reflect.Type; import java.util.*; -public class MappedField { +/** + * @param The type of the persistence class container (pojo) for this mapped field + * @param The type of the field value + */ +public class MappedField { private static final Logger LOGGER = LoggerFactory.getLogger(MappedField.class); public static final String ID_KEY = "_id"; @@ -29,25 +30,39 @@ public class MappedField { static { ANNOTATIONS_TO_BE_HANDLED.add(Id.class); ANNOTATIONS_TO_BE_HANDLED.add(LockingVersion.class); + ANNOTATIONS_TO_BE_HANDLED.add(EncodeNullHandlingStrategy.class); + ANNOTATIONS_TO_BE_HANDLED.add(DecodeUndefinedHandlingStrategy.class); + ANNOTATIONS_TO_BE_HANDLED.add(EncodeNulls.class); } + final Field field; - final Class persistedClass; + final Class persistedClass; - private Codec codec; + private Codec codec; private PrimitiveType primitiveType; final FieldTypePair fieldTypePair; + final EncodeNullHandlingStrategy.Strategy encodeNullHandlingStrategy; + final DecodeUndefinedHandlingStrategy.Strategy decodeUndefinedHandlingStrategy; + final boolean encodeNulls; + + final CodecConfiguration codecConfiguration; + // Annotations that have been found relevant to mapping private final Map, Annotation> foundAnnotations; - public MappedField(FieldTypePair fieldTypePair, Class persistedClass, TypeCodecRegistry typeCodecRegistry) { + public MappedField(FieldTypePair fieldTypePair, + Class persistedClass, + TypeCodecRegistry typeCodecRegistry, + CodecConfiguration codecConfiguration) { this.field = fieldTypePair.getField(); this.field.setAccessible(true); this.fieldTypePair = fieldTypePair; this.persistedClass = persistedClass; this.foundAnnotations = buildAnnotationMap(field); + this.codecConfiguration = codecConfiguration; if (field.getType().isPrimitive()) { this.primitiveType = PrimitiveType.get(field.getType()); @@ -75,10 +90,26 @@ public MappedField(FieldTypePair fieldTypePair, Class persistedClass, TypeCod } else { this.codec = typeCodecRegistry.getCodec(fieldTypePair.getRealType()); } - } + + /** + * get configuration for field + */ + + EncodeNullHandlingStrategy classEncodeNullHandlingStrategy = persistedClass.getDeclaredAnnotation(EncodeNullHandlingStrategy.class); + EncodeNullHandlingStrategy fieldEncodeNullHandlingStrategy = getAnnotation(EncodeNullHandlingStrategy.class); + this.encodeNullHandlingStrategy = (fieldEncodeNullHandlingStrategy != null) ? fieldEncodeNullHandlingStrategy.value() : (classEncodeNullHandlingStrategy != null) ? classEncodeNullHandlingStrategy.value() : codecConfiguration.getEncodeNullHandlingStrategy(); + + DecodeUndefinedHandlingStrategy classDecodeUndefinedHandlingStrategy = persistedClass.getDeclaredAnnotation(DecodeUndefinedHandlingStrategy.class); + DecodeUndefinedHandlingStrategy fieldDecodeUndefinedHandlingStrategy = getAnnotation(DecodeUndefinedHandlingStrategy.class); + this.decodeUndefinedHandlingStrategy = (fieldDecodeUndefinedHandlingStrategy != null) ? fieldDecodeUndefinedHandlingStrategy.value() : (classDecodeUndefinedHandlingStrategy != null) ? classDecodeUndefinedHandlingStrategy.value() : codecConfiguration.getDecodeUndefinedHandlingStrategy(); + + EncodeNulls classEncodeNulls = persistedClass.getDeclaredAnnotation(EncodeNulls.class); + EncodeNulls fieldEncodeNulls = getAnnotation(EncodeNulls.class); + this.encodeNulls = (fieldEncodeNulls != null) ? fieldEncodeNulls.value() : (classEncodeNulls != null) ? classEncodeNulls.value() : codecConfiguration.isEncodeNulls(); } + private static Map, Annotation> buildAnnotationMap(Field field) { final Map, Annotation> foundAnnotations = new HashMap<>(); for (final Class annotationClass : ANNOTATIONS_TO_BE_HANDLED) { @@ -123,13 +154,6 @@ public String getMappedFieldName() { return field.getName(); } - /** - * @return the declaring class of the java field - */ - public Class getDeclaringClass() { - return field.getDeclaringClass(); - } - /** * @return the underlying java field */ @@ -157,7 +181,7 @@ public int hashCode() { return field.hashCode(); } - public boolean setFieldValue(Object instance, Object value) { + public boolean setFieldValue(T instance, F value) { try { field.set(instance, value); return true; @@ -168,16 +192,16 @@ public boolean setFieldValue(Object instance, Object value) { } } - public Object getFieldValue(Object instance) { + public F getFieldValue(T instance) { try { - return field.get(instance); + return (F) field.get(instance); } catch (IllegalAccessException e) { LOGGER.warn("Could not get field value.", field, instance, e); } return null; } - public void encode(BsonWriter writer, T instance, EncoderContext encoderContext) { + public void encode(BsonWriter writer, T instance, EncoderContext encoderContext) { LOGGER.debug("Encode field : " + getMappedFieldName()); if (field.getType().isPrimitive()) { if (isLockingVersionField()) { @@ -186,20 +210,34 @@ public void encode(BsonWriter writer, T instance, EncoderContext encoderCont primitiveType.encode(writer, instance, encoderContext, this); } } else if (codec != null) { - Object fieldValue = getFieldValue(instance); - if (fieldValue == null && codec instanceof TypeCodec) { - TypeCodec typeCodec = (TypeCodec) codec; - fieldValue = typeCodec.defaultInstance(); + F fieldValue = getFieldValue(instance); + if (fieldValue == null) { + switch (encodeNullHandlingStrategy) { + case CODEC: { + if (codec instanceof TypeCodec) { + TypeCodec typeCodec = (TypeCodec) codec; + fieldValue = typeCodec.defaultInstance(); + } + break; + } + case KEEP_NULL: + break; + + } } - if (fieldValue != null) { + if (encodeNulls || fieldValue != null) { writer.writeName(getMappedFieldName()); - codec.encode(writer, fieldValue, encoderContext); + if (fieldValue == null) { + writer.writeNull(); + } else { + codec.encode(writer, fieldValue, encoderContext); + } } } } - private void writeLockingVersion(BsonWriter writer, T instance) { + private void writeLockingVersion(BsonWriter writer, T instance) { try { writer.writeName(getMappedFieldName()); int lockingVersion = field.getInt(instance) + 1; @@ -209,7 +247,7 @@ private void writeLockingVersion(BsonWriter writer, T instance) { } } - public void decode(BsonReader reader, T instance, DecoderContext decoderContext) { + public void decode(BsonReader reader, T instance, DecoderContext decoderContext) { LOGGER.debug("Decode field : " + getMappedFieldName() + " Signature: " + fieldTypePair.getRealType()); if (field.getType().isPrimitive()) { if (reader.getCurrentBsonType() == BsonType.NULL || reader.getCurrentBsonType() == BsonType.UNDEFINED) { @@ -218,16 +256,16 @@ public void decode(BsonReader reader, T instance, DecoderContext decoderCont primitiveType.decode(reader, instance, decoderContext, this); } } else if (codec != null) { - if (reader.getCurrentBsonType() == BsonType.NULL) { + if (reader.getCurrentBsonType() == BsonType.NULL ) { reader.readNull(); setFieldValue(instance, null); - } else if (reader.getCurrentBsonType() == BsonType.UNDEFINED) { + } + else if (reader.getCurrentBsonType() == BsonType.UNDEFINED) { reader.skipValue(); - } else { - Object decoded = codec.decode(reader, decoderContext); - if (decoded != null) { - setFieldValue(instance, decoded); - } + } + else { + F decoded = codec.decode(reader, decoderContext); + setFieldValue(instance, decoded); } } } @@ -245,25 +283,34 @@ public Codec getCodec() { } /** - * * @param instance to initialize - * @param type of the instance - * @return true, if the codec initialized (changed) the field + * @return true, if the codec initialized (potentially changed) the field */ - public boolean initializeDefault(T instance) { + public void initializeUndefinedValue(T instance) { if (field.getType().isPrimitive()) { - return false; - } else if (codec != null && codec instanceof TypeCodec) { - TypeCodec typeCodec = (TypeCodec) codec; - if (getFieldValue(instance) == null) { - Object defaultValue = typeCodec.defaultInstance(); - if (defaultValue != null) { + return; + } + switch (decodeUndefinedHandlingStrategy) { + case CODEC: { + // this potentially overwrites the default value set within the pojo + if (codec != null && codec instanceof TypeCodec) { + TypeCodec typeCodec = (TypeCodec) codec; + F defaultValue = typeCodec.defaultInstance(); setFieldValue(instance, defaultValue); - return true; + return; + } else { + LOGGER.info("The provided codec {} for field {} is not capable of retrieving default values.", codec, field); } + break; + } + case SET_TO_NULL: { + // this potentially overwrites the default value set within the pojo + setFieldValue(instance, null); + return; } + case KEEP_POJO_DEFAULT: + break; } - return false; } diff --git a/src/main/java/de/bild/codec/PojoCodecProvider.java b/src/main/java/de/bild/codec/PojoCodecProvider.java index c92db42..6bbc49a 100644 --- a/src/main/java/de/bild/codec/PojoCodecProvider.java +++ b/src/main/java/de/bild/codec/PojoCodecProvider.java @@ -2,6 +2,8 @@ import com.google.common.reflect.AbstractInvocationHandler; +import de.bild.codec.annotations.DecodeUndefinedHandlingStrategy; +import de.bild.codec.annotations.EncodeNullHandlingStrategy; import org.bson.BsonReader; import org.bson.BsonValue; import org.bson.BsonWriter; @@ -29,9 +31,14 @@ public class PojoCodecProvider implements CodecProvider { private final TypesModel typesModel; private final PojoContext pojoContext; - PojoCodecProvider(final Set> classes, final Set packages, final Set> ignoreAnnotations, final List codecResolvers) { + PojoCodecProvider(final Set> classes, + final Set packages, + final Set> ignoreAnnotations, + List typeCodecProviders, + final List codecResolvers, + CodecConfiguration codecConfiguration) { this.typesModel = new TypesModel(classes, packages, ignoreAnnotations); - this.pojoContext = new PojoContext(typesModel, codecResolvers); + this.pojoContext = new PojoContext(typesModel, codecResolvers, typeCodecProviders, codecConfiguration); } public static Builder builder() { @@ -136,9 +143,10 @@ public static class Builder { private Set> classes = new HashSet<>(); private List codecResolvers = new ArrayList<>(); private Set> ignoreAnnotations = new HashSet<>(); - - private Builder() { - } + private List typeCodecProviders = new ArrayList<>(); + private EncodeNullHandlingStrategy.Strategy encodeNullHandlingStrategy = EncodeNullHandlingStrategy.Strategy.CODEC; + private DecodeUndefinedHandlingStrategy.Strategy decodeUndefinedHandlingStrategy = DecodeUndefinedHandlingStrategy.Strategy.KEEP_POJO_DEFAULT; + private boolean encodeNulls = false; public Builder setPackages(Set packages) { this.packages = packages; @@ -160,6 +168,34 @@ public Builder ignoreTypesAnnotatedWith(Class... annotatio return this; } + /** + * In case you need to register + * @param typeCodecProviders + * @return + */ + public Builder register(TypeCodecProvider... typeCodecProviders) { + this.typeCodecProviders.addAll(Arrays.asList(typeCodecProviders)); + return this; + } + + public Builder encodeNullHandlingStrategy(EncodeNullHandlingStrategy.Strategy encodeNullHandlingStrategy) { + if (encodeNullHandlingStrategy != null) { + this.encodeNullHandlingStrategy = encodeNullHandlingStrategy; + } + return this; + } + + public Builder decodeUndefinedHandlingStrategy(DecodeUndefinedHandlingStrategy.Strategy decodeUndefinedHandlingStrategy) { + if (decodeUndefinedHandlingStrategy != null) { + this.decodeUndefinedHandlingStrategy = decodeUndefinedHandlingStrategy; + } + return this; + } + + public Builder encodeNulls(boolean encodeNulls) { + this.encodeNulls = encodeNulls; + return this; + } /** * A CodecResolver is supposed to provide specialized codecs in case the default implementation * {@link BasicReflectionCodec} is not sufficient @@ -173,7 +209,12 @@ public Builder registerCodecResolver(CodecResolver... codecResolvers) { } public PojoCodecProvider build() { - return new PojoCodecProvider(classes, packages, ignoreAnnotations, codecResolvers); + CodecConfiguration codecConfiguration = CodecConfiguration.builder() + .decodeUndefinedHandlingStrategy(decodeUndefinedHandlingStrategy) + .encodeNullHandlingStrategy(encodeNullHandlingStrategy) + .encodeNulls(encodeNulls) + .build(); + return new PojoCodecProvider(classes, packages, ignoreAnnotations, typeCodecProviders, codecResolvers, codecConfiguration); } } } diff --git a/src/main/java/de/bild/codec/PojoContext.java b/src/main/java/de/bild/codec/PojoContext.java index c4a8838..1854629 100644 --- a/src/main/java/de/bild/codec/PojoContext.java +++ b/src/main/java/de/bild/codec/PojoContext.java @@ -18,7 +18,6 @@ import java.lang.reflect.Type; import java.lang.reflect.TypeVariable; import java.lang.reflect.WildcardType; -import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Set; @@ -34,10 +33,91 @@ public class PojoContext { private final Map> codecMap = new ConcurrentHashMap<>(); private final TypesModel typesModel; private final List codecResolvers; + private final List typeCodecProviders; + private final CodecConfiguration codecConfiguration; - public PojoContext(final TypesModel typesModel, List codecResolvers) { + /** + * The TypeCodecProvider for all known standard types + */ + private static final TypeCodecProvider DEFAULT_TYPE_CODEC_PROVIDER = new TypeCodecProvider() { + @Override + public Codec get(Type type, TypeCodecRegistry typeCodecRegistry) { + // byte arrays are handled well by the mongo java driver + if (TypeUtils.isArrayType(type)) { + 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); + } + // the default codecs provided by the mongo driver lack the decode method, hence this redefinition + else if (Float.class.equals(type)) { + return (Codec) FLOAT_CODEC; + } else if (Short.class.equals(type)) { + return (Codec) SHORT_CODEC; + } else if (Byte.class.equals(type)) { + return (Codec) BYTE_CODEC; + } + else if (TypeUtils.isAssignable(type, SpecialFieldsMap.class)) { + return new SpecialFieldsMapCodec(type, typeCodecRegistry); + } + + // List ? + Codec codec = ListTypeCodec.getCodecIfApplicable(type, typeCodecRegistry); + if (codec != null) { + return codec; + } + // Set ? + codec = SetTypeCodec.getCodecIfApplicable(type, typeCodecRegistry); + if (codec != null) { + return codec; + } + // Map ? + codec = MapTypeCodec.getCodecIfApplicable(type, typeCodecRegistry); + if (codec != null) { + return codec; + } + return null; + } + }; + + + public 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 = pojoContext.getCodec(type, this); + if (codec == null) { + codec = codecRegistry.get(ReflectionHelper.extractRawClass(type)); + } + return codec; + } + + @Override + public CodecRegistry getRegistry() { + return codecRegistry; + } } @@ -76,40 +156,37 @@ public synchronized Codec getCodec(Type type, TypeCodecRegistry typeCodec /** - * Iterates over the list of codecResolvers and returns a ReflectionCodec if match is found. + * 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 ReflectionCodec if responsible resolver si found */ - public synchronized ReflectionCodec resolve(Type type, TypeCodecRegistry typeCodecRegistry) { - ReflectionCodec codec; + public synchronized PolymorphicCodec resolve(Type type, TypeCodecRegistry typeCodecRegistry) { + PolymorphicCodec codec; for (CodecResolver codecResolver : codecResolvers) { - codec = codecResolver.getCodec(type, typeCodecRegistry); + codec = codecResolver.getCodec(type, typeCodecRegistry, codecConfiguration); if (codec != null) { return codec; } } + // enums are special - a PolymorphicCodec for enums can be build on the fly {@link EnumReflectionCodecWrapper} if (TypeUtils.isAssignable(type, Enum.class)) { return new EnumReflectionCodecWrapper(typeCodecRegistry.getCodec(type)); } // fallback is BasicReflectionCodec - return new BasicReflectionCodec(type, typeCodecRegistry); + return new BasicReflectionCodec(type, typeCodecRegistry, codecConfiguration); } - private static class EnumReflectionCodecWrapper> implements ReflectionCodec { + private static class EnumReflectionCodecWrapper> implements PolymorphicCodec { final Codec codec; public EnumReflectionCodecWrapper(Codec codec) { this.codec = codec; } - @Override - public Map getPersistenceFields() { - return Collections.emptyMap(); - } - - @Override public T decodeFields(BsonReader reader, DecoderContext decoderContext, T instance) { while (reader.readBsonType() != BsonType.END_OF_DOCUMENT) { String fieldName = reader.readName(); @@ -121,7 +198,6 @@ public T decodeFields(BsonReader reader, DecoderContext decoderContext, T instan reader.skipValue(); } } - // throw new EnumValueNotFoundException??? instead of returning null? return null; } @@ -133,44 +209,20 @@ public void encodeFields(BsonWriter writer, T instance, EncoderContext encoderCo } @Override - public void initializeDefaults(T instance) { - - } - - @Override - public MappedField getMappedField(String mappedFieldName) { + public T newInstance() { return null; } @Override - public MappedField getIdField() { - return null; + public Class getEncoderClass() { + return codec.getEncoderClass(); } @Override - public T decode(BsonReader reader, DecoderContext decoderContext) { - T newInstance; - if (reader.getCurrentBsonType() == null || reader.getCurrentBsonType() == BsonType.DOCUMENT) { - reader.readStartDocument(); - newInstance = decodeFields(reader, decoderContext, newInstance()); - reader.readEndDocument(); - return newInstance; - } else { - LOGGER.error("Expected to read document but reader is in state {}. Skipping value!", reader.getCurrentBsonType()); - reader.skipValue(); - return null; + public void verifyFieldsNotNamedLikeAnyDiscriminatorKey(Set discriminatorKeys) throws IllegalArgumentException { + if (discriminatorKeys.contains("enum")) { + throw new IllegalArgumentException("One of the discriminator keys equals the reserved word 'enum' " + discriminatorKeys); } - - } - - @Override - public void encode(BsonWriter bsonWriter, T t, EncoderContext encoderContext) { - codec.encode(bsonWriter, t, encoderContext); - } - - @Override - public Class getEncoderClass() { - return codec.getEncoderClass(); } } @@ -180,52 +232,34 @@ public Class getEncoderClass() { * @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) { - // first treat special types - // byte arrays are handled well by the mongo java driver + Codec codec; + + // first treat special types and custom codecs + for (TypeCodecProvider typeCodecProvider : typeCodecProviders) { + codec = typeCodecProvider.get(type, typeCodecRegistry); + if (codec != null) { + return codec; + } + } + + // byte[] will be handled by the mongo driver if (type.equals(byte[].class)) { return null; - } else if (TypeUtils.isArrayType(type)) { - 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); - } - // the default codecs provided by the mongo driver lack the decode method, hence this redefinition - else if (Float.class.equals(type)) { - return (Codec) FLOAT_CODEC; - } else if (Short.class.equals(type)) { - return (Codec) SHORT_CODEC; - } else if (Byte.class.equals(type)) { - return (Codec) BYTE_CODEC; } // enums will be handled by a general enum codec registered as CodecProvider within CodecRegistry else if (TypeUtils.isAssignable(type, Enum.class)) { return null; - } else if (TypeUtils.isAssignable(type, SpecialFieldsMap.class)) { - return new SpecialFieldsMapCodec(type, typeCodecRegistry); } - // List ? - Codec codec = ListTypeCodec.getCodecIfApplicable(type, typeCodecRegistry); - if (codec != null) { - return codec; - } - // Set ? - codec = SetTypeCodec.getCodecIfApplicable(type, typeCodecRegistry); - if (codec != null) { - return codec; - } - // Map ? - codec = MapTypeCodec.getCodecIfApplicable(type, typeCodecRegistry); + // standard codec available ? + codec = DEFAULT_TYPE_CODEC_PROVIDER.get(type, typeCodecRegistry); if (codec != null) { return codec; } /** - * now try pojo codec + * now try pojo codec with potentially polymorphic structures */ Set validTypesForType = typesModel.getAssignableTypesWithinClassHierarchy(type); @@ -241,10 +275,9 @@ else if (TypeUtils.isAssignable(type, Enum.class)) { codec = resolve(singleType, typeCodecRegistry); if (codec != null) { return codec; - } else { - return new BasicReflectionCodec<>(singleType, typeCodecRegistry); } } + return null; } @@ -275,35 +308,6 @@ private boolean isClassPartOfPolymorphicStructureWithinTypesModel(Class clazz) { return false; } - - /** - * 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 = pojoContext.getCodec(type, this); - if (codec == null) { - codec = codecRegistry.get(ReflectionHelper.extractRawClass(type)); - } - return codec; - } - - @Override - public CodecRegistry getRegistry() { - return codecRegistry; - } - } - - /** * Class is used internally to detect cycles. */ @@ -364,4 +368,4 @@ public Short decode(BsonReader reader, DecoderContext decoderContext) { return (short) reader.readInt32(); } } -} +} \ No newline at end of file diff --git a/src/main/java/de/bild/codec/PolymorphicCodec.java b/src/main/java/de/bild/codec/PolymorphicCodec.java new file mode 100644 index 0000000..653c4e4 --- /dev/null +++ b/src/main/java/de/bild/codec/PolymorphicCodec.java @@ -0,0 +1,74 @@ +package de.bild.codec; + +import org.bson.BsonReader; +import org.bson.BsonType; +import org.bson.BsonWriter; +import org.bson.codecs.DecoderContext; +import org.bson.codecs.EncoderContext; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Set; + +/** + * + * Provides functionality for handling polymorphic structures. + * decode() as well as encode() have default implementations within this interface. + * * + * @param the value type + */ +public interface PolymorphicCodec extends TypeCodec { + Logger LOGGER = LoggerFactory.getLogger(PolymorphicCodec.class); + + /* + * When encoding polymorphic types, a discriminator must be written to the database along with the instance. + * Instead of restructuring the json within the database, the discriminator key/value will be written at the same + * json level as the instance data. Nesting the instance data in a deeper level may complicate things in the database, + * especially if you store non-polymorphic entities together with polymorphic entities in one collection and if + * you need to set an index at a specific field these entities have in common. + * + * Hence provide methods for encoding all entity fields as well as one for decoding just the fields. The wrapping + * curly brackets will be written along with the discriminator in encode() + * + */ + T decodeFields(BsonReader reader, DecoderContext decoderContext, T instance); + + void encodeFields(BsonWriter writer, T instance, EncoderContext encoderContext); + + T newInstance(); + + @Override + default T decode(BsonReader reader, DecoderContext decoderContext) { + T newInstance; + if (reader.getCurrentBsonType() == null || reader.getCurrentBsonType() == BsonType.DOCUMENT) { + reader.readStartDocument(); + newInstance = decodeFields(reader, decoderContext, newInstance()); + reader.readEndDocument(); + return newInstance; + } else { + LOGGER.error("Expected to read document but reader is in state {}. Skipping value!", reader.getCurrentBsonType()); + reader.skipValue(); + return null; + } + } + + @Override + default void encode(BsonWriter writer, T value, EncoderContext encoderContext) { + if (value == null) { + writer.writeNull(); + } + else { + writer.writeStartDocument(); + encodeFields(writer, value, encoderContext); + writer.writeEndDocument(); + } + } + + /** + * A check for properties with names equal to any of the identified discriminator keys + * @param discriminatorKeys the identified discriminator keys + * throws an {@link IllegalArgumentException} if a name of an internally used property is equal to one of the discriminator keys + */ + void verifyFieldsNotNamedLikeAnyDiscriminatorKey(Set discriminatorKeys) throws IllegalArgumentException; + +} \ No newline at end of file diff --git a/src/main/java/de/bild/codec/PolymorphicReflectionCodec.java b/src/main/java/de/bild/codec/PolymorphicReflectionCodec.java index c6ac9b7..dcba322 100644 --- a/src/main/java/de/bild/codec/PolymorphicReflectionCodec.java +++ b/src/main/java/de/bild/codec/PolymorphicReflectionCodec.java @@ -20,12 +20,12 @@ public class PolymorphicReflectionCodec implements TypeCodec { private static final Logger LOGGER = LoggerFactory.getLogger(PolymorphicReflectionCodec.class); final Class clazz; - final Map discriminatorToCodec = new HashMap<>(); - final Map, ReflectionCodec> classToCodec = new HashMap<>(); + 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<>(); - ReflectionCodec fallBackCodec; + PolymorphicCodec fallBackCodec; final boolean isCollectible; public PolymorphicReflectionCodec(Type type, Set validTypes, TypeCodecRegistry typeCodecRegistry, PojoContext pojoContext) { @@ -42,7 +42,7 @@ public PolymorphicReflectionCodec(Type type, Set validTypes, TypeCodecRegi discriminatorKeys.putIfAbsent(clazz, discriminatorKey); allDiscriminatorKeys.add(discriminatorKey); - ReflectionCodec codecFor = pojoContext.resolve(validType, typeCodecRegistry); + PolymorphicCodec codecFor = pojoContext.resolve(validType, typeCodecRegistry); if (isFallBack) { if (fallBackCodec != null) { @@ -75,7 +75,7 @@ public PolymorphicReflectionCodec(Type type, Set validTypes, TypeCodecRegi for (String discriminator : allDiscriminators) { - ReflectionCodec registeredCodec = this.discriminatorToCodec.putIfAbsent(discriminator, codecFor); + 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); @@ -85,19 +85,12 @@ public PolymorphicReflectionCodec(Type type, Set validTypes, TypeCodecRegi } } - //check for properties within classes that are named exacly like one of the used main discrimimnator keys - for (ReflectionCodec usedCodec : classToCodec.values()) { - for (String allDiscriminatorKey : allDiscriminatorKeys) { - MappedField mappedField = usedCodec.getMappedField(allDiscriminatorKey); - if (mappedField != null) { - LOGGER.error("A field {} within {} is named like one of the discriminator keys {}", mappedField.getMappedFieldName(), usedCodec.getEncoderClass(), allDiscriminatorKeys); - throw new IllegalArgumentException("A field " + mappedField.getMappedFieldName() + " within " + usedCodec.getEncoderClass() + " is named like one of the discriminator keys " + allDiscriminatorKeys); - - } - } + //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); } - + // 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); @@ -113,10 +106,15 @@ private String getDiscriminatorKeyForClass(Class clazz) { @Override public T decode(BsonReader reader, DecoderContext decoderContext) { + if (reader.getCurrentBsonType() == BsonType.NULL) { + reader.readNull(); + return null; + } + String discriminator = null; reader.mark(); reader.readStartDocument(); - ReflectionCodec codec = null; + PolymorphicCodec codec = null; while (reader.readBsonType() != BsonType.END_OF_DOCUMENT) { String fieldName = reader.readName(); if (allDiscriminatorKeys.contains(fieldName)) { @@ -171,30 +169,31 @@ public T decode(BsonReader reader, DecoderContext decoderContext) { } - protected T decodeWithType(BsonReader reader, DecoderContext decoderContext, ReflectionCodec typeCodec) { - return typeCodec.decode(reader, decoderContext); + protected T decodeWithType(BsonReader reader, DecoderContext decoderContext, PolymorphicCodec polymorphicCodec) { + return polymorphicCodec.decode(reader, decoderContext); } @Override public void encode(BsonWriter writer, T value, EncoderContext encoderContext) { - writer.writeStartDocument(); - encodeFields(writer, value, encoderContext); - writer.writeEndDocument(); - } - - public void encodeFields(BsonWriter writer, T value, EncoderContext encoderContext) { - ReflectionCodec codecForValue = getCodecForClass(value.getClass()); - if (codecForValue != null) { - writer.writeName(discriminatorKeys.get(codecForValue.getEncoderClass())); - writer.writeString(mainDiscriminators.get(codecForValue.getEncoderClass())); - encodeType(writer, value, encoderContext, codecForValue); - } else { - LOGGER.warn("The value to be encoded has the wrong type {}. This codec can only handle {}", value.getClass(), discriminatorToCodec); + 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 ReflectionCodec getCodecForDiscriminator(String discriminator) { + private PolymorphicCodec getCodecForDiscriminator(String discriminator) { if (discriminator == null) { LOGGER.warn("Discriminator key cannot be null."); return null; @@ -207,25 +206,22 @@ private ReflectionCodec getCodecForDiscriminator(String discriminator) { * * @return a codec responsible for a valid class within the class hierarchy */ - public ReflectionCodec getCodecForClass(Class clazz) { + public PolymorphicCodec getCodecForClass(Class clazz) { if (clazz == null || Object.class.equals(clazz)) { return null; } - ReflectionCodec codec = classToCodec.get(clazz); + PolymorphicCodec codec = classToCodec.get(clazz); if (codec != null) { return codec; } return getCodecForClass(clazz.getSuperclass()); } - private ReflectionCodec getCodecForValue(T document) { + private PolymorphicCodec getCodecForValue(T document) { return getCodecForClass(document.getClass()); } - protected void encodeType(BsonWriter writer, T value, EncoderContext encoderContext, ReflectionCodec typeCodec) { - typeCodec.encodeFields(writer, value, encoderContext); - } @Override public Class getEncoderClass() { @@ -239,7 +235,7 @@ public boolean isCollectible() { @Override public T generateIdIfAbsentFromDocument(T document) { - ReflectionCodec codecForValue = getCodecForValue(document); + PolymorphicCodec codecForValue = getCodecForValue(document); if (codecForValue != null) { codecForValue.generateIdIfAbsentFromDocument(document); } @@ -248,7 +244,7 @@ public T generateIdIfAbsentFromDocument(T document) { @Override public boolean documentHasId(T document) { - ReflectionCodec codecForValue = getCodecForValue(document); + PolymorphicCodec codecForValue = getCodecForValue(document); if (codecForValue != null) { return codecForValue.documentHasId(document); } @@ -257,7 +253,7 @@ public boolean documentHasId(T document) { @Override public BsonValue getDocumentId(T document) { - ReflectionCodec codecForValue = getCodecForValue(document); + PolymorphicCodec codecForValue = getCodecForValue(document); if (codecForValue != null) { return codecForValue.getDocumentId(document); } diff --git a/src/main/java/de/bild/codec/ReflectionCodec.java b/src/main/java/de/bild/codec/ReflectionCodec.java index bb6ce5f..38b0ad7 100644 --- a/src/main/java/de/bild/codec/ReflectionCodec.java +++ b/src/main/java/de/bild/codec/ReflectionCodec.java @@ -1,34 +1,39 @@ package de.bild.codec; -import org.bson.BsonReader; -import org.bson.BsonWriter; -import org.bson.codecs.DecoderContext; -import org.bson.codecs.EncoderContext; - import java.util.Map; +import java.util.Set; + /** - * - * @param the value type + * Used by Polymorphia internally to tag codecs that use reflection to build a Codec for al properties + * @param */ -public interface ReflectionCodec extends TypeCodec { +public interface ReflectionCodec extends PolymorphicCodec { Map getPersistenceFields(); - T decodeFields(BsonReader reader, DecoderContext decoderContext, T instance); - - void encodeFields(BsonWriter writer, T instance, EncoderContext encoderContext); - - default void postDecode(T instance) { - // do nothing - } - - default void preEncode(T instance) { - // solely meant to not introduce braking changes - } - - void initializeDefaults(T instance); - MappedField getMappedField(String mappedFieldName); - MappedField getIdField(); -} + /** + * Called after entity has been decoded + * @param instance + */ + void postDecode(T instance); + + /** + * Called just before encoding + * @param instance + */ + void preEncode(T instance); + + @Override + default void verifyFieldsNotNamedLikeAnyDiscriminatorKey(Set propertyNames) throws IllegalArgumentException { + for (String propertyName : propertyNames) { + MappedField mappedField = getMappedField(propertyName); + if (mappedField != null) { + LOGGER.error("A field {} within {} is named like one of the discriminator keys {}", mappedField.getMappedFieldName(), getEncoderClass(), propertyNames); + throw new IllegalArgumentException("A field " + mappedField.getMappedFieldName() + " within " + getEncoderClass() + " is named like one of the discriminator keys " + propertyNames); + + } + } + } +} \ No newline at end of file diff --git a/src/main/java/de/bild/codec/ReflectionHelper.java b/src/main/java/de/bild/codec/ReflectionHelper.java index a00aafa..05c8604 100644 --- a/src/main/java/de/bild/codec/ReflectionHelper.java +++ b/src/main/java/de/bild/codec/ReflectionHelper.java @@ -26,6 +26,22 @@ public static List getDeclaredAndInheritedFieldTypePairs(final Ty return list; } + /** + * Finds a field with fully resolved type within the given type + * @param type any type generic or simply a class + * @param fieldName a name of a field within that type + * @return null, or the resolved FieldTypePair + */ + public static FieldTypePair getDeclaredAndInheritedFieldTypePair(final Type type, String fieldName) { + List declaredAndInheritedFieldTypePairs = getDeclaredAndInheritedFieldTypePairs(type, true); + for (FieldTypePair declaredAndInheritedFieldTypePair : declaredAndInheritedFieldTypePairs) { + if (fieldName.equals(declaredAndInheritedFieldTypePair.getField().getName())) { + return declaredAndInheritedFieldTypePair; + } + } + return null; + } + private static void getFieldTypePairsRecursive(final Type type, final boolean returnFinalFields, List currentList, Map realTypeMap) { if (type instanceof ParameterizedType) { getFieldTypePairsRecursive((ParameterizedType) type, returnFinalFields, currentList, realTypeMap); diff --git a/src/main/java/de/bild/codec/SetTypeCodec.java b/src/main/java/de/bild/codec/SetTypeCodec.java index 3ec81b4..2f8887d 100644 --- a/src/main/java/de/bild/codec/SetTypeCodec.java +++ b/src/main/java/de/bild/codec/SetTypeCodec.java @@ -5,6 +5,8 @@ import java.lang.reflect.Type; import java.util.LinkedHashSet; import java.util.Set; +import java.util.SortedSet; +import java.util.TreeSet; /** * @@ -19,6 +21,9 @@ public SetTypeCodec(Class collectionClass, Type valueType, TypeCodecRegistry @Override protected Constructor getDefaultConstructor(Class clazz) { if (clazz.isInterface()) { + if (SortedSet.class.isAssignableFrom(clazz)) { + return super.getDefaultConstructor((Class) TreeSet.class); + } return super.getDefaultConstructor((Class) LinkedHashSet.class); } return super.getDefaultConstructor(clazz); diff --git a/src/main/java/de/bild/codec/SpecialFieldsMapCodec.java b/src/main/java/de/bild/codec/SpecialFieldsMapCodec.java index d6c1960..e429a1e 100644 --- a/src/main/java/de/bild/codec/SpecialFieldsMapCodec.java +++ b/src/main/java/de/bild/codec/SpecialFieldsMapCodec.java @@ -84,23 +84,28 @@ private List readList(final BsonReader reader, final DecoderContext deco @Override public void encode(BsonWriter writer, T map, EncoderContext encoderContext) { - writer.writeStartDocument(); - for (Map.Entry entry : map.entrySet()) { - writer.writeName(entry.getKey()); - Object value = entry.getValue(); + if (map == null) { + writer.writeNull(); + } + else { + writer.writeStartDocument(); + for (Map.Entry entry : map.entrySet()) { + writer.writeName(entry.getKey()); + Object value = entry.getValue(); - Codec fieldMappingCodec = fieldMappingCodecs.get(entry.getKey()); - if (fieldMappingCodec != null) { - fieldMappingCodec.encode(writer, value, encoderContext); - } else { - if (value != null) { - Codec codec = codecRegistry.get(value.getClass()); - codec.encode(writer, value, encoderContext); + Codec fieldMappingCodec = fieldMappingCodecs.get(entry.getKey()); + if (fieldMappingCodec != null) { + fieldMappingCodec.encode(writer, value, encoderContext); } else { - writer.writeNull(); + if (value != null) { + Codec codec = codecRegistry.get(value.getClass()); + codec.encode(writer, value, encoderContext); + } else { + writer.writeNull(); + } } } + writer.writeEndDocument(); } - writer.writeEndDocument(); } } \ No newline at end of file diff --git a/src/main/java/de/bild/codec/TypeCodec.java b/src/main/java/de/bild/codec/TypeCodec.java index b9f3d25..b4b9fea 100644 --- a/src/main/java/de/bild/codec/TypeCodec.java +++ b/src/main/java/de/bild/codec/TypeCodec.java @@ -6,15 +6,17 @@ /** - * All codecs within polymorphia would implement this interface. + * All codecs used within polymorphia need to implement this interface. + * + * * @param the value type */ public interface TypeCodec extends Codec { - default T newInstance() { - return null; - } - + /** + * Override this method if your Codec needs to supply default values as replacements for null values. + * @return null or a default value + */ default T defaultInstance() { return null; } diff --git a/src/main/java/de/bild/codec/TypeCodecProvider.java b/src/main/java/de/bild/codec/TypeCodecProvider.java new file mode 100644 index 0000000..cb6f044 --- /dev/null +++ b/src/main/java/de/bild/codec/TypeCodecProvider.java @@ -0,0 +1,18 @@ +package de.bild.codec; + +import org.bson.codecs.Codec; + +import java.lang.reflect.Type; + + +/** + * The mongo driver {@link org.bson.codecs.configuration.CodecProvider} was not designed to accept {@link Type} as parameter + * This {@link TypeCodecProvider} can be used to register {@link Codec}s for any given {@link Type} + * Register your {@link TypeCodecProvider} when building {@link PojoCodecProvider.Builder#register(TypeCodecProvider...)} + * + * TypeCodecProvider helps you to handle a given fields within your Pojos as special way. + */ +public interface TypeCodecProvider { + + Codec get(Type type, TypeCodecRegistry typeCodecRegistry); +} diff --git a/src/main/java/de/bild/codec/annotations/DecodeUndefinedHandlingStrategy.java b/src/main/java/de/bild/codec/annotations/DecodeUndefinedHandlingStrategy.java new file mode 100644 index 0000000..0acba68 --- /dev/null +++ b/src/main/java/de/bild/codec/annotations/DecodeUndefinedHandlingStrategy.java @@ -0,0 +1,40 @@ +package de.bild.codec.annotations; + +import de.bild.codec.TypeCodec; + +import java.lang.annotation.*; + + +/** + * This strategy can be used to influence field values while decoding if no value is found in the database. + * This comes in quite handy if your data model evolves and you find properties not to be encoded for old Pojos. + * + * A global default value can be set via {@link de.bild.codec.PojoCodecProvider.Builder#decodeUndefinedHandlingStrategy(Strategy)} + * If not set, default is {@link DecodeUndefinedHandlingStrategy.Strategy#KEEP_POJO_DEFAULT} (due to historical behaviour of {@link de.bild.codec.PojoCodecProvider}) + * + * + * You can use this annotation at class level or at field level. If you use it at class level, you can override each field with + * a field level annotation. + * + *
    + *
  • {@link Strategy#SET_TO_NULL} sets the value of the field to null, even if the pojo provides a default value
  • + *
  • {@link Strategy#CODEC} {@link TypeCodec#defaultInstance()} is being used to initialize the field, even if the pojo provides a default value
  • + *
  • {@link Strategy#KEEP_POJO_DEFAULT} keeps the pojo default, could be null or anything set during creation (constructor or default initialization of the field)
  • + *
+ * + + */ +@Documented +@Inherited +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.FIELD, ElementType.TYPE}) +public @interface DecodeUndefinedHandlingStrategy { + Strategy value(); + + enum Strategy { + SET_TO_NULL, + CODEC, + KEEP_POJO_DEFAULT; + } + +} \ No newline at end of file diff --git a/src/main/java/de/bild/codec/annotations/EncodeNullHandlingStrategy.java b/src/main/java/de/bild/codec/annotations/EncodeNullHandlingStrategy.java new file mode 100644 index 0000000..edc9952 --- /dev/null +++ b/src/main/java/de/bild/codec/annotations/EncodeNullHandlingStrategy.java @@ -0,0 +1,42 @@ +package de.bild.codec.annotations; + +import de.bild.codec.TypeCodec; + +import java.lang.annotation.*; + +/** + * Use this annotation to specify the handling of null values prior to encoding to the database. + * This annotation is thought for convenience. + * You can use it at class level or at field level. If you use it at class level, you can override each field with + * a field level annotation. + * + * A global default value can be set via {@link de.bild.codec.PojoCodecProvider.Builder#encodeNullHandlingStrategy(Strategy)} + * If not set, default is {@link Strategy#CODEC} (due to historical behaviour of {@link de.bild.codec.PojoCodecProvider}) + * + * You can e.g. make sure, that lists fields that are null are always encoded as empty lists, if desired. + * + * Right now, two strategies exist: + *
    + *
  • {@link Strategy#CODEC} : If null is found then {@link TypeCodec#defaultInstance()} is being used to generate a default value e.g. empty list/set/map
  • + *
  • {@link Strategy#KEEP_NULL} : keep null
  • + *
+ * + * + * + * For future improvements more strategies could be added, e.g. one strategy could be to register + * a default-value-generator at the field. + * + * + */ +@Documented +@Inherited +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.FIELD, ElementType.TYPE}) +public @interface EncodeNullHandlingStrategy { + Strategy value(); + + enum Strategy { + CODEC, + KEEP_NULL + } +} \ No newline at end of file diff --git a/src/main/java/de/bild/codec/annotations/EncodeNulls.java b/src/main/java/de/bild/codec/annotations/EncodeNulls.java new file mode 100644 index 0000000..d308708 --- /dev/null +++ b/src/main/java/de/bild/codec/annotations/EncodeNulls.java @@ -0,0 +1,19 @@ +package de.bild.codec.annotations; + +import java.lang.annotation.*; + +/** + * If you need nulls to be written out to the database, use this annotation either at pojo class level or at field level. + * Please note: If you set EncodeNulls=false and decode entities with undefined values within the mongo database, + * the {@link DecodeUndefinedHandlingStrategy} will influence the decoded value + * + * Global behaviour can be set during registration of {@link de.bild.codec.PojoCodecProvider.Builder#encodeNulls(boolean)} + * + */ +@Documented +@Inherited +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.FIELD, ElementType.TYPE}) +public @interface EncodeNulls { + boolean value() default true; +} \ No newline at end of file diff --git a/src/main/java/de/bild/codec/annotations/Id.java b/src/main/java/de/bild/codec/annotations/Id.java index 6618ac7..61fd4eb 100644 --- a/src/main/java/de/bild/codec/annotations/Id.java +++ b/src/main/java/de/bild/codec/annotations/Id.java @@ -12,7 +12,6 @@ @Inherited @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.FIELD) - public @interface Id { Class value() default ObjectIdGenerator.class; diff --git a/src/test/java/de/bild/codec/CodecResolverTest.java b/src/test/java/de/bild/codec/CodecResolverTest.java index 689be06..32fe310 100644 --- a/src/test/java/de/bild/codec/CodecResolverTest.java +++ b/src/test/java/de/bild/codec/CodecResolverTest.java @@ -3,7 +3,6 @@ import com.mongodb.MongoClient; import com.mongodb.client.MongoCollection; import com.mongodb.client.model.Filters; -import de.bild.codec.annotations.DiscriminatorKey; import de.bild.codec.annotations.Id; import org.apache.commons.lang3.reflect.TypeUtils; import org.bson.BsonReader; @@ -31,7 +30,6 @@ import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import java.io.StringWriter; -import java.lang.reflect.Type; import java.util.Date; import java.util.Map; @@ -53,9 +51,9 @@ public static CodecRegistry getCodecRegistry() { new EnumCodecProvider(), PojoCodecProvider.builder() .register(CodecResolverTest.class) - .registerCodecResolver((CodecResolver) (type, typeCodecRegistry) -> { + .registerCodecResolver((CodecResolver) (type, typeCodecRegistry, codecConfiguration) -> { if (TypeUtils.isAssignable(type, Base.class)) { - return new DocumentCodec((Class) type, typeCodecRegistry); + return new DocumentCodec((Class) type, typeCodecRegistry, codecConfiguration); } return null; }) @@ -155,13 +153,13 @@ public int hashCode() { static class DocumentCodec extends BasicReflectionCodec { final ReflectionCodec documentMetaCodec; - public DocumentCodec(Class type, TypeCodecRegistry typeCodecRegistry) { - super(type, typeCodecRegistry); + public DocumentCodec(Class type, TypeCodecRegistry typeCodecRegistry, CodecConfiguration codecConfiguration) { + super(type, typeCodecRegistry, codecConfiguration); MappedField mappedField = getMappedField("meta"); Codec metaCodec = mappedField.getCodec(); if (metaCodec instanceof PolymorphicReflectionCodec) { PolymorphicReflectionCodec polymorphicMetaCodec = (PolymorphicReflectionCodec) metaCodec; - this.documentMetaCodec = polymorphicMetaCodec.getCodecForClass(mappedField.getField().getType()); + this.documentMetaCodec = (ReflectionCodec)polymorphicMetaCodec.getCodecForClass(mappedField.getField().getType()); } else { this.documentMetaCodec = (ReflectionCodec) metaCodec; } diff --git a/src/test/java/de/bild/codec/ListTypeCodecTest.java b/src/test/java/de/bild/codec/ListTypeCodecTest.java index 427c74a..f9d4dc4 100644 --- a/src/test/java/de/bild/codec/ListTypeCodecTest.java +++ b/src/test/java/de/bild/codec/ListTypeCodecTest.java @@ -19,8 +19,8 @@ import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import java.io.StringWriter; -import java.util.ArrayList; -import java.util.List; +import java.lang.reflect.Constructor; +import java.util.*; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.instanceOf; @@ -57,6 +57,11 @@ static class EncodingPojo { } + static class SetPojo { + SortedSet integerSortedSet; + Set integerSet; + } + /** * Testing if List can be decoded into a */ @@ -82,4 +87,13 @@ public void testResilience() { Assert.assertNotNull(decodingPojo.someList); assertThat(decodingPojo.someList, instanceOf(ListOfStrings.class)); } + + @Test + public void testDifferentTypes() { + Codec setPojoCodec = codecRegistry.get(SetPojo.class); + Assert.assertTrue(setPojoCodec instanceof BasicReflectionCodec); + BasicReflectionCodec basicReflectionCodec = (BasicReflectionCodec)setPojoCodec; + Constructor> constructor = ((SetTypeCodec, Integer>) basicReflectionCodec.getMappedField("integerSortedSet").getCodec()).defaultConstructor; + Assert.assertTrue(TreeSet.class.equals(constructor.getDeclaringClass())); + } } diff --git a/src/test/java/de/bild/codec/NullHandlingTest.java b/src/test/java/de/bild/codec/NullHandlingTest.java new file mode 100644 index 0000000..41f1f5e --- /dev/null +++ b/src/test/java/de/bild/codec/NullHandlingTest.java @@ -0,0 +1,136 @@ +package de.bild.codec; + +import com.mongodb.MongoClient; +import de.bild.codec.annotations.DecodeUndefinedHandlingStrategy; +import de.bild.codec.annotations.EncodeNullHandlingStrategy; +import de.bild.codec.annotations.EncodeNulls; +import lombok.EqualsAndHashCode; +import org.bson.codecs.Codec; +import org.bson.codecs.DecoderContext; +import org.bson.codecs.EncoderContext; +import org.bson.codecs.configuration.CodecRegistries; +import org.bson.codecs.configuration.CodecRegistry; +import org.bson.json.JsonReader; +import org.bson.json.JsonWriter; +import org.bson.json.JsonWriterSettings; +import org.hamcrest.MatcherAssert; +import org.json.JSONException; +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.skyscreamer.jsonassert.JSONAssert; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +import java.io.StringWriter; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.SortedSet; + +import static org.hamcrest.Matchers.empty; + +@RunWith(SpringJUnit4ClassRunner.class) +@SpringBootTest(classes = NullHandlingTest.class) +@ComponentScan(basePackages = "de.bild") +public class NullHandlingTest { + private static final Logger LOGGER = LoggerFactory.getLogger(NullHandlingTest.class); + + + static class Config { + @Bean() + public static CodecRegistry getCodecRegistry() { + return CodecRegistries.fromRegistries( + CodecRegistries.fromProviders( + new EnumCodecProvider(), + PojoCodecProvider.builder() + .register(NullHandlingTest.class) + .encodeNulls(false) + .decodeUndefinedHandlingStrategy(DecodeUndefinedHandlingStrategy.Strategy.KEEP_POJO_DEFAULT) + .encodeNullHandlingStrategy(EncodeNullHandlingStrategy.Strategy.KEEP_NULL) + .build() + ), + MongoClient.getDefaultCodecRegistry()); + } + } + + @Autowired + CodecRegistry codecRegistry; + + interface SomeInterface {} + + @EncodeNulls(true) + @EqualsAndHashCode + @DecodeUndefinedHandlingStrategy(DecodeUndefinedHandlingStrategy.Strategy.CODEC) + static class PojoProperty implements SomeInterface { + SortedSet sortedSet; + } + + @EqualsAndHashCode + @EncodeNulls(false) + static class BasePojo { + String aString; + + @EncodeNulls(false) + List encodeNullsFalse; + + @EncodeNulls(true) + List encodeNullsTrue; + + @EncodeNullHandlingStrategy(EncodeNullHandlingStrategy.Strategy.CODEC) + List encodeNullHandlingStrategy_CODEC; + + @DecodeUndefinedHandlingStrategy(DecodeUndefinedHandlingStrategy.Strategy.CODEC) + @EncodeNulls(false) //EncodeNullHandlingStrategy.Strategy.KEEP_NULL + List encodeNullsFalseDecodeUndefined_CODEC = Arrays.asList(new PojoProperty(), new PojoProperty()); + + @DecodeUndefinedHandlingStrategy(DecodeUndefinedHandlingStrategy.Strategy.KEEP_POJO_DEFAULT) + @EncodeNulls(false)//EncodeNullHandlingStrategy.Strategy.KEEP_NULL + List encodeNullsFalseDecodeUndefined_KEEP_POJO_DEFAULT = Arrays.asList(new PojoProperty()); + + + @DecodeUndefinedHandlingStrategy(DecodeUndefinedHandlingStrategy.Strategy.KEEP_POJO_DEFAULT) + @EncodeNulls + List encodeNullsShouldDecodeToNull = new ArrayList<>(Arrays.asList(null, new PojoProperty(), null, null)); + + } + + @Test + public void basicTest() throws JSONException { + BasePojo basePojo = new BasePojo(); + + basePojo.encodeNullsFalseDecodeUndefined_CODEC = null; // encode to undefined + basePojo.encodeNullsFalseDecodeUndefined_KEEP_POJO_DEFAULT = null; // encode with null value set + basePojo.encodeNullsShouldDecodeToNull = null; // encode with null value set + + Codec primitivePojoCodec = codecRegistry.get(BasePojo.class); + + StringWriter stringWriter = new StringWriter(); + JsonWriter writer = new JsonWriter(stringWriter, new JsonWriterSettings(true)); + primitivePojoCodec.encode(writer, basePojo, EncoderContext.builder().build()); + LOGGER.info("The encoded json looks like: {}", stringWriter); + + BasePojo readBasePojo = primitivePojoCodec.decode(new JsonReader(stringWriter.toString()), DecoderContext.builder().build()); + + JSONAssert.assertEquals(stringWriter.toString(), "{\n" + + " \"encodeNullsTrue\" : null,\n" + + " \"encodeNullHandlingStrategy_CODEC\" : [],\n" + + " \"encodeNullsShouldDecodeToNull\" : null\n" + + "}", true); + + + Assert.assertNull(readBasePojo.encodeNullsFalse); + Assert.assertNull(readBasePojo.aString); + Assert.assertNull(readBasePojo.encodeNullsTrue); + + MatcherAssert.assertThat(readBasePojo.encodeNullHandlingStrategy_CODEC, empty()); + MatcherAssert.assertThat(readBasePojo.encodeNullsFalseDecodeUndefined_CODEC, empty()); + Assert.assertEquals(readBasePojo.encodeNullsFalseDecodeUndefined_KEEP_POJO_DEFAULT, Arrays.asList(new PojoProperty())); + Assert.assertNull(readBasePojo.encodeNullsShouldDecodeToNull); + } +} \ No newline at end of file diff --git a/src/test/java/de/bild/codec/idspecialcodecresolver/ExternalIdCodecProviderTest.java b/src/test/java/de/bild/codec/idspecialcodecresolver/ExternalIdCodecProviderTest.java index a19d8ab..91f962d 100644 --- a/src/test/java/de/bild/codec/idspecialcodecresolver/ExternalIdCodecProviderTest.java +++ b/src/test/java/de/bild/codec/idspecialcodecresolver/ExternalIdCodecProviderTest.java @@ -39,9 +39,9 @@ public static CodecRegistry getCodecRegistry() { new EnumCodecProvider(), PojoCodecProvider.builder() .register(Pojo.class.getPackage().getName()) - .registerCodecResolver((CodecResolver) (type, typeCodecRegistry) -> { + .registerCodecResolver((CodecResolver) (type, typeCodecRegistry, codecConfiguration) -> { if (TypeUtils.isAssignable(type, CustomId.class)) { - return new CustomIdCodec((Class)type, typeCodecRegistry); + return new CustomIdCodec((Class)type, typeCodecRegistry, codecConfiguration); } return null; }).build() @@ -60,8 +60,8 @@ public CustomId generate() { } static class CustomIdCodec extends BasicReflectionCodec { - public CustomIdCodec(Class type, TypeCodecRegistry typeCodecRegistry) { - super(type, typeCodecRegistry); + public CustomIdCodec(Class type, TypeCodecRegistry typeCodecRegistry, CodecConfiguration codecConfiguration) { + super(type, typeCodecRegistry, codecConfiguration); } @Override @@ -75,11 +75,6 @@ public void encodeFields(BsonWriter writer, T instance, EncoderContext encoderCo // do some custom stuff here. as an example have a look at super.encodeFields(writer, instance, encoderContext) super.encodeFields(writer, instance, encoderContext); } - - @Override - public T defaultInstance() { - return null; - } } diff --git a/src/test/java/de/bild/codec/idtypemismatch/ExternalIdCodecProviderTest.java b/src/test/java/de/bild/codec/idtypemismatch/ExternalIdCodecProviderTest.java new file mode 100644 index 0000000..e38b6b7 --- /dev/null +++ b/src/test/java/de/bild/codec/idtypemismatch/ExternalIdCodecProviderTest.java @@ -0,0 +1,112 @@ +package de.bild.codec.idtypemismatch; + +import com.mongodb.MongoClient; +import com.mongodb.client.MongoCollection; +import com.mongodb.client.model.Filters; +import de.bild.codec.EnumCodecProvider; +import de.bild.codec.IdGenerationException; +import de.bild.codec.IdGenerator; +import de.bild.codec.PojoCodecProvider; +import de.bild.codec.annotations.IgnoreType; +import de.bild.codec.idtypemismatch.model.CustomId; +import de.bild.codec.idtypemismatch.model.Pojo; +import org.bson.BsonReader; +import org.bson.BsonWriter; +import org.bson.codecs.Codec; +import org.bson.codecs.DecoderContext; +import org.bson.codecs.EncoderContext; +import org.bson.codecs.configuration.CodecProvider; +import org.bson.codecs.configuration.CodecRegistries; +import org.bson.codecs.configuration.CodecRegistry; +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +import java.util.Random; + +@RunWith(SpringJUnit4ClassRunner.class) +@SpringBootTest(classes = ExternalIdCodecProviderTest.class) +@EnableAutoConfiguration +@ComponentScan(basePackages = "de.bild") +public class ExternalIdCodecProviderTest { + private static final Random RANDOM = new Random(); + + static class Config { + @Bean() + public static CodecRegistry getCodecRegistry() { + return CodecRegistries.fromRegistries( + CodecRegistries.fromProviders( + new EnumCodecProvider(), + new CustomIdCodecProvider(), + PojoCodecProvider.builder() + .register(Pojo.class.getPackage().getName()) + .ignoreTypesAnnotatedWith(IgnoreType.class) + .build() + ), + MongoClient.getDefaultCodecRegistry()); + } + } + + @SuppressWarnings("unchecked") + public static class CustomIdCodecProvider implements CodecProvider { + @Override + public Codec get(Class clazz, CodecRegistry codecRegistry) { + if (CustomId.class.isAssignableFrom(clazz)) { + return (Codec)new CustomIdCodec(); + } + return null; + } + } + + static class CustomIdCodec implements Codec { + @Override + public CustomId decode(BsonReader bsonReader, DecoderContext decoderContext) { + return CustomId.builder().aStringProperty(bsonReader.readString()).build(); + } + + @Override + public void encode(BsonWriter bsonWriter, CustomId customId, EncoderContext encoderContext) { + bsonWriter.writeString(customId.getAStringProperty()); + } + + @Override + public Class getEncoderClass() { + return CustomId.class; + } + } + + /** + * using WrongCustomIdGenerator generates a wrong customId and therefore generates an exception + * use at {@link Pojo#id} - @Id(collectible = true, value = ExternalIdCodecProviderTest.WrongCustomIdGenerator.class) + */ + public static class WrongCustomIdGenerator implements IdGenerator { + @Override + public Object generate() { + return "SomeRandomWrongCustomId"; + } + } + + + @Autowired + CodecRegistry codecRegistry; + + @Autowired + private MongoClient mongoClient; + + @Test(expected = IdGenerationException.class) + public void testExternalId() { + Pojo pojo = Pojo.builder().id(null).someOtherProperty("some nice string").build(); + MongoCollection collection = mongoClient.getDatabase("test").getCollection("documents").withDocumentClass(Pojo.class); + collection.insertOne(pojo); + + Pojo readPojo = collection.find(Filters.eq("_id", pojo.getId())).first(); + + Assert.assertNotNull(readPojo); + } +} diff --git a/src/test/java/de/bild/codec/idtypemismatch/model/CustomId.java b/src/test/java/de/bild/codec/idtypemismatch/model/CustomId.java new file mode 100644 index 0000000..1501240 --- /dev/null +++ b/src/test/java/de/bild/codec/idtypemismatch/model/CustomId.java @@ -0,0 +1,15 @@ +package de.bild.codec.idtypemismatch.model; + +import de.bild.codec.annotations.IgnoreType; +import lombok.*; + +@EqualsAndHashCode +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Getter +@Setter +@IgnoreType +public class CustomId { + String aStringProperty; +} diff --git a/src/test/java/de/bild/codec/idtypemismatch/model/Pojo.java b/src/test/java/de/bild/codec/idtypemismatch/model/Pojo.java new file mode 100644 index 0000000..c2672ea --- /dev/null +++ b/src/test/java/de/bild/codec/idtypemismatch/model/Pojo.java @@ -0,0 +1,17 @@ +package de.bild.codec.idtypemismatch.model; + +import de.bild.codec.annotations.Id; +import de.bild.codec.idtypemismatch.ExternalIdCodecProviderTest; +import lombok.*; + +@EqualsAndHashCode +@Getter +@Setter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class Pojo { + @Id(collectible = true, value = ExternalIdCodecProviderTest.WrongCustomIdGenerator.class) + CustomId id; + String someOtherProperty; +} diff --git a/src/test/java/de/bild/codec/typecodecprovider/TypeCodecProviderTest.java b/src/test/java/de/bild/codec/typecodecprovider/TypeCodecProviderTest.java new file mode 100644 index 0000000..f1d8d91 --- /dev/null +++ b/src/test/java/de/bild/codec/typecodecprovider/TypeCodecProviderTest.java @@ -0,0 +1,258 @@ +package de.bild.codec.typecodecprovider; + +import com.mongodb.MongoClient; +import de.bild.codec.*; +import lombok.*; +import org.apache.commons.lang3.reflect.TypeUtils; +import org.bson.BsonReader; +import org.bson.BsonWriter; +import org.bson.codecs.Codec; +import org.bson.codecs.DecoderContext; +import org.bson.codecs.EncoderContext; +import org.bson.codecs.configuration.CodecRegistries; +import org.bson.codecs.configuration.CodecRegistry; +import org.bson.json.JsonReader; +import org.bson.json.JsonWriter; +import org.bson.json.JsonWriterSettings; +import org.hamcrest.CoreMatchers; +import org.hamcrest.MatcherAssert; +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +import java.io.StringWriter; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.*; + +import static org.hamcrest.MatcherAssert.assertThat; + +@RunWith(SpringJUnit4ClassRunner.class) +@SpringBootTest(classes = TypeCodecProviderTest.class) +@ComponentScan(basePackages = "de.bild") +public class TypeCodecProviderTest { + + static class Config { + @Bean() + public static CodecRegistry getCodecRegistry() { + return CodecRegistries.fromRegistries( + CodecRegistries.fromProviders( + new EnumCodecProvider(), + PojoCodecProvider.builder() + .register(TypeCodecProviderTest.class) + .register(new CustomTypeCodecProvider(), new SetOfStringTypeCodecProvider()) + .build() + ), + MongoClient.getDefaultCodecRegistry()); + } + } + + @Autowired + CodecRegistry codecRegistry; + + + public static class CustomTypeCodecProvider implements TypeCodecProvider { + @Override + public Codec get(Type type, TypeCodecRegistry typeCodecRegistry) { + if (TypeUtils.isAssignable(type, CustomType.class)) { + return (Codec) new CustomTypeCodec((ParameterizedType) type, typeCodecRegistry); + } + return null; + } + } + + public static class SetOfStringTypeCodecProvider implements TypeCodecProvider { + static final Type setOfStringsType; + static { + try { + setOfStringsType = Pojo.class.getDeclaredField("strings").getGenericType(); + } catch (NoSuchFieldException e) { + throw new IllegalArgumentException("Could not get type of field strings in Pojo.class"); + } + } + @Override + public Codec get(Type type, TypeCodecRegistry typeCodecRegistry) { + if (TypeUtils.isAssignable(type, setOfStringsType)) { + return (Codec) new SetOfStringCodec((ParameterizedType) type, typeCodecRegistry); + } + return null; + } + } + + /** + * A codec that specifically handles Sets of Strings + */ + public static class SetOfStringCodec implements Codec>{ + final Class> clazz; + + public SetOfStringCodec(ParameterizedType type, TypeCodecRegistry typeCodecRegistry) { + this.clazz = ReflectionHelper.extractRawClass(type); + + } + + @Override + public Set decode(BsonReader reader, DecoderContext decoderContext) { + Set stringSet = new HashSet<>(); + String[] split = reader.readString().split("#"); + stringSet.addAll(Arrays.asList(split)); + return stringSet; + } + + @Override + public void encode(BsonWriter writer, Set value, EncoderContext encoderContext) { + StringBuilder sb = new StringBuilder(); + for (String s : value) { + sb.append(s).append('#'); + } + writer.writeString(sb.toString()); + } + + @Override + public Class> getEncoderClass() { + return clazz; + } + } + + + public static class CustomTypeCodec implements Codec> { + final Codec nameCodec; + final Codec> listCodec; + final Codec> innerTypeCodec; + final Class> clazz; + + public CustomTypeCodec(ParameterizedType type, TypeCodecRegistry typeCodecRegistry) { + Type typeParameter = type.getActualTypeArguments()[0]; + this.nameCodec = typeCodecRegistry.getCodec(String.class); + this.listCodec = typeCodecRegistry.getCodec(TypeUtils.parameterize(ArrayList.class, typeParameter)); + this.innerTypeCodec = typeCodecRegistry.getCodec(ReflectionHelper.getDeclaredAndInheritedFieldTypePair(type, "innerType").getRealType()); + this.clazz = ReflectionHelper.extractRawClass(type); + } + + @Override + public CustomType decode(BsonReader reader, DecoderContext decoderContext) { + reader.readStartDocument(); + reader.readName("name"); + String name = nameCodec.decode(reader, decoderContext); + + reader.readName("list"); + List stringArrayList = listCodec.decode(reader, decoderContext); + + reader.readName("innerType"); + InnerType innerType = innerTypeCodec.decode(reader, decoderContext); + + reader.readEndDocument(); + CustomType customType = new CustomType(name); + customType.setInnerType(innerType); + customType.addAll(stringArrayList); + + return customType; + } + + @Override + public void encode(BsonWriter writer, CustomType value, EncoderContext encoderContext) { + writer.writeStartDocument(); + writer.writeName("name"); + nameCodec.encode(writer, value.getANameForTheList(), encoderContext); + + writer.writeName("list"); + listCodec.encode(writer, value, encoderContext); + + writer.writeName("innerType"); + innerTypeCodec.encode(writer, value.getInnerType(), encoderContext); + writer.writeEndDocument(); + } + + @Override + public Class> getEncoderClass() { + return clazz; + } + } + + @NoArgsConstructor + public static class InnerType { + T someThing; + + public InnerType(T someThing) { + this.someThing = someThing; + } + } + + @Setter + @Getter + public static class CustomType extends ArrayList { + String aNameForTheList; + InnerType innerType; + T type; + + public CustomType(String aNameForTheList) { + this.aNameForTheList = aNameForTheList; + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder("CustomType{"); + sb.append("aNameForTheList='").append(aNameForTheList).append('\''); + sb.append("list='" + super.toString()).append('\''); + sb.append(", innerType=").append(innerType); + sb.append(", type=").append(type); + sb.append('}'); + return sb.toString(); + } + } + + @Setter + @Getter + @NoArgsConstructor + @AllArgsConstructor + @Builder + public static class Pojo { + String name; + Set strings; // should be serialized as "value1#value2#value3#" instead of a json array + CustomType customTypeString; + CustomType customTypeInteger; + } + + @Test + public void testDifferentTypes() { + Codec pojoCodec = codecRegistry.get(Pojo.class); + + CustomType customTypeString = new CustomType("A custom string type"); + String[] strings = {"a", "nice", "list", "of", "strings"}; + customTypeString.addAll(Arrays.asList(strings)); + customTypeString.setInnerType(new InnerType<>("String value")); + + + CustomType customTypeInteger = new CustomType("A custom integer type"); + Integer[] integers = {1, 42, 66, 89}; + customTypeInteger.addAll(Arrays.asList(integers)); + customTypeInteger.setInnerType(new InnerType<>(11234567)); + + + String[] stringsForSet = {"Tee", "Brot", "Butter"}; + Pojo pojo = Pojo.builder() + .customTypeString(customTypeString) + .customTypeInteger(customTypeInteger) + .name("aName") + .strings(new HashSet<>(Arrays.asList(stringsForSet))) + .build(); + + StringWriter stringWriter = new StringWriter(); + JsonWriter writer = new JsonWriter(stringWriter, new JsonWriterSettings(true)); + pojoCodec.encode(writer, pojo, EncoderContext.builder().build()); + System.out.println(stringWriter.toString()); + + Pojo decodedPojo = pojoCodec.decode(new JsonReader(stringWriter.toString()), DecoderContext.builder().build()); + + System.out.println(decodedPojo); + + Assert.assertNotNull(decodedPojo); + MatcherAssert.assertThat(decodedPojo.getCustomTypeString(), CoreMatchers.hasItems(strings)); + MatcherAssert.assertThat(decodedPojo.getCustomTypeInteger(), CoreMatchers.hasItems(integers)); + MatcherAssert.assertThat(decodedPojo.getStrings(), CoreMatchers.hasItems(stringsForSet)); + } +}