Skip to content
Advertisement

Can I extend MapStruct methods?

I’m developing a library and I expect the library to have a mapper like so:

import org.mapstruct.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import spring.graphql.rest.rql.core.nodes.PropertyNode;
import spring.graphql.rest.rql.core.nodes.TraversalMapper;
import spring.graphql.rest.rql.example.dto.AccountDto;
import spring.graphql.rest.rql.example.models.Account;

import java.util.*;

@Mapper(componentModel = "spring", nullValueCheckStrategy = NullValueCheckStrategy.ALWAYS,
        nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE, uses = {TraversalMapper.class})
public abstract class AccountMapper {

    @Autowired
    @Qualifier("accountMapperImpl")
    protected AccountMapper accountMapper;

    @Autowired
    protected PostMapper postMapper;

    @Autowired
    protected PersonMapper personMapper;

    @Autowired
    protected CommentMapper commentMapper;

    @IterableMapping(qualifiedByName = "dynamicAccounts")
    public abstract Set<AccountDto> rqlToAccountDtos(Set<Account> entity, @Context StringBuilder currentPath, @Context List<PropertyNode> propertyNodes, @Context List<String> properties, @Context String property);

    // TODO: Implement automatic generation/addition of these mappers
    @Named("dynamicAccounts")
    @Mapping(target = "friends", expression = "java(properties.contains("friends") ? accountMapper.rqlToAccountDtos(entity.getFriends(), currentPath, propertyNodes, properties, "friends") : null)")
    @Mapping(target = "posts", expression = "java(properties.contains("posts") ? postMapper.toPostDtos(entity.getPosts(), currentPath, propertyNodes, properties, "posts") : null)")
    @Mapping(target = "comments", expression = "java(properties.contains("comments") ? commentMapper.toCommentDtos(entity.getComments(), currentPath, propertyNodes, properties, "comments") : null)")
    @Mapping(target = "person", expression = "java(properties.contains("person") ? personMapper.toPersonDto(entity.getPerson(), currentPath, propertyNodes, properties, "person") : null)")
    public abstract AccountDto rqlToAccountDto(Account entity, @Context StringBuilder currentPath, @Context List<PropertyNode> propertyNodes, @Context List<String> properties, @Context String property);

    @Named("dynamicAccountsDefaultCollection")
    public Set<AccountDto> toAccountDtosDefault(Collection<Account> entity, @Context List<PropertyNode> propertyNodes) {
        return rqlToAccountDtos(new HashSet<>(entity), new StringBuilder(), propertyNodes, new ArrayList<>(), "");
    }

    @Named("dynamicAccountsDefault")
    public AccountDto toAccountDtoDefault(Account entity, @Context List<PropertyNode> propertyNodes) {
        return rqlToAccountDto(entity, new StringBuilder(), propertyNodes, new ArrayList<>(), "");
    }
}

Which is meant for internal remapping. Now, I’d like to add a feature where the user can “override” this method, in the sense that they could for example add a statement to ignore the password of the account, while also calling the internal methods.

Few examples as reference points:

import org.mapstruct.*;
import spring.graphql.rest.rql.core.nodes.PropertyNode;
import spring.graphql.rest.rql.example.dto.AccountDto;
import spring.graphql.rest.rql.example.models.Account;

import java.util.List;

@Mapper(componentModel = "spring", nullValueCheckStrategy = NullValueCheckStrategy.ALWAYS,
        nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE)
public abstract class RealAccountMapper extends AccountMapper {

    @Mapping(target = "username", ignore = true)
    public abstract AccountDto toAccountDtoDefault(Account entity, @Context List<PropertyNode> propertyNodes);
}

@Mapper(componentModel = "spring", nullValueCheckStrategy = NullValueCheckStrategy.ALWAYS,
        nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE)
public abstract class RealAccountMapper {

    @Mapping(target = "username", ignore = true)
    @Mapper(uses = AccountMapper.class, qualifiedByName = "dynamicAccountsDefault")
    public abstract AccountDto toAccountDto(Account entity, @Context List<PropertyNode> propertyNodes);
}

The user should be able to define a property to ignore, or for example add with a specified source. From what I experimented, even without the overrides and such, it just generates a separate mapping method, instead of even trying to reuse the other one.

Is this possible without needing to implement custom methods on the user side which would call the internal methods? If not, is there a particular reason, or has it just not come up as a possible feature in MapStruct?

Advertisement

Answer

MapStruct generates code during compilation. This means that it is not possible to ignore certain properties and invoke another mapper for this, because the other mapper already has that implemented .

As a new feature we could add something to the @Mapper that would basically merge the configuration from the parent mapper.

e.g.

@Mapper(componentModel = "spring", nullValueCheckStrategy = NullValueCheckStrategy.ALWAYS,
        nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE, methodOverrideStrategy = OverrideStrategy.MERGE)
public abstract class RealAccountMapper extends AccountMapper {

    @Override
    @Mapping(target = "username", ignore = true)
    public abstract AccountDto toAccountDtoDefault(Account entity, @Context List<PropertyNode> propertyNodes);
}

This would mean that if this new methodOverrideStrategy (the name is not final) would mean that MapStruct will perform a merge of the annotation on the toAccountDtoDefault method.

For MapStruct it would be as if the user has written.

    @Named("dynamicAccountsDefault")
    @Override
    @Mapping(target = "username", ignore = true)
    public abstract AccountDto toAccountDtoDefault(Account entity, @Context List<PropertyNode> propertyNodes);

I would suggest that you raise this as a feature request in the MapStruct issue tracker.

Advertisement