MapStruct : enrich mapping annotation to define custom mapper

nbchn picture nbchn · Jun 2, 2017 · Viewed 19.1k times · Source

Here is my context: I am using byteBuddy to dynamically generate a class that transform an object into another one based on a external configuration. I encountered some issues and I wanted to find an alternative that's how I discovered MapStruct.

So I tried to build simple mapper and I wanted to know if there is the possibility to customize the annotation to add transformation functions. For instance I would like to have:

@Mapping(
    source = "mySourceField", 
    sourceType = "String",
    target = "myTargetField",
    targetType = "Integer",
    transformation = {"toInteger", "toSquare"}
),

And on the mapper implementation I would have something like :

 public TypeDest toSiteCatTag(TypeSrc obj) {

    if ( obj == null ) {

        return null;
    }

    TypeDest objDest = new TypeDest();

    objDest.myTargetField = Formatter.toSquare(
        Formatter.toInteger(obj.mySourceField));

    return objDest;
}

If someone can help me to achieve that I would be grateful and it would save me a lot of time.

Thanks in advance.

Answer

Filip picture Filip · Jun 2, 2017

If your 2 types TypeDest and TypeSrc are not generated on runtime, i.e. they are your compiled classes, then you can achieve what you want. MapStruct does not work on runtime as it is an Annotation Processor and generates java code. If there are some issues, like you are trying to map non-existing fields or there are ambiguous mapping methods then you will get compile time errors.

It will look something like:

@Mapper
public interface MyMapper {

    @Mapping(source = "mySourceField", target = "myTargetField", qualifiedByName = "myTransformation")// or you can use a custom @Qualifier annotation with qualifiedBy
    TypeDest toSiteCatTag(TypeSrc obj);

    @Named("myTransformation")// or your custom @Qualifier annotation
    default Integer myCustomTransformation(String obj) {
        return Formatter.toSquare(Formatter.toInteger(obj));
    }
}

There is a way to do it without the custom method in the mapper, but you'll need to have a method somewhere that applies the toInteger and then toSquare transformation. If you have a method with the signature Integer squaredString(String obj) in your Formatter.

e.g.

@Qualifier
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.CLASS)
public @interface SquaredString {}

public class Formatter {

    @SquaredString// you can also use @Named, this is just as an example
    public static Integer squaredString(String obj) {
        return toSquare(toInteger(obj));
    }
    //your other methods are here as well
}

Then you can do this in your mapper:

@Mapper(uses = { Formatter.class })
public interface MyMapper {

    @Mapping(source = "mySourceField", target = "myTargetField", qualifiedBy = SquaredString.class)
    TypeDest toSiteCatTag(TypeSrc obj);
}

The examples above will only be applied to the specific mapping since qualifedByName / qualified is used. If you want to have a different way of converting a String to Integer, then you can define a method either in your Mapper or in some of the classes in Mapper#uses with the signature Integer convertString(String obj). MapStruct will then delegate the conversion from String to Integer to this method.

You can find more about mapping with qualifiers here in the reference documentation, and here for more information regarding the mapping method resolution.