javaspringgenericsmapstruct

Mapstruct - Generic Mapper and default value for null


I have several DTOResponse class, each one of them extending MiniDTOResponse.

@Data
@NoArgsConstructor
public class MiniDTOResponse extends EmptyDTOResponse {
    private boolean exists; 
}

@Data
public class ServicesDTOResponse extends MiniDTOResponse {
    private boolean light;
    private ToiletDTOResponse toilets;
    private int barCounterNumber;
}
@Data
public class ToiletDTOResponse extends MiniDTOResponse {
    private int numMaleRestrooms;
    private int numFemaleRestrooms;
}
@Data
public class VIPDTOResponse extends MiniDTOResponse {
    private int numVIPSeats;
    private boolean isAccesible;
}

and so on. There are also bigger DTOs, with Ids and dates of deletion, such as

@Data
@EqualsAndHashCode(callSuper = true)
public class BasicDTOResponse extends BasicDTO {
    private Date dateDeletion;
    private String userDeletion;
}

@Data
public class AttendanceDTOResponse extends BasicDTOResponse {
    private int numStanding;
    private int numSeats;
    private ServicesDTOResponse services;
    private VIPDTOResponse vip;
}

and different Mapstruct mappers for each DTO.

My goal is to return something like this, assuming there are services and toilets but no vip

{
    "id": "39F47F8B83F5653CE0631F1EA8C0D44F",
    "dateDeletion": null,
    "userDeletion": null,
    "numStanding": 30000,
    "numSeats": 20000,
    "services": {
        "exists": true,
        "light": false,
        "toilets": {
            "exists": true,
            "numMaleRestrooms" : 5,
            "numFemaleRestrooms" : 6
        },
        "barCounterNumber": 6,
    },
    "vip": {
        "exists": false,
        "numVIPSeats": 0,
        "isAccesible": false,
    }
}

so i created this

@Mapper(componentModel = "spring")
public interface GenericMapper {

    default boolean exists(String id) {
        return StringUtils.isNotBlank(id);
    }
    default boolean active(ZonedDateTime dateDeletion) {
        return dateDeletion == null;
    }

    default boolean computeExists(BaseEntity entity) {
        return entity != null && exists(entity.getId()) && active(entity.getFechaBaja());
    }
    
    @AfterMapping
    default <K extends BaseEntity, T extends MiniDTOResponse> void setExists(@MappingTarget T dto, K entity) {
        boolean exists = computeExists(entity);
        dto.setExists(hay);
    }

    @ObjectFactory
    default <K extends BaseEntity, T extends MiniDTOResponse> T createDto(K source, @TargetType Class<T> dtoClass) {
        try {
            T dto = dtoClass.getDeclaredConstructor().newInstance();
            setExists(dto, source);
            return dto;
        } catch (Exception e) {
            throw new RuntimeException("Couldn't create DTO", e);
        }
    }

    default <T extends MiniDTOResponse> T createDTOwithExitsFalse(Class<T> dtoClass) {
        try {
            T dto = dtoClass.getDeclaredConstructor().newInstance();
            dto.setExists(false);
            return dto;
        } catch (Exception e) {
            throw new RuntimeException("Couldn't create DTO", e);
        }
    }
}


public interface AttendanceMapper extends GenericMapper {
    @Mapping(target = "numStanding", source = "standing")
    @Mapping(target = "numSeats", source = "seats")
    @Mapping(target = "services", source = "installations", defaultExpression = "java(createDTOwithExitsFalse(ServicesDTOResponse.class))")
    @Mapping(target = "vip", source= "vipFacilities", defaultExpression = "java(createDTOwithExitsFalse(VIPDTOResponse.class))")
    AttendanceDTOResponse toDto (Attendance source);
}

and works like a charm without having to write lots of defaults on every Mapper.

My problem is that its not working with Toilets. If I declare that AttendanceMapper uses ToiletsMapper (what is expected) and both of them (ToiletsMapper and AttendanceMapper) extends GenericMapper, i get a Ambiguous factory methods found for creating ServicesDTOResponse: T createDto(K source, @TargetType Class<T> dtoClass), T ToiletMapper.createDto(K source, @TargetType Class<T> dtoClass). See https://mapstruct.org/faq/#ambiguous for more info. as both of them are extending the GenericMapper.

What can I do? I'd love to do it in ageneric way, as there are like 40 DTORespones. Maybe there is a cleaner way of resolving the exists and the default of every attribute without using expressions, I'm willing to know how to do it!


Solution

  • Instead of extending from other mapper import them.

    @Mapper( uses = { GenericMapper.class } )
    public interface AttendanceMapper {
    }