Problem mapping classes with different attributes with MapStruct - java

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.

Related

Mapstruct "ignore" sets the value of target

do I something wrong or si this bug in Mapstruct? I am having this mapper definition:
#Mapping(target = "isEnabled", ignore = true)
#Mapping(target = "isCredentialsNonExpired", ignore = true)
#Mapping(target = "isAccountNonLocked", ignore = true)
#Mapping(target = "isAccountNonExpired", ignore = true)
#Mapping(target = "grantedAuthorities", ignore = true)
#Mapping(target = "userName", source = "email")
UserCredentials toUserCredentials(User user);
and MapStruct generates this implementation:
#Component
public class UserCredentialsMapperImpl implements UserCredentialsMapper {
#Override
public UserCredentials toUserCredentials(User user) {
if ( user == null ) {
return null;
}
String userName = null;
String password = null;
userName = user.getEmail();
password = user.getPassword();
boolean isEnabled = false;
boolean isCredentialsNonExpired = false;
boolean isAccountNonLocked = false;
boolean isAccountNonExpired = false;
Collection<? extends GrantedAuthority> grantedAuthorities = null;
UserCredentials userCredentials = new UserCredentials( grantedAuthorities, password, userName, isAccountNonExpired, isAccountNonLocked, isCredentialsNonExpired, isEnabled );
return userCredentials;
}
}
as you can see even though I have set isEnabled to ignore it is setting it to false instead of ignoring it...
EDITED:
After comments from Federico klez Culloca I realized, that MapStruct is doing everything right. The problem was in my logic. I wanted to have default values for those ignored fields, but because my class was immutable and I had all arg constructor, MapStruct had to insert some value into those fields and that is why it inserted there false value. I was able to solve this problem using Builder pattern, now MapStruct sets only the correct fields and default values are used for the rest. Thanks all for help.
#Override
public UserCredentials toUserCredentials(User user) {
if ( user == null ) {
return null;
}
UserCredentialsBuilder userCredentials = UserCredentials.builder();
userCredentials.userName( user.getEmail() );
userCredentials.password( user.getPassword() );
return userCredentials.build();
}

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

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.

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;
}
}

How to map values from map to object based on containsKey?

I have a map of values like so:
Map<String, Object> values = Map.of("name", "myName", "address", null);
I want to update an object like this:
class User {
String name;
String address;
String country;
}
Now I want the fields in User to be overwritten only if the source map has the key defined. So the address field should be set to null (because there's an explicit mapping to null), but the country field should not be changed (because there's no "country" key in the map).
This is similar to what nullValuePropertyMappingStrategy = IGNORE does, but not quite, as the check is a map.containsKey check instead of a standard null check.
Can I extend MapStruct so it can do this?
My MapStruct code:
#Mapper
interface MyMapper {
#Mapping(target = "name", expression = "java( from.getMap().get(\"name\") )")
#Mapping(target = "address", expression = "java( from.getMap().get(\"address\") )")
#Mapping(target = "country", expression = "java( from.getMap().get(\"country\") )")
To get(MapWrapper from, #MappingTarget To to);
}
MapStruct can't do this out of the box.
However, you could wrap your Map into a Bean. So something like this:
public class MapAccessor{
private Map<String, Object> mappings;
public MapAccessor(Map<String, Object> mappings) {
this.mappings = mappings;
}
public Object getAddress(){
return this.mappings.get("address");
}
public boolean hasAddress(){
return this.mappings.containsKey("address");
}
...
}
And then you could a normal mapper mapping WrappedMap to your targetbean and using the NullValuePropertyMappingStrategy..
Note: your mapper is a lot simpler than as well..
#Mapper( nullValuePropertyMappingStrategy = NullValueProperertyMappingStrategy.IGNORE )
interface MyMapper {
To get(MapAccessor from, #MappingTarget To to);
}

Map JSON string column of a JPA entity to Java object automatically

I have a JPA entity object with following structure:
#Table(name="item_info")
class Item(){
#Id
#GeneratedValue(strategy=GenerationType.IDENTITY)
private Long id;
#Column(name="item_name")
private String itemName;
#Column(name="product_sku")
private String productSku;
#Column(name="item_json")
private String itemJsonString;
#Transient
private ItemJson itemJson;
//Getters and setters
}
The itemJsonString field contains a json string value such as '{"key1":"value1","key2":"value2"}'
And the itemJson field contains the corresponding object which maps to the json string.
I get this entity object from database as follows:
Item item = itemRepository.findOne(1L); // Returns item with id 1
Now, the itemJson field is null since it is a transient field. And I have to set it manually using Jackson's ObjectMapper as follows:
itemJson = objectMapper.readValue(item.getItemJsonString(), ItemJson.class);
How can I make it such that when I do itemRepository.findOne(), it returns an Item object with the itemJson field mapped to the json String automatically?
Your best bet would be to implement a javax.persistence.Converter. It would look something like:
import javax.persistence.AttributeConverter;
import javax.persistence.Converter;
#Converter(autoApply = true)
public class LocalDateAttributeConverter implements AttributeConverter<ItemJson, String> {
#Override
public String convertToDatabaseColumn(ItemJson entityValue) {
if( entityValue == null )
return null;
ObjectMapper mapper = new ObjectMapper();
return mapper.writeValueAsString(entityValue);
}
#Override
public ItemJson convertToEntityAttribute(String databaseValue) {
if( databaseValue == null )
return null;
ObjectMapper mapper = new ObjectMapper();
return mapper.readValue(databaseValue, ItemJson.class);
}
}
I've used this with WildFly and didn't have to do anything except have it be in the war file I was deploying.
Here is the full working version of AttributeConverter + JPA + Kotlin.
Entity Class
In my case, database was mysql (8.x), which supports JSON as the underlying data type for column definition, and we can apply a custom converter using #Convert annotation.
#Entity
data class Monitor (
#Id
val id: Long? = null,
#Column(columnDefinition = "JSON")
#Convert(converter = AlertConverter::class)
var alerts: List<Alert> = emptyList(),
var active: Boolean = false
)
Converter Definition
Attribute converter needs to specify the conversion mechanism from data to db and reverse. We are using Jackson to convert a java object into String format and vice versa.
#Converter(autoApply = true)
class AlertConverter : AttributeConverter<List<Alert>, String> {
private val objectMapper = ObjectMapper()
override fun convertToDatabaseColumn(data: List<Alert>?): String {
return if (data != null && !data.isEmpty())
objectMapper.writeValueAsString(data)
else ""
}
override fun convertToEntityAttribute(dbData: String?): List<Alert> {
if (StringUtils.isEmpty(dbData)) {
return emptyList()
}
return objectMapper.readValue(dbData, object : TypeReference<List<Alert>>() {})
}
}
You could postLoad callback for manipulating entity after it's loaded. So try something like this inside your entity class
#PostLoad
public void afterLoad() {
ObjectMapper mapper = new ObjectMapper();
itemJson = mapper.readValue(item.getItemJsonString(), ItemJson.class);
}

Categories

Resources