How to unit test if all mapstruct targets are really mapped (targeted)? - java

I am thinking about how to verify by a unit test to find out if all targets of the mapstruct mappings are active / in use...
I have the following construct:
#Mapper(uses = {DateMapper.class, StateMapper.class})
public abstract class Blah
implements IDtoContextMapper<SourceEntity, StuffDto> {
#Mapping(source = "id", target = "id")
#Mapping(source = "someNumber", target = "someStuffNumber")
#Mapping(target = "status", qualifiedByName = "mapStatus")
public abstract Blah toDto(
SourceEntity entity, #Context MappingContext mappingContext);
#Override
public Class<StuffDto> getClassOfDto() {
return StuffDto.class;
}
#Override
public Class<SourceEntity> getClassOfEntity() {
return SourceEntity.class;
}
}
And the generated map struct impl looks like:
#Override
public StuffDto toDto(SourceEntity entity, MappingContext mappingContext) {
StuffDto target = mappingContext.getMappedInstance( entity, StuffDto.class );
if ( target != null ) {
return target;
}
if ( entity == null ) {
return null;
}
StuffDto stuffDto = new StuffDto();
mappingContext.incrementDepth( entity, StuffDto );
stuffDto.setId( entity.getId() );
stuffDto.setStatus( stateMapper.mapStatus( entity.getStatus() ) );
stuffDto.setSomeStuffNumber( entity.getSomeNumber() );
mappingContext.updateOccurences( entity, stuffDto );
return stuffDto;
}
What I am looking for is a simple junit test that verfies that all targets (=all members) of the StuffDto are mapped somehow (by source->target mapping or by qualifiedByName).
Which approach could I begin to try?
Any ideas welcome, I just started to play around :-)

What MapStruct offers is to have compile errors if some target properties is not mapped.
This can be configured through the ReportingPolicy on the Mapper#unmappedTargetPolicy for the target properties or Mapper#unmappedSourcePolicy.
So in your case to make sure that all target properties are mapped you can use
Mapper(uses = {DateMapper.class, StateMapper.class}, unmappedTargetPolicy = ReportingPolicy.ERROR)
public abstract class Blah
implements IDtoContextMapper<SourceEntity, StuffDto> {
#Mapping(source = "someNumber", target = "someStuffNumber")
#Mapping(target = "status", qualifiedByName = "mapStatus")
public abstract Blah toDto(
SourceEntity entity, #Context MappingContext mappingContext);
#Override
public Class<StuffDto> getClassOfDto() {
return StuffDto.class;
}
#Override
public Class<SourceEntity> getClassOfEntity() {
return SourceEntity.class;
}
}
Note: I have removed #Mapping(source = "id", target = "id") since it is obsolete. MapStruct will implicitly map this property.

Related

Problem mapping classes with different attributes with MapStruct

I'm trying to make a mapping using MapStruct but I don't know how to deal with the fields from one to the other.
I have the classes below:
class DataDomain {
private List<Domain> data;
}
class Domain {
private String codDist;
private String numFun;
private String txtJust;
private Boolean valPar;
private LocalDateTime dateHr;
private Integer numPn;
}
class DataEntity {
private String codDist;
private String numFun;
private List<ParEntity> pares;
}
class ParEntity {
private String numFun;
private String txtJus;
private String indValPar;
private String dateHr;
private String numPn;
}
interface ParOutMapper{
ParOutMapper INSTANCE = Mappers.getMapper(ParOutMapper.class);
#Mapping(target = "data", source = "entity")
DataDomain map(DataEntity entity);
Domain toDomain(DataEntity entity);
default List<Domain> toList(DataEntity entity) {
return entity != null ? singletonList(toDomain(entity)) : new ArrayList<>();
}
default DataEntity map(DataDomain domain) {
return domain != null
&& domain.getData() != null
&& !domain.getData().isEmpty() ? toEntity(domain.getData().get(0)) : null;
}
DataEntity toEntity(Domain domains);
List<Domain> toDomainList(List<DataEntity> domainList);
}
That's what I've done so far, but it's giving divergence in the mapping because both have different structures and I ended up getting lost in how to apply their origin and destination field to field.
If possible and someone knows how to do it in an interesting correct way I would be very grateful.
I would suggest the following solution
#Mapper(unmappedTargetPolicy = ReportingPolicy.ERROR,
componentModel = "spring",
collectionMappingStrategy = CollectionMappingStrategy.ADDER_PREFERRED,
builder = #Builder(disableBuilder = true))
public interface ParOutMapper {
#Mapping(target = "data", source = "entity")
DataDomain map(DataEntity entity);
#Mapping(target = "txtJust", source = "pares", qualifiedByName = "txtJust")
#Mapping(target = "valPar", source = "pares", qualifiedByName = "valPar")
#Mapping(target = "dateHr", source = "pares", qualifiedByName = "dateHr")
#Mapping(target = "numPn", source = "pares", qualifiedByName = "numPn")
Domain toDomain(DataEntity entity);
default List<Domain> toList(DataEntity entity) {
return entity != null ? singletonList(toDomain(entity)) : new ArrayList<>();
}
default DataEntity map(DataDomain domain) {
return domain != null
&& domain.getData() != null
&& !domain.getData().isEmpty() ? toEntity(domain.getData().get(0)) : null;
}
#Mapping(target = "pares", ignore = true)
DataEntity toEntity(Domain domains);
List<Domain> toDomainList(List<DataEntity> domainList);
#AfterMapping
default DataEntity valuesToList(Domain domains, #MappingTarget DataEntity dataEntity){
ParEntity parEntity = new ParEntity();
parEntity.setDateHr(domains.getDateHr().toString()); // alternative call custom entity to list mapping here !
parEntity.setTxtJus(domains.getTxtJust());
parEntity.setNumPn(domains.getNumPn().toString());
parEntity.setNumFun(domains.getNumFun());
parEntity.setIndValPar(domains.getValPar().toString());
dataEntity.setPares(List.of(parEntity));
return dataEntity;
}
#Named("txtJust")
default String mapTxtJust(List<ParEntity> pares) {
return pares.get(0).getTxtJus(); // or custom mapping logic here
}
#Named("valPar")
default Boolean mapValPar(List<ParEntity> pares) {
return Boolean.valueOf(pares.get(0).getIndValPar()); // or custom mapping logic here
}
#Named("dateHr")
default LocalDateTime mapDateHr(List<ParEntity> pares) {
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm");
return LocalDateTime.parse(pares.get(0).getDateHr(),formatter); // or custom mapping logic here
}
#Named("numPn")
default Integer mapNumPn(List<ParEntity> pares) {
return Integer.valueOf(pares.get(0).getNumPn()); // or custom mapping logic here
}
}
Since you tagged your question with spring-boot i assume you are using it. Therefore i would suggest to use the provided component model by mapstruct in its configuration
I am unsure how you want to do your mapping of list to entitiy or entity to list. With my approach you can do it value by value or with the entire list. Both workes either way.
The solution compiles and workes for DataEntity toEntity(Domain domains); and Domain toDomain(DataEntity entity); i did not recognize any other problems since mapstruct is able to generate the required mappings.

How mapstruct map an entity to string and string to respective entity

I have the below structure and i want to map this using mapstruct.
class DTO
{
private Integer id;
String comment;
//getters & setters
}
class ParentEntity
{
private Integer id;
CommentEntity comment;
//getters & setters
}
class CommentEntity
{
private Integer id;
private String text;
//getters & setters
}
#Mapper(componentModel = "spring")
public interface SampleMapper
{
#Mapping(source = "entity.comment.text", target = "comment")
public DTO toDTO(final ParentEntity entity);
#Mapping(source = "dto.comment", target = "comment.text")
public ParentEntity toEntity(final DTO dto);
}
The below is the implementation generated by mapstruct for toDTO method
#Override
public DTO toDTO(ParentEntity entity) {
if ( entity == null ) {
return null;
}
DTO dto = new DTO();
dto.setComment( entityCommentText( entity ) );
....................
}
private String entityCommentText(ParentEntity entity) {
if ( entity == null ) {
return null;
}
Comment comment = entity.getComment();
if ( comment == null ) {
return null;
}
String text = comment.getText();
if ( text == null ) {
return null;
}
return text;
}
The below is the implementation generated by mapstruct for toEntity method
#Override
public ParentEntity toEntity(DTO dto) {
if ( dto == null ) {
return null;
}
ParentEntity entity = new ParentEntity();
entity.setComment( dtoToCommentEntity( dto ) );
.............
}
protected CommentEntity dtoToCommentEntity(DTO dto) {
if ( dto == null ) {
return null;
}
CommentEntity commentEntity = new CommentEntity();
commentEntity.setText( dto.getComment() );
return commentEntity;
}
My question is the toDTO() method is setting the comment only if the text is not null. But the toEntity() method is not checking for the null or empty text.
So if i get "comment":null in my DTO, it is creating a new comment object and setting text as null.
How to avoid this?
Can someone explain the behavior and suggest me the proper way to do it?
Thanks!
Like this:
#Mapper( componentModel = "spring" )
public interface MyMapper {
#Mapping(source = "entity.comment.text", target = "comment")
DTO toDTO(final ParentEntity entity);
// make sure the entire parameter dto is mapped to comment
#Mapping(source = "dto", target = "comment")
ParentEntity toEntity(final DTO dto);
// and MapStruct will select your own implementation
default CommentEntity dTOToCommentEntity(DTO dTO) {
if ( dTO == null ) {
return null;
}
CommentEntity commentEntity = null;
if ( dTO.getComment()!= null ) {
commentEntity = new CommentEntity();
commentEntity.setText( dTO.getComment() );
}
return commentEntity;
}
}

#Mapping with custom source and target variable is not working as expected when target has variable with same name as source

I have below Source and Target classes, i am using lombok for generating getters and setters
public class Target {
private String name;
private String newName;
}
public class Source {
private String name;
}
and let say if I want to map Source.name to Target.newName
I am using below Mapper class with #Mapping to specify source and target variables.
but once i compile the code and check the generated ClassMapperImpl
it is maping Source.name to Target.name and not to Target.new Name
#Mapper
public interface ClassMapper {
#Mapping(source = "name", target = "newName")
Target sourceToTarget(Source s);
}
I think they are both mapped when I try:
public class ClassMapperImpl implements ClassMapper {
#Override
public Target sourceToTarget(Source s) {
if ( s == null ) {
return null;
}
Target target = new Target();
target.setNewName( s.getName() );
target.setName( s.getName() );
return target;
}
}
Please use ignore on the name property.
#Mapper
public interface ClassMapper {
#Mapping(source = "name", target = "newName")
#Mapping(ignore = true, target = "name")
Target sourceToTarget(Source s);
}

Mapstruct: #InheritInverseConfiguration --> There is no suitable result type constructor for reversing this method

I try to use mapstruct feature #InheritInverseConfiguration. But it gives me the following error: There is no suitable result type constructor for reversing this method.
But the generated Impl look pretty good.
#Mapper(uses = {
BOIdMapper.class,
DateMapper.class,
CarVOFactory.class,
})
public interface CarMapper {
CarMapper INSTANCE = Mappers.getMapper(CarMapper .class);
#Mappings({
#Mapping(source = "VOKey", target = "id"),
#Mapping(source = "gueltAb", target = "gueltigAb")
})
Car map(CarVO carVO);
#InheritInverseConfiguration
CarVO map(Car car);
public class CarVOFactory {
public CarVO createCarVO() {
return VOFactory.createVO(CarVO.class);
}
}
}
This is the POJO Car
public class Car {
private String id;
private LocalDate gueltigAb;
...
}
This is the other Car-Class:
public interface CarVO extends ... {
void setVOKey (VOKey<CAR> voKey);
}

MapStruct : Dealing with HashMap entries as source

Here is my source object:
public class Record {
public final long captureTime;
public final String environnement;
public final String bundle;
public final String type;
public final String id;
public final Map<String,Object> meta;
}
Here is my destination object:
public class MappedRecord {
public final long captureTime;
public final String environnement;
public final String bundle;
public final String type;
public final String id;
public final String ip;
public final String server;
}
And my mapper looks like the following:
public interface RecordMapper {
RecordMapper MAPPER = Mappers.getMapper( RecordMapper.class );
#Mappings({
#Mapping(source = "captureTime", target = "captureTime"),
#Mapping(source = "environnement", target = "environnement"),
#Mapping(source = "bundle", target = "bundle"),
#Mapping(source = "type", target = "type"),
#Mapping(source = "id", target = "id"),
#Mapping(expression = "java((String) r.meta.get(\"ip\"))", target = "ip"),
#Mapping(expression = "java((String) r.meta.get(\"server\"))", target = "server"),
})
MappedRecord toMappedRecord(Record r);
}
For now it works well but I would like to know if there is a more "elegant" way to set Map entries as source. Because with this I did not manage to add transformation functions using the "qualifiedByName" property, it looks like it can only work when a "source" is specified. Did I misunderstand something ?
I tried the following approaches without satisfying results :
Overwrite getter for specific fields in my Record class
Add a transformation function with the "qualifiedByName" property. Something like:
#Named("metaGetter")
default String dataGetter (String property) {
return (String) r.meta.get(property);
}
But obviously this does not compile as the property name is not valid as a proper source.
Thanks for your time.
Write your own qualifier:
public class MappingUtil {
#Qualifier
#Target(ElementType.METHOD)
#Retention(RetentionPolicy.SOURCE)
public #interface Ip {
}
#Qualifier
#Target(ElementType.METHOD)
#Retention(RetentionPolicy.SOURCE)
public static #interface Server {
}
#Ip
public String ip(Map<String, Object> in) {
return (String)in.get("ip");
}
#Server
public String server(Map<String, Object> in) {
return (String)in.get("server");
}
}
and then just add to the mapping:
#Mapper( uses = MappingUtil.class )
public interface SourceTargetMapper {
SourceTargetMapper MAPPER = Mappers.getMapper( SourceTargetMapper.class );
#Mappings( {
#Mapping(source = "map", target = "ip", qualifiedBy = MappingUtil.Ip.class ),
#Mapping(source = "map", target = "server", qualifiedBy = MappingUtil.Server.class ),
} )
Target toTarget( Source s );
}

Categories

Resources