32

I have come across a situation where my mapping method has 3 parameters, and all the three are being used in deriving one of the properties of the target type.

I have created a default mapping method in the interface keeping the logic for deriving the property, now for calling this method, I could use an expression = "java( /*method call here*/ )" in the @Mapping annotation.

Is there any way to do this with any of the mapstruct annotation like @qualifiedByName, I tried commenting the annotation having expression property and used qualifiedByName, but it doesn't work :

@Mapper
public interface OneMapper {

    @Mapping(target="id", source="one.id")
    //@Mapping(target="qualified",expression = "java( checkQualified (one, projId, code) )")
    @Mapping(target="qualified",qualifiedByName="checkQualifiedNamed")
    OneDto createOne (One one, Integer projId, Integer val, String code);

    @Named("checkQualifiedNamed")
    default Boolean checkQualified (One one, Integer projId, Integer val, String code) {
        if(one.getProjectId() == projId && one.getVal() == val && one.getCode().equalsIgnoreCase(code)) {
            return Boolean.TRUE;
        }
        return Boolean.FALSE;                   
    }
}
Vivek Gupta
  • 2,534
  • 3
  • 15
  • 28

4 Answers4

39

Currently MapStruct does not support mapping methods with multiple source properties.

However, in your case you can use the @Context from 1.2.0. From what I understand the projId and the code are there just as helper of the mapping, and they are not used to map target properties from.

So you can do something like (It should work in theory):

@Mapper
public interface OneMapper {

    @Mapping(target="id", source="one.id")
    @Mapping(target="qualified", qualifiedByName="checkQualifiedNamed")
    OneDto createOne (One one, @Context Integer projId, @Context String code);

    @Named("checkQualifiedNamed")
    default Boolean checkQualified (One one, @Context Integer projId, @Context String code) {
        if(one.getProjectId() == projId && one.getCode().equalsIgnoreCase(code)) {
            return Boolean.TRUE;
        }
        return Boolean.FALSE;                   
    }
}

Another alternative would be to extract all those properties into a separate class and pass that along (this would allow for multiple parameters of the same type).

The class would look like:

public class Filter {

    private final Integer projId;
    private final Integer val;
    private final String code;

    public Filter (Integer projId, Integer val, String code) {
        this.projId = projId;
        this.val = val;
        this.code = code;
    }

    //getters
}

Your mapper will then look like:

@Mapper
public interface OneMapper {

    @Mapping(target="id", source="one.id")
    @Mapping(target="qualified", qualifiedByName="checkQualifiedNamed")
    OneDto createOne (One one, @Context Filter filter);

    @Named("checkQualifiedNamed")
    default Boolean checkQualified (One one, @Context Filter filter) {
        if(one.getProjectId() == filter.getProjId() && one.getVal() == filter.getVal() && one.getCode().equalsIgnoreCase(filter.getCode())) {
            return Boolean.TRUE;
        }
        return Boolean.FALSE;                   
    }
}

You can then call the mapper like: mapper.createOne(one, new Filter(projId, val, code));

Filip
  • 19,269
  • 7
  • 51
  • 60
  • thanks for the inputs @Filip, this seems very close to what I require, there is one more issue that I am facing here, in my real project there are 4 parameters, one entity object and 3 supporting properties, 2 of the three supporting properties are Integer, and there can not be multiple contexts with the same type, I tried making entity as Context and keeping one of the Integer in source but that ways it ignores all the other attributes in entity. I have updated the code snippet in my question if you could suggest something for this? – Vivek Gupta Dec 08 '17 at 08:02
  • I've updated the answer. Basically you can replace all the properties that you have with a wrapper class that would represent the `@Context`. That one is much more powerful and allows you to perform a to of things. Have a look at the documentation for [passing context / state objects](http://mapstruct.org/documentation/stable/reference/html/#passing-context) – Filip Dec 08 '17 at 16:50
  • In my application defining a new class for the filter purpose might be tough, however i'll check for the feasibility, and decide for one of the two, using Expression or defining a Filer. Thanks for your effort and time. – Vivek Gupta Dec 09 '17 at 17:56
  • I don't know your code, so I can't say. In any case not allowing same types is due to the fact that we can't really do a proper job in mapping which one woth which. By the way you should accept the answer if it answers your question – Filip Dec 09 '17 at 18:55
  • 2
    Thx a lot, it has helped to me to partially solve my issue. However it didn't work for me like that. I had to specified the source in `@Mapping(target="qualified", qualifiedByName="checkQualifiedNamed")`. How can you then pass the 2 parameters to `checkQualified`? (In my case the second parameter is not a @Context) – Eselfar Feb 22 '18 at 10:23
12

Since version 1.2 it is supported: http://mapstruct.org/documentation/stable/reference/html/#mappings-with-several-source-parameters

For example like this:

@Mapping(source = "person.description", target = "description")
@Mapping(source = "address.houseNo", target = "houseNumber")
DeliveryAddressDto personAndAddressToDeliveryAddressDto(Person person, Address address);

UPDATE

Since Mapstruct allows to map multiple source arguments into a single target, I would advise to extract the checkQualified method from the mapper and instead compute the outcome beforehand and invoke the mapper with the result of the checkQualified method. Mapstruct is a mapping library, and does not excel in performing arbitrary logic. It's not impossible, but personally, I don't see the value it adds in your particular case.

With the logic extracted your mapper could look like this:

@Mapper
public interface OneMapper {
    OneDto toOneDto(One one, Boolean qualified);
}

The mapper can be used like this:

One one = new One(1, 10, 100, "one");
boolean qualified = checkQualified(one, 10, 100, "one");
boolean notQualified = checkQualified(one, 10, 100, "two");
OneDto oneDto = mapper.toOneDto(one, isQualified);

For a full example, see: https://github.com/phazebroek/so-mapstruct/blob/master/src/main/java/nl/phazebroek/so/MapStructDemo.java

despot
  • 7,167
  • 9
  • 44
  • 63
BitfulByte
  • 4,117
  • 1
  • 30
  • 40
  • 4
    You haven't told anything about `qualifiedByName` (( – gavenkoa Aug 26 '20 at 14:40
  • 1
    Because there is no need to use it. qualifiedby is handy if you have multiple mapping methods with the same method signature but different behaviour. Anyway, you struck my curiosity and I have updated my answer with a more elaborate example to more closely resemble the OP code. – BitfulByte Aug 27 '20 at 19:51
  • if I'm not mistaken.. Does it matter which of `qualified` or `notQualified ` you meant instead of (undefined) `isQualified`? – cellepo Sep 01 '23 at 02:18
10

If you need to calculate a single target field based on multiple source fields from the same source object, you can pass the full source object to the custom mapper function instead of individual fields:

Example Entity:

@Entity
@Data
public class User {
    private String firstName;
    private String lastName;
}

Example DTO:


public class UserDto {
    private String fullName;
}

... and the mapper... Instead of passing a single source (firstName):

@Mapper
public abstract class UserMapper {

    @Mapping(source = "firstName", target = "fullName", qualifiedByName = "nameTofullName")
    public abstract UserDto userEntityToUserDto(UserEntity userEntity);


    @Named("nameToFullName")
    public String nameToFullName(String firstName) {
        return String.format("%s HOW DO I GET THE LAST NAME HERE?", firstName);
    }

... pass the full entity object (userEntity) as the source:

@Mapper
public abstract class UserMapper {

    @Mapping(source = "userEntity", target = "fullName", qualifiedByName = "nameToFullName")
    public abstract UserDto userEntityToUserDto(UserEntity userEntity);


    @Named("nameToFullName")
    public String nameToOwner(UserEntity userEntity) {
        return String.format("%s %s", userEntity.getFirstName(), userEntity.getLastName());
    }
cellepo
  • 4,001
  • 2
  • 38
  • 57
Wes Grant
  • 829
  • 7
  • 13
0

You can create a default method which calls internally mapstruct method with additional context params.in this way, you can obtain all parameters in 'qualifiedByName' part

@Mapper
public interface OneMapper {

    default OneDto createOne(One one, Integer projId, Integer val, String code) {
        return createOneWithContext(one,porjId,val,code
                                    one,porjId,val,code //as context params
        );
    }

    @Mapping(target="id", source="one.id")
    @Mapping(target="qualified",source="one",qualifiedByName="checkQualifiedNamed")
    OneDto createOneWithContext (One one, Integer projId, Integer val, String code
                     @Context One oneAsContext, 
                     @Context Integer projIdAsContext, 
                     @Context Integer valAsContext, 
                     @Context String codeAsContext
    
);

    @Named("checkQualifiedNamed")
    default Boolean checkQualified (One one, @Context Integer projId, @Context Integer val, @Context String code) {
        if(one.getProjectId() == projId && one.getVal() == val && one.getCode().equalsIgnoreCase(code)) {
            return Boolean.TRUE;
        }
    return Boolean.FALSE;                   
    }
}



```