ashutosh
ashutosh

Reputation: 649

spring data mongodb: access default POJO converter from within custom converter

I have spring data mongo custom converters setup via xml as follows

<mongo:mapping-converter id="mongoConverter" db-factory-ref="mongoDbFactory">
    <mongo:custom-converters>
        <mongo:converter ref="customWriteConverter" />
        <mongo:converter ref="customReadConverter" />
    </mongo:custom-converters>
</mongo:mapping-converter>

<bean id="mongoTemplate" class="org.springframework.data.mongodb.core.MongoTemplate">
    <constructor-arg ref="mongoDbFactory"/>
    <constructor-arg ref="mongoConverter"/>
</bean>

<bean id="customWriteConverter" class="package.WriteConverter" />
<bean id="customReadConverter" class="package.ReadConverter" />

In the custom read/write converter, I would like to re-use spring-data-mongo's default pojo converter to save certain properties as subdocuments.

consider a simplified example -

class A {
    B b;
    String var1;
    int var2;
}

class B {
    String var3;
    String var4;
}

I want to handle conversion of class A using customWriteConverter and customReadConverter, but in my custom converters I also want to delegate conversion of class B back to spring-data-mongo's default POJO converter.

How can I do this? I have not been able to successfully autowire a MongoConverter or MongoTemplate into the custom converter since the MongoConverter/MongoTemplate bean creation is in progress when it tries to create the custom converter. Is it possible to get access to the default converter and use that from within the custom converter?

Upvotes: 15

Views: 4413

Answers (6)

CelinHC
CelinHC

Reputation: 1984

Here this working with spring-boot-starter-data-mongodb version 2.5.2

package com.example.mongo;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.convert.converter.Converter;
import org.springframework.data.mongodb.MongoDatabaseFactory;
import org.springframework.data.mongodb.config.AbstractMongoClientConfiguration;
import org.springframework.data.mongodb.core.convert.DbRefResolver;
import org.springframework.data.mongodb.core.convert.DefaultDbRefResolver;
import org.springframework.data.mongodb.core.convert.MappingMongoConverter;
import org.springframework.data.mongodb.core.convert.MongoConverter;
import org.springframework.data.mongodb.core.convert.MongoCustomConversions.MongoConverterConfigurationAdapter;
import org.springframework.data.mongodb.core.mapping.MongoMappingContext;

@Configuration
public class MongoConfig extends AbstractMongoClientConfiguration {

    private @Value("${spring.data.mongodb.database}") String database;
    private @Autowired MongoDatabaseFactory mongoDatabaseFactory;

    @Override
    protected String getDatabaseName() {
        return database;
    }

    @Override
    protected void configureConverters(MongoConverterConfigurationAdapter converterConfigurationAdapter) {
        DbRefResolver dbRefResolver = new DefaultDbRefResolver(mongoDatabaseFactory);
        MongoConverter mongoConverter = new MappingMongoConverter(dbRefResolver, new MongoMappingContext());
        converterConfigurationAdapter.registerConverters(customConverters(mongoConverter));
    }

    public List<Converter<?, ?>> customConverters(MongoConverter mongoConverter) {
        MyCustomConverter custom = new MyCustomConverter(mongoConverter);
        return List.of(custom);
    }

}

Upvotes: 0

Kian Jawadi
Kian Jawadi

Reputation: 1

I know it's quite late but today I just faced this problem and I thought maybe it's better to share it here.

Since MongoTemplate (and its default converters) are initialized after our custom-converters, it's not possible to inject those directly into our converters, but we can access those by implementing ApplicationContextAware in our converters. After accessing to mongoTemplate, we can delegate the read/write conversion to it by calling mongoTemplate.getConverter().read and mongoTemplate.getConverter().write methods respectively.

Let's examine an example. Assume we have two POJOs:

public class Outer {
    public String var1;
    public int var2;

    public Inner inner;
}
public class Inner {
    public String var3;
    public String var4;
}

The WriteConverter could be something like this:

import org.bson.Document;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.core.convert.converter.Converter;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.stereotype.Component;

@Component
public class CustomWriteConverter implements Converter<Outer, Document>, ApplicationContextAware {

    private ApplicationContext applicationContext;
    private MongoTemplate mongoTemplate;

    @Override
    public Document convert(Outer source) {
        // initialize the mongoTemplate
        if (mongoTemplate == null) {
            this. mongoTemplate = applicationContext.getBean(MongoTemplate.class);
        }

        // do some custom stuff

        Document document = new Document();
        document.put("var1", source.var1);
        document.put("var2", source.var2);

        // Using MongoTemplate's converters
        Document inner = new Document();
        mongoTemplate.getConverter().write(source.inner, inner);

        document.put("inner", inner);


        return document;
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }

}

And the ReadConverter:

import org.bson.Document;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.core.convert.converter.Converter;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.stereotype.Component;

@Component
public class CustomReadConverter implements Converter<Document, Outer>, ApplicationContextAware {

    private ApplicationContext applicationContext;
    private MongoTemplate mongoTemplate;

    @Override
    public Outer convert(Document source) {
        // initialize the mongoTemplate
        if (mongoTemplate == null) {
            this. mongoTemplate = applicationContext.getBean(MongoTemplate.class);
        }

        // do some custom stuff

        Outer outer = new Outer();
        outer.var1 = source.getString("var1");
        outer.var2 = source.getInteger("var2");

        // Using MongoTemplate's converters
        Inner inner = mongoTemplate.getConverter().read(Inner.class, (Document) source.get("inner"));
        outer.inner = inner;

        return outer;
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }

}

The mongoTemplate could be initialized by multiple threads (because it's in a race condition), but since it has a scope of singleton, there would be no problem.

Now the only thing to do is to register our converters.

Upvotes: 0

rougou
rougou

Reputation: 1216

This may not be exactly the same use case, but I had to modify existing mongo documents on a lazy basis (without using $project, etc). Basically, I copied Spring's getDefaultMongoConverter method (which changed since earlier answers here and may change again in the future) and added an argument to pass a custom converter(s). When creating the custom converter itself (FooConverter), I pass in an empty list for the customer converters (this may differ if you have additional converters for sub-documents). Then when creating the final converter I pass in my FooConverter.

Here is some (untested) sample code. This assumes auto-configuration is enabled and thus MongoDbFactory is already wired in. If not, you'll be creating your own MongoDbFactory bean but everything else is pretty much the same.

@Bean
public MongoTemplate mongoTemplate(final MongoDbFactory mongoDbFactory) throws Exception {
    FooReadConverter fooConverter = new FooReadConverter(mongoDbFactory);
    MongoConverter converter = getMongoConverter(mongoDbFactory, List.of(fooConverter));

    return new MongoTemplate(mongoDbFactory, converter);
}

/**
 * Get a mongo converter
 * @see org.springframework.data.mongodb.core.MongoTemplate#getDefaultMongoConverter
 */
static MongoConverter getMongoConverter(MongoDbFactory factory, List<?> customConverters) {

    DbRefResolver dbRefResolver = new DefaultDbRefResolver(factory);
    MongoCustomConversions conversions = new MongoCustomConversions(customConverters);

    MongoMappingContext mappingContext = new MongoMappingContext();
    mappingContext.setSimpleTypeHolder(conversions.getSimpleTypeHolder());
    mappingContext.afterPropertiesSet();

    MappingMongoConverter converter = new MappingMongoConverter(dbRefResolver, mappingContext);
    converter.setCustomConversions(conversions);
    converter.setCodecRegistryProvider(factory);
    converter.afterPropertiesSet();

    return converter;
}

@ReadingConverter
static class FooReadConverter implements Converter<Document, Foo> {

    private final MongoConverter defaultConverter;

    public FooReadConverter(MongoDbFactory dbFactory) {
        this.defaultConverter = getMongoConverter(dbFactory, List.of());
    }

    @Override
    public Foo convert(Document source) {
        boolean isOldFoo = source.containsKey("someKeyOnlyInOldFoo");
        Foo foo;
        if (isOldFoo) {
            OldFoo oldFoo = defaultConverter.read(OldFoo.class, source);
            foo = oldFoo.toNewFoo();
        } else {
            foo = defaultConverter.read(Foo.class, source);
        }

        return foo;
    }
}

Upvotes: 1

S2201
S2201

Reputation: 1349

Try to inject BeanFactory in your converter and in convert method fetch mongoTemplate from BeanFactory...

Upvotes: 0

Alex Paransky
Alex Paransky

Reputation: 1015

If you are converting TO mongo database and want to default some conversions, you could do something like this:

    ...

    @Resource
    private ObjectFactory<MappingMongoConverter>
        mappingMongoConverterObjectFactory;

    private MappingMongoConverter
        mappingMongoConverter;

    ...

    //Otherwise, use default MappingMongoConverter
    //
    if (result == null)
        result =
            getMappingMongoConverter()
                .convertToMongoType(
                    value
                );

    ...

    MappingMongoConverter getMappingMongoConverter() {
        if (mappingMongoConverter == null)
            mappingMongoConverter =
                mappingMongoConverterObjectFactory.getObject();
        return
            mappingMongoConverter;
    }

The MappingMongoConverter cannot be directly @Resource (ed) in my case since it's in the process of being constructed when other converters are being built. So, Spring detects a circular reference. I am not sure if there is a better "lazy" method of doing this without all the run-around of ObjectFactory, getter method, and caching.

Now, if someone can figure out a method of defaulting to standard processing while going back (from DBObject to java Object) that would complete this circle.

Upvotes: 3

Harsh Poddar
Harsh Poddar

Reputation: 2554

This method is used in MongoTemplate class to get a default converter.

private static final MongoConverter getDefaultMongoConverter(MongoDbFactory factory) {
    DbRefResolver dbRefResolver = new DefaultDbRefResolver(factory);
    MappingMongoConverter converter = new MappingMongoConverter(dbRefResolver, new MongoMappingContext());
    converter.afterPropertiesSet();
    return converter;
}

MappingMongoConverter is not final and so can be overridden for a specific purpose. As mentioned in my comment above, look at this question to maybe find out solution to your problem.

Upvotes: 5

Related Questions