javamongodbmorphia

Morphia 2.3 mapping returning String instead of Locale as Map Key


We are in the process of upgrading from Morphia 1.3.2 to Morphia 2.3.2

In the microservice we have most recently upgraded, there is a class containing a field messageTemplates:

private Map<Locale, MessageTemplate> messageTemplates;

    public Map<Locale, MessageTemplate> getMessageTemplates() {
        if(messageTemplates == null){
            this.messageTemplates = new HashMap<>();
        }
        return messageTemplates;
    }

    public void setMessageTemplates(Map<Locale, MessageTemplate> messageTemplates) {
        this.messageTemplates = messageTemplates;
    }

The json data within the Mongo collection for this field looks like:

    "_id" : "55c8a25e-137c-44f8-8e75-18617aafd374",
    "className" : "com.swipejobs.desktopnotifications.model.NotificationTypeDetails",
    "messageTemplates" : {
        "en" : {
            "subject" : "",
            "body" : "Your verification PIN is ${pincode}. Please do not reply to this message.",
            "emailSubject" : "",
            "emailBody" : ""
        }
    },

When a document is retrieved from the collection, the key in the map is an object of class String, rather than an object of type Locale. So the key of the map is converted to a String when saved to the collection, but does not seem to be mapped back to a Locale when the document is retrieved.

Our MongoClient is setup:

import com.swipejobs.dataaccess.codecs.ZoneIdCodec;
import com.swipejobs.dataaccess.codecs.ZonedDateTimeCodec;
import dev.morphia.Datastore;
import dev.morphia.mapping.codec.LocaleCodec;


        CodecRegistry codecRegistry = CodecRegistries.fromRegistries(CodecRegistries.fromCodecs(new ZonedDateTimeCodec(),
                                                                                                new ZoneIdCodec(),
                                                                                                new LocaleCodec()),
                                                                     MongoClientSettings.getDefaultCodecRegistry(),
                                                                     CodecRegistries.fromProviders(PojoCodecProvider.builder().automatic(true).build()));
ConnectionString connectionString = new ConnectionString(uri);
MongoClientSettings settings = MongoClientSettings.builder().codecRegistry(codecRegistry).applyConnectionString(connectionString).build();

MongoClient mongoClient = MongoClients.create(settings);

MapperOptions options = MapperOptions.builder().discriminator(DiscriminatorFunction.className()).discriminatorKey("className").collectionNaming(NamingStrategy.identity()).build();

mDataStore = Morphia.createDatastore(mMongoClient, mDbName, options);
mDataStore.ensureIndexes();
datastore.getMapper().map(NotificationTypeDetails.class);



Does anyone have any knowledge of this scenario?

Our expectation was that the keys in the map would be of type Locale rather than type String.

We added the following hack:

    private Map<Locale, MessageTemplate> fixMap(Map<Locale, MessageTemplate> map){
        return map.entrySet().stream()
                             .map(this::fixEntry)
                             .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
    }

    private Map.Entry<Locale, MessageTemplate> fixEntry(Map.Entry<Locale, MessageTemplate> entry) {
        Locale key = fixKey(entry.getKey());
        MessageTemplate value = entry.getValue();

        return Map.of(key, value).entrySet().stream().findFirst().get();
    }

    private Locale fixKey(Object key){
        if (key.getClass().equals(Locale.class))
            return (Locale) key;
        if (key.getClass().equals(String.class))
            return new Locale((String) key);
        return Locale.ENGLISH;
    }

Which fixes our problem, but does not explain why it is occurring. As can be seen previously, we have added the LocaleCodec but it seems to be doing nothing.


Solution

  • I was actually fixing a bug related to this just last night and did some digging. Map keys are not subject to codec de/serialization for a number of technical reasons. These keys are converted to/from Strings using a Conversions entry. Now this isn't entirely a public API but it's been used from time to time by different users. Since it's not a public API, it might change under you but it's been pretty stable for a few years now so I think that fear is mostly hypothetical at this point. That said, I'll show you how to define one:

    To register a conversion you'd call Conversions.regsiter() which has the following signature

    public static <S, T> void register(Class<S> source, Class<T> target, Function<S, T> function)
    

    And here is one of the built-in conversions:

    register(String.class, URI.class, str -> URI.create(str.replace("%46", ".")));
    

    This conversion will take a String coming from the database whose target type is a URI and call the appropriate code to make that conversion. In this case, it's URI.create (and it does some clean up on the escape chars in the string). For Locale you'd do something similar. Two things to note:

    1. This conversion applies to all String -> Locale conversions so it needs to be generalized. In this case, Locale is a pretty mundane type so the need for exotic and specific conversions is almost certainly 0.
    2. When writing to the database, if the conversion isn't found, the map codec being used will simply toString() the key since document keys in the database can only be strings. This may not result in a format you want or one that can simply be passed back in to a Locale method to parse back out. If that's the case, you'll want to do `register(Locale.class, String.class, l -> { /* convert your Locale to a String here /* }) somewhere in your application code.

    And that should get you squared away. If that doesn't work for whatever reason, please file an issue or open a discussion on GitHub and I'll try to get you fixed up.