Skip to content

Commit

Permalink
##2.0.0
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
webermich committed Jan 15, 2018
1 parent df8ebdb commit 9633847
Show file tree
Hide file tree
Showing 33 changed files with 1,262 additions and 331 deletions.
79 changes: 65 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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

Expand All @@ -235,7 +286,7 @@ Release notes are available [release_notes.md](release_notes.md).
<dependency>
<groupId>de.bild.backend</groupId>
<artifactId>polymorphia</artifactId>
<version>1.7.0</version>
<version>2.0.0</version>
</dependency>
```

Expand Down
4 changes: 2 additions & 2 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

<groupId>de.bild.backend</groupId>
<artifactId>polymorphia</artifactId>
<version>1.7.0</version>
<version>2.0.0</version>

<name>${project.groupId}:${project.artifactId}</name>

Expand Down Expand Up @@ -124,7 +124,7 @@
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>test</scope>
<scope>compile</scope>
</dependency>
</dependencies>

Expand Down
11 changes: 11 additions & 0 deletions release_notes.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
1 change: 0 additions & 1 deletion src/main/java/de/bild/codec/AbstractTypeCodec.java
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,6 @@ protected Constructor<T> getDefaultConstructor(Class<T> clazz) {
}
}

@Override
public T newInstance() {
try {
return defaultConstructor.newInstance();
Expand Down
71 changes: 27 additions & 44 deletions src/main/java/de/bild/codec/BasicReflectionCodec.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -19,7 +20,7 @@
public class BasicReflectionCodec<T> extends AbstractTypeCodec<T> implements ReflectionCodec<T> {

private static final Logger LOGGER = LoggerFactory.getLogger(BasicReflectionCodec.class);
MappedField idField;
MappedField<T, Object> idField;

/**
* a list of the fields to map
Expand All @@ -30,13 +31,13 @@ public class BasicReflectionCodec<T> extends AbstractTypeCodec<T> 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<T, Object> mappedField = new MappedField<>(fieldTypePair, encoderClass, typeCodecRegistry, codecConfiguration);
persistenceFields.put(mappedField.getMappedFieldName(), mappedField);
if (mappedField.isIdField()) {
if (idField == null) {
Expand All @@ -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);
}
}
}
Expand Down Expand Up @@ -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<String> 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);
Expand All @@ -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);
Expand All @@ -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;
Expand All @@ -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;
Expand Down
19 changes: 19 additions & 0 deletions src/main/java/de/bild/codec/CodecConfiguration.java
Original file line number Diff line number Diff line change
@@ -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;
}
7 changes: 4 additions & 3 deletions src/main/java/de/bild/codec/CodecResolver.java
Original file line number Diff line number Diff line change
Expand Up @@ -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<T> {
/**
*
* @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<T> getCodec(Type type, TypeCodecRegistry typeCodecRegistry, CodecConfiguration codecConfiguration);
}
1 change: 0 additions & 1 deletion src/main/java/de/bild/codec/CollectionTypeCodec.java
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,6 @@ public void encode(BsonWriter writer, C value, EncoderContext encoderContext) {
writer.writeEndArray();
}

@Override
public C defaultInstance() {
return newInstance();
}
Expand Down
11 changes: 11 additions & 0 deletions src/main/java/de/bild/codec/IdGenerationException.java
Original file line number Diff line number Diff line change
@@ -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);
}
}
1 change: 0 additions & 1 deletion src/main/java/de/bild/codec/MapTypeCodec.java
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ protected Constructor<Map<K, V>> getDefaultConstructor(Class<Map<K, V>> clazz) {
return super.getDefaultConstructor(clazz);
}

@Override
public Map<K, V> defaultInstance() {
return newInstance();
}
Expand Down
Loading

0 comments on commit 9633847

Please sign in to comment.