javamodelmapper

Can I configure ModelMapper to use a converter for all properties with the source type?


I'm trying to configure ModelMapper to use a certain converter for all properties that implement a certain interface, across any entity class. I can get everything working by defining a TypeMap for each entity, and defining mappings for each relevant property, but I'm hoping there's a simpler way that doesn't need to be updated anytime a property is added.

So far I have something like this:

public interface Labelled { String getLabel; // returns a message code }
public class Vegetable implements labelled { ... }
public class Protein implements labelled { ... }
public class Starch implements labelled { ... }
public class Dinner { Vegetable veg; Starch starch; Protein protein; String name; // getters and setters }
public class DinnerDto { String veg; String starch; String protein; String name; // get/set }

public class LabelledToStringConverter extends AbstractConverter<Labelled, String> {
      private final MessageSource messageSource;

    public LabelledToStringConverter(MessageSource messageSource) {
        this.messageSource = messageSource;
    }

    @Override
    protected String convert(Labelled labelled) {
        final Locale locale = LocaleContextHolder.getLocale();
        final String messageCode = labelled.getLabel();
        return messageSource.getMessage(messageCode, null, messageCode, locale);
    }
}

@Configuration
public class Configuration {
    @Bean
    public ModelMapper modelMapper(final MessageSource messageSource) {
        final ModelMapper mapper = new ModelMapper();
        final Converter<Labelled, String> converter = new LabelledToStringConverter(messageSource);
        mapper.createTypeMap(Dinner.class, DinnerDto.class)
                .addMappings(map -> {
                    map.using(converter).map(Dinner::getVegetable, DinnerDto::setVegetable);
                    map.using(converter).map(Dinner::getProtein, DinnerDto::setProtein);
                    map.using(converter).map(Dinner::getStarch, DinnerDto::setStarch);
                });
        return mapper;
    }

...but I'd prefer to avoid mapping each individual field, and if possible each type. TypeMap.setPropertyConverter doesn't work when there are fields of other types. ModelMap.addConverter doesn't appear to do anything (I've tried both implementing Converter and extending AbstractConverter).

Is there another way to configure ModelMapper with my converter and have it apply that to all relevant fields without explicitly defining a mapping for each one?


Solution

  • Rather than calling ModelMapper.addConverter, which actually creates or adds to a TypeMap under the hood, a converter can be added to ModelMapper's implicit mapping by implementing ConditionalConverter and adding our converter directly to the underlying configuration. The converter class:

    // Our converter will implement ConditionalConverter instead
    public class LabelledToStringConverter implements ConditionalConverter<Labelled, String> {
          private final MessageSource messageSource;
    
        public LabelledToStringConverter(MessageSource messageSource) {
            this.messageSource = messageSource;
        }
    
        @Override
        public MatchResult match(Class<?> sourceType, Class<?> destinationType) {
            return Labelled.class.isAssignableFrom(sourceType) && String.class.isAssignableFrom(destinationType)
                    ? MatchResult.FULL : MatchResult.NONE;
        }
    
        @Override
        public String convert(MappingContext<Labelled, String> context) {
            if (context.getSource() == null) {
                return null;
            }
            final Locale locale = LocaleContextHolder.getLocale();
            final String messageCode = context.getSource().getLabel();
            return messageSource.getMessage(messageCode, null, messageCode, locale);
        }
    }
    

    The ModelMapper setup:

    final ModelMapper mapper = new ModelMapper();
    mapper.getConfiguration().getConverters().add(new LabelledToStringConverter(messageSource));
    // Do any other explicit type mapping necessary, e.g. where names don't match
    

    This will use the converter implementation anytime the conditions are matched, here anytime the model mapper is converting a property from Labelled to String.