2

I am trying to get a grasp of ModelMapper for the following use case:

class A {
    public String name;
    public Map<String, ATranslation> translations;
}

class ATranslation {
    public String desc;
    public String content;
}

class DTO {
    public String name;
    public String desc;
    public String content;
}

Assume Constructors, Getters and Setters.

public class App {
    public static void main(String[] args) {
       Map<String, ATranslation> translations = new HashMap<>();
       translations.put("en", new ATranslation("en-desc","content1"));
       translations.put("nl", new ATranslation("nl-desc","content2"));
       A entity = new A("John Wick",translations);

       System.out.println(App.toDto(entity,"en"));
       System.out.println(App.toDto(entity,"nl"));
    }
    
    private static DTO toDto(A entity, String lang) {
        ModelMapper modelMapper = new ModelMapper();

        //how to set up ModelMapper?

        return modelMapper.map(entity, DTO.class);
    }
}

Without any setup the output is:

DTO(name=John Wick, desc=null, content=null)
DTO(name=John Wick, desc=null, content=null)

A converter does not work:

modelMapper
    .createTypeMap(A.class, DTO.class)
    .setConverter(new Converter<A, DTO>() {
       public DTO convert(MappingContext<A, DTO> context) {
          A s = context.getSource();
          DTO d = context.getDestination();
          d.setDesc(s.getTranslation().get(lang).getDesc());
          d.setContent(s.getTranslation().get(lang).getContent());
          return d;
       }
   });

A postConverter does work, but does not seem to be the most ModelMapper way...

modelMapper
    .createTypeMap(A.class, DTO.class)
    .setPostConverter(new Converter<A, DTO>() {
       public DTO convert(MappingContext<A, DTO> context) {
          A s = context.getSource();
          DTO d = context.getDestination();
          d.setDesc(s.getTranslation().get(lang).getDesc()); //tedious, if many fields...
          d.setContent(s.getTranslation().get(lang).getContent()); //feels redundant already
          return d;
       }
   });
DTO(name=John Wick, desc=en-desc, content=content1)
DTO(name=John Wick, desc=nl-desc, content=content2)

Is there a better way to use ModelMapper here?

SpazzMarticus
  • 1,218
  • 1
  • 20
  • 40

1 Answers1

0

Did not test it! This is just a theoretical answer composed from research.


Design considerations

Lead by your concerns about the tedious implementation of a Post-Converter the following solution is designed out of small components.

I tried to decompose your mapping use-case into smaller problems, and solve each using a component from the mapping framework.

Mechanics of a mapper

Looking at how a bean- or object- or model-mapper typically works, may shed some light on the issue at hand.

A mapper maps an object of type or source-class A to an new object of another type or target-class B.

ModelMapper - components to use

I tried to re-use examples from Baeldung's tutorial: Guide to Using ModelMapper. We need 3 steps:

(a) inferred property- or type-mapping for String name to equivalent target

(b) Expression Mapping: customized property-mapping for Map translation to String desc

(c) parameterized converter to translate and lookup by key String language and extract or convert the value ATranslation to String desc

The features we use are:

  1. Property Mapping for (a) and (b)
  2. Converters for (c)

1. property-mapping

In its simple form it infers the mapping of properties. It does so by mapping the properties of the source to the target. By default most mappers map the properties to ones equivalent by type or name:

field types and names perfectly match

In your case this worked for the property String name.

// GIVEN
Map<String, ATranslation> translations = Map.of(
  "en", new ATranslation("en-desc"),
  "nl", new ATranslation("nl-desc")
);
A entity = new A("John Wick", translations);

// SETUP (a) property-mapping by type
TypeMap<A, DTO> typeMap = modelMapper.createTypeMap(A.class, DTO.class);

// WHEN mapping the properties
DTO dto = modelMapper.map(entity, DTO.class);
    
// THEN desc expected to be null
assertNull(dto.getDesc());
assertEquals(entity.getName(), dto.getName());

2. conversion

In some use-cases you need some kind of conversion, when type or name of the properties to map can not be inferred by simple equivalence.

Define a translation factory-method to configure the converter. It creates a new converter or lets say "interpreter for the specified language" on demand.

// Converter: translates for specified language, means lookup in the map using a passed parameter
Converter<Map<String, ATranslation>, String> translateToLanguage(final String language) {
    return c -> c.getOrDefault(language, new ATranslation("")).getDesc();  // language needs to be final inside a lambda
}

This method can be used to translate or convert

// SETUP (a) property-mapping as default type-map
TypeMap<A, DTO> typeMap = modelMapper.createTypeMap(A.class, DTO.class);

// (b) map the property translations (even so other type) to desc
typeMap.addMapping(Source::getTranslation, Destination::setDesc);
// (c) add the converter to the property-mapper
typeMap.addMappings(
  mapper -> mapper.using(translateToLanguage("nl")).map(A::getTranslation, DTO::setDesc)
);

// WHEN mapping the properties
DTO dto = modelMapper.map(entity, DTO.class);

// THEN desc expected to be mapped to the specified language's translation
assertEquals(entity.getTranslation().get("nl").getDesc(), dto.getDesc());
assertEquals(entity.getName(), dto.getName());
hc_dev
  • 8,389
  • 1
  • 26
  • 38
  • Thanks for your answer. Your proposed Converter is only able to map one property, which results in n Converters for n properties. Is there a way to map multiple properties? (I updated the question to consist of two properties, to highlight the problem I face.) – SpazzMarticus Sep 16 '22 at 04:47