From 770dd14a729cdf987aeff316c75dc6f7290ff509 Mon Sep 17 00:00:00 2001 From: Brian Roberts Date: Tue, 5 Jan 2021 14:11:12 +0100 Subject: [PATCH] Allow overriding a default codec, like InstantCodec. Caveat: in order for this to work for Entity fields, you also need to override decoder / encoder in SimpleDecoder / SimpleEncoder. Fixes #180 --- .../mongo/config/MongoMappingContext.java | 8 +- .../gorm/mongo/CustomCodecSpec.groovy | 119 +++++++++++++++++- 2 files changed, 122 insertions(+), 5 deletions(-) diff --git a/grails-datastore-gorm-mongodb/src/main/groovy/org/grails/datastore/mapping/mongo/config/MongoMappingContext.java b/grails-datastore-gorm-mongodb/src/main/groovy/org/grails/datastore/mapping/mongo/config/MongoMappingContext.java index df73c16f..5ae1429d 100644 --- a/grails-datastore-gorm-mongodb/src/main/groovy/org/grails/datastore/mapping/mongo/config/MongoMappingContext.java +++ b/grails-datastore-gorm-mongodb/src/main/groovy/org/grails/datastore/mapping/mongo/config/MongoMappingContext.java @@ -162,10 +162,6 @@ protected void initialize(ConnectionSourceSettings settings) { Iterable codecList = ConfigurationUtils.findServices(codecClasses, Codec.class); List> codecs = new ArrayList<>(); - for (Codec codec : codecList) { - codecs.add(codec); - } - codecs.add(new InstantCodec()); codecs.add(new LocalDateCodec()); codecs.add(new LocalDateTimeCodec()); @@ -175,6 +171,10 @@ protected void initialize(ConnectionSourceSettings settings) { codecs.add(new PeriodCodec()); codecs.add(new ZonedDateTimeCodec()); + for (Codec codec : codecList) { + codecs.add(codec); + } + if(mongoConnectionSourceSettings.getCodecRegistry() != null) { this.codecRegistry = CodecRegistries.fromRegistries( mongoConnectionSourceSettings.getCodecRegistry(), diff --git a/grails-datastore-gorm-mongodb/src/test/groovy/org/grails/datastore/gorm/mongo/CustomCodecSpec.groovy b/grails-datastore-gorm-mongodb/src/test/groovy/org/grails/datastore/gorm/mongo/CustomCodecSpec.groovy index bfeb822c..63b64e51 100644 --- a/grails-datastore-gorm-mongodb/src/test/groovy/org/grails/datastore/gorm/mongo/CustomCodecSpec.groovy +++ b/grails-datastore-gorm-mongodb/src/test/groovy/org/grails/datastore/gorm/mongo/CustomCodecSpec.groovy @@ -1,21 +1,42 @@ package org.grails.datastore.gorm.mongo +import grails.gorm.time.InstantConverter +import grails.mongodb.MongoEntity +import grails.persistence.Entity +import org.bson.BsonDateTime +import org.bson.BsonDocumentWrapper import org.bson.BsonReader +import org.bson.BsonType import org.bson.BsonWriter import org.bson.codecs.Codec import org.bson.codecs.DecoderContext import org.bson.codecs.EncoderContext +import org.bson.types.ObjectId +import org.grails.datastore.bson.codecs.decoders.SimpleDecoder +import org.grails.datastore.bson.codecs.encoders.SimpleEncoder +import org.grails.datastore.bson.codecs.temporal.TemporalBsonConverter +import org.grails.datastore.mapping.engine.EntityAccess +import org.grails.datastore.mapping.model.PersistentProperty import org.grails.datastore.mapping.mongo.MongoDatastore import spock.lang.AutoCleanup import spock.lang.Shared import spock.lang.Specification +import java.time.Instant +import java.util.concurrent.atomic.AtomicLong + +import static java.time.temporal.ChronoUnit.DAYS + /** * Created by graemerocher on 27/09/2016. */ class CustomCodecSpec extends Specification { - @AutoCleanup @Shared MongoDatastore datastore = new MongoDatastore(['grails.mongodb.codecs':[BirthdayCodec]], Person) + @AutoCleanup @Shared MongoDatastore datastore = new MongoDatastore( + ['grails.mongodb.codecs':[BirthdayCodec, + InstantAsBsonDateTimeCodec + ]], + Person, InstantHolder) void "Test custom codecs"() { when:"A new person is saved" @@ -31,6 +52,42 @@ class CustomCodecSpec extends Specification { Person.findByBirthday(birthday) !Person.findByBirthday(new Birthday(new Date() - 7)) } + + void "Test codec overriding for simple type Instant"() { + setup: + def defaultInstantDecoder = SimpleDecoder.SIMPLE_TYPE_DECODERS[Instant] + def defaultInstantEncoder = SimpleEncoder.SIMPLE_TYPE_ENCODERS[Instant] + SimpleDecoder.SIMPLE_TYPE_DECODERS[Instant] = new InstantAsBsonDateTimeDecoder() + SimpleEncoder.SIMPLE_TYPE_ENCODERS[Instant] = new InstantAsBsonDateTimeEncoder() + + when:"A new instant holder is saved" + InstantAsBsonDateTimeCodec.resetCounts() + InstantHolder.DB.drop() + def instant = Instant.now() + def holder = new InstantHolder(anInstant: instant) + + def codecRegistry = InstantHolder.collection.codecRegistry + def wrapper = new BsonDocumentWrapper(holder, codecRegistry.get(InstantHolder)) + def serializedInstant = wrapper.get('anInstant') + + holder.save(flush:true) + InstantHolder ih = InstantHolder.first() + + then:"The serialization is correct" + serializedInstant.class == BsonDateTime + codecRegistry.get(Instant).class == InstantAsBsonDateTimeCodec + ih.anInstant + InstantAsBsonDateTimeCodec.encodeCount.get() == 0 + InstantHolder.findByAnInstant(instant) + InstantAsBsonDateTimeCodec.encodeCount.get() == 1 + !InstantHolder.findByAnInstant(instant.minus(7, DAYS)) + InstantAsBsonDateTimeCodec.encodeCount.get() == 2 + InstantAsBsonDateTimeCodec.decodeCount.get() == 0 + + cleanup: + SimpleDecoder.SIMPLE_TYPE_DECODERS[Instant] = defaultInstantDecoder + SimpleEncoder.SIMPLE_TYPE_ENCODERS[Instant] = defaultInstantEncoder + } } class BirthdayCodec implements Codec { @@ -42,3 +99,63 @@ class BirthdayCodec implements Codec { } Class getEncoderClass() { Birthday } } + +class InstantAsBsonDateTimeCodec implements Codec { + public static AtomicLong decodeCount = new AtomicLong() + public static AtomicLong encodeCount = new AtomicLong() + static void resetCounts() { + decodeCount.set(0) + encodeCount.set(0) + } + + Instant decode(BsonReader reader, DecoderContext decoderContext) { + decodeCount.incrementAndGet() + return Instant.ofEpochMilli(reader.readDateTime()) + } + + void encode(BsonWriter writer, Instant value, EncoderContext encoderContext) { + encodeCount.incrementAndGet() + writer.writeDateTime(value.toEpochMilli()) + } + + Class getEncoderClass() { Instant } +} + +trait InstantAsBsonDateTimeConverter implements TemporalBsonConverter, InstantConverter { + + @Override + void write(BsonWriter writer, Instant value) { + writer.writeDateTime(value.toEpochMilli()) + } + + @Override + Instant read(BsonReader reader) { + Instant.ofEpochMilli(reader.readDateTime()) + } + + @Override + BsonType bsonType() { + BsonType.DATE_TIME + } + +} + +class InstantAsBsonDateTimeEncoder implements SimpleEncoder.TypeEncoder, InstantAsBsonDateTimeConverter { + @Override + void encode(BsonWriter writer, PersistentProperty property, Object value) { + write(writer, (Instant)value) + } +} + +class InstantAsBsonDateTimeDecoder implements SimpleDecoder.TypeDecoder, InstantAsBsonDateTimeConverter { + @Override + void decode(BsonReader reader, PersistentProperty property, EntityAccess entityAccess) { + entityAccess.setPropertyNoConversion(property.name, read(reader)) + } +} + +@Entity +class InstantHolder implements MongoEntity { + ObjectId id + Instant anInstant +}