Mapstruct can't map properties while using ObjectFactory - java

I need some community help to point me out where I'm wrong in my code... :)
I try to use mapstruct to map fields between 2 entities with the help of an #ObjectFactory.
Entity 1:
public class ScimUser {
#JsonProperty("addresses")
#Valid
private List<UserAddress> addresses = null;
}
Entity 2:
public class User {
#JsonProperty("postalAddress")
private PostalAddress postalAddress = null;
}
Mapper:
#Mapper(componentModel = "spring", uses = { AddressFactory.class })
public interface ScimUserMapper {
#Mapping(target = "postalAddress", source = "scimUser.addresses")
User toUser(ScimUser scimUser);
#Mapping(target = "addresses", source = "user.postalAddress")
ScimUser toScimUser(User user);
}
ObjectFactory:
#Component
public class AddressFactory {
#Autowired
private CountryMapper countryMapper;
#Autowired
private CountryRepository countryRepository;
#ObjectFactory
public PostalAddress toPostalAddress(List<UserAddress> addresses, #TargetType Class<PostalAddress> type) {
PostalAddress postalAddress = new PostalAddress();
if (addresses != null && !addresses.isEmpty()) {
UserAddress userAddress = addresses.stream().filter(UserAddress::isPrimary).findFirst().orElse(null);
if (userAddress == null) {
userAddress = addresses.get(0);
}
postalAddress.setAddressLine1(userAddress.getStreetAddress());
postalAddress.setPostCode(userAddress.getPostalCode());
postalAddress.setState(userAddress.getRegion());
postalAddress.setCity(userAddress.getLocality());
CountryJpa countryJpa = countryRepository.getCountryByIso2Code(userAddress.getCountry());
if (countryJpa != null) {
Country country = countryMapper.fromJPA(countryJpa);
postalAddress.setCountry(country);
}
}
return postalAddress;
}
#ObjectFactory
public List<UserAddress> toUserAddressList(PostalAddress address, #TargetType Class<List<UserAddress>> type) {
UserAddress userAddress = new UserAddress();
userAddress.setCountry(address.getCountry().getIso2());
userAddress.setFormatted("?");
userAddress.setLocality(address.getCity());
userAddress.setPostalCode(address.getPostCode());
userAddress.setPrimary(true);
userAddress.setRegion(address.getState());
userAddress.setStreetAddress(address.getAddressLine1());
userAddress.setType("?");
return Collections.singletonList(userAddress);
}
}
The code above gets me this error during source code generation:
Can't map property "java.util.List addresses" to "PostalAddress postalAddress". Consider to declare/implement a mapping method: "PostalAddress map(java.util.List value)".
Can't map property "PostalAddress postalAddress" to "java.util.List addresses". Consider to declare/implement a mapping method: "java.util.List map(PostalAddress value)".
It's not the first time that I struggle with using these object factories and I really don't get what I am doing wrong.
So if someone has an idea, I'd be glad to read it. :)

You are using the #ObjectFactory wrong. What you want to achieve is a custom mapping method.
#ObjectFactory needs to be used to create the target instance object. In your case if you just remove #ObjectFactory and #TargetType from your method then it should work correctly.
I have to stress out that you are doing quite some manual mapping there. You can easily provide methods for mapping between a single UserAddress and PostalAddress and just add wrappers for the collections.

Related

is it possitble to do mappingTarget Map from Object in mapstruct?

The application I used has lots of json fields in entity.
So I should update only the fields I know.
You can see the below code.
#Mapper(
config = CommonMapper.class,
nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE,
nullValueCheckStrategy = NullValueCheckStrategy.ALWAYS)
public interface ProductMapper {
#Mapping(target = "id", source = "id") // it is not working
void updateMapFromProduct(#MappingTarget Map<String, Object> map, Product product);
}
so I want to see the ProductMapperImpl like this.
is it possible?
#Generated(
value = "org.mapstruct.ap.MappingProcessor",
date = "2022-09-02T10:04:50+0900",
comments = "version: 1.4.2.Final, compiler: IncrementalProcessingEnvironment from gradle-language-java-7.5.jar, environment: Java 11.0.11 (AdoptOpenJDK)"
)
#Component
public class ProductMapperImpl implements ProductMapper {
#Override
public void updateMapFromProduct(Map<String, Object> map, Product product) {
if ( map == null ) {
return null;
}
map.put("id", product.getId());
}
}
Mapping from a Bean into a Map is currently not supported in MapStruct. There is an open feature request that you can follow to track this functionality.

How to get Java's object field's name from JSON fields name

I want to filter out some fields in the response. Filtering should be done before the Java object is serialised into the JSON.
Consider:
public class Entity {
#JsonProperty("some_property")
String someProperty;
#JsonProperty("nested_entity")
#OneToMany(mappedBy = "entity", fetch = FetchType.LAZY, cascade = CascadeType.ALL)
NestedEntity nestedEntity;
// other fields for eg fieldA, fieldB
}
API endpoint
get api/entity/{id}?fields=some_property,field_a
Now the ask is, in the o/p we should filter out only someProperty and fieldA. Like
{
"some_property": "foo",
"field_a": "bar"
}
But since these are JSON fields not Java object fields I can't filter or get these fields them by Reflection. Is there a way we can achieve this, i.e. filtering of Java object based on json fields ?
FYI: The advantage of filtering before serialization is that the lazy-fields' DB calls are saved unless these fields are required
Thanks in advance!
On the suggestion of #robocode using #JsonFilter and also to support empty fields or no fields filtering added JacksonConfiguration
#JsonFilter("entityFilter")
public class Entity {
#JsonProperty("some_property")
String someProperty;
// other fields for eg fieldA, fieldB
}
#Configuration
public class JacksonConfiguration {
public JacksonConfiguration(ObjectMapper objectMapper) {
objectMapper.setFilterProvider(new SimpleFilterProvider().setFailOnUnknownId(false));
}
}
public class FieldMapper {
#SneakyThrows
public static Dto getFilteredFields(Dto make, String fields[]) {
ObjectMapper objectMapper = new ObjectMapper();
if(ArrayUtils.isNotEmpty(fields)) {
FilterProvider filters = new SimpleFilterProvider().addFilter(
"entityFilter", SimpleBeanPropertyFilter.filterOutAllExcept(fields)
);
objectMapper.setFilterProvider(filters);
} else {
objectMapper.setFilterProvider(new SimpleFilterProvider().setFailOnUnknownId(false));
}
JsonNode j = objectMapper.readTree(objectMapper.writeValueAsString(make));
return objectMapper.convertValue(j, Dto.class);
}
}
// controller code to get the dto for api/entity/{id}?fields=some_property,field_a
Dto filteredFields = getFilteredFields(dto, fields);
return filteredFields

MapStruct - custom mapping of target field based on 2 or more different source objects

I am trying to figure out how to implement the following mapping:
class SuperComplexClass {
Long value;
String description;
}
class MapIntoMe {
// Many other fields that is also mapped
SuperComplexClass superComplexObject;
}
class MapFromMe {
ComplexClassPart1 complexClassPart;
}
class AdditionalData {
ComplexClassPart2 complexClassPart;
}
#Mapper
public interface SomeFancyMapper {
#Mapping(target = "superComplexObject", source = "{mfm.complexPart, ad.complexPart}",
qualifiedByName = "mapSuperComplexObject")
MapIntoMe mapFromMeIntoMe(MapFromMe mfm, AdditionalData ad);
#Named("mapSuperComplexObject")
default SuperComplexClass mapSuperComplexObject(ComplexPart1 p1, ComplexPart2 p2) {
SuperComplexClass superObject = new SuperComplexClass();
//some logic that calculates and fills superObject]
return superObject;
}
}
And now obviously expression like source = "{mfm.complexPart, ad.complexPart}" is not working, but it shows clearly what I would like to achieve.
So far I wasn't able to find the answer if that's possible with this approach and without some ugly workarounds.
Any ideas?
Currently it is not supported to reuse mapping methods with more than one parameter. That is why something like the expression you shared doesn't work.
However, you could use expression, #AfterMapping or #Context (in case you don't need to use AdditionalData for other mapping) to achieve what you need.
Using Expression
#Mapper
public interface SomeFancyMapper {
#Mapping(target = "superComplexObject", expression = "java(mapSuperComplexObject(mfm.getComplexPart(), ad.getComplexPart()))")
MapIntoMe mapFromMeIntoMe(MapFromMe mfm, AdditionalData ad);
default SuperComplexClass mapSuperComplexObject(ComplexPart1 p1, ComplexPart2 p2) {
SuperComplexClass superObject = new SuperComplexClass();
//some logic that calculates and fills superObject]
return superObject;
}
}
Using #AfterMapping
#Mapper
public interface SomeFancyMapper {
#Mapping(target = "superComplexObject", ignore = true)
MapIntoMe mapFromMeIntoMe(MapFromMe mfm, AdditionalData ad);
#AfterMapping
default void mapSuperComplexObject(#MappingTarget MapIntoMe target, MapFromMe mfm, AdditionalData ad) {
SuperComplexClass superObject = new SuperComplexClass();
//some logic that calculates and fills superObject]
return superObject;
}
}
Using #Context
#Mapper
public interface SomeFancyMapper {
#Mapping(target = "superComplexObject", source = "complexPart",
qualifiedByName = "mapSuperComplexObject")
MapIntoMe mapFromMeIntoMe(MapFromMe mfm, #Context AdditionalData ad);
#Named("mapSuperComplexObject")
default SuperComplexClass mapSuperComplexObject(ComplexPart1 p1, #Context AdditionalData ad) {
SuperComplexClass superObject = new SuperComplexClass();
//some logic that calculates and fills superObject]
return superObject;
}
}
Keep in mind that when using #Context the parameter annotated with that annotation cannot be used in Mapping#target. It is an additional context that can be passed to other mapping methods or lifecycle methods.

Mapping Hierrachical Beans using mapstruct

This is an extension to this question.
class Customer{
// distinct properties
}
class RetailCustomer extends Customer{
// distinct properties
}
class WholeSaleCustomer extends Customer{
// distinct properties
}
class CustomerDO {
// String custType ; // flag used to determine if Customer is wholeSale or Retail
//few properties same as Customer/WholeSaleCustomer/RetailCustomer
// few distinct properties
}
#Mapper
public interface CustomerMapper{
default Customer toCustomer(CustomerDO customerDO) {
String custType = customerDO.getCustType();
if("W".equalsIgnoreCase(custType)){
return toWholeSaleCustomer(customerDO);
}
else {
return toRetailCustomer(CustomerDO);
}
}
#Mappings({
#Mapping(source="a", target="b"),
#Mapping(source="c", target="d"),
#Mapping(source="m", target="m")
})
WholeSaleCustomer toWholeSaleCustomer(CustomerDO customerDO);
#Mappings({
#Mapping(source="e", target="f"),
#Mapping(source="g", target="h"),
#Mapping(source="n", target="n")
})
RetailCustomer toRetailCustomer(CustomerDO customerDO);
}
I need to map from CustomerDO to WholeSaleCustomer/RetailCustomer based on custType flag in CustomerDO. But above defined mapper doesn't work. It gives me below error while compiling
CustomerMapper.java:[23,34] Ambiguous mapping methods found for mapping property "com.domain.CustomerDO customerDO" to com.role.Customer: com.role.Customer: toCustomer
r(com.domain.CustomerDO customerDO), com.role.WholeSaleCustomer toWholeSaleCustomer(com.domain.CustomerDO wsCustomer), com.role.RetailCustomer toRetailCustomer(com.domain.CustomerDO wsCustomer)
But if I change toCustomer(CustomerDo customerDO) signature to toCustomer(Object customerDO) and remove either of toWholeSaleCustomer/toRetailCustomer, it works. It will only map either of two types. But I want both. I've similar case for Service Bean. There are serveral child Services. I should be able to map them all whenever they are required
What you are looking for is Mapping method selection based on qualifiers.
So if your customer objects look like:
class WholeSaleCustomer extends Customer {
// distinct properties
}
class CustomerDO {
// String custType ; // flag used to determine if Customer is wholeSale or Retail
//few properties same as Customer/WholeSaleCustomer/RetailCustomer
// few distinct properties
private CustomerDO customerDO;
}
Then you would have to tell MapStruct which method it needs to use to perform the mapping. So your mapper would look like:
#Mapper
public interface CustomerMapper {
#Named("baseCustomer")
default Customer toCustomer(CustomerDO customerDO) {
String custType = customerDO.getCustType();
if("W".equalsIgnoreCase(custType)){
return toWholeSaleCustomer(customerDO);
}
else {
return toRetailCustomer(CustomerDO);
}
}
#Mappings({
#Mapping(source="customerDO", qualifiedByName = "baseCustomer"),
#Mapping(source="c", target="d"),
#Mapping(source="m", target="m")
})
WholeSaleCustomer toWholeSaleCustomer(CustomerDO customerDO);
#Mappings({
#Mapping(source="customerDO", qualifiedByName = "baseCustomer"),
#Mapping(source="g", target="h"),
#Mapping(source="n", target="n")
})
RetailCustomer toRetailCustomer(CustomerDO customerDO);
}
#Named should come from org.mapstruct.Named.

Mapstruct self defined mapper AND automatically generated one

I understand Mapstruct allows me to define my own mapper logic, I am doing it like this:
#Mapper(componentModel = "spring")
public abstract class ProjectMapper {
public ProjectInfo map(ProjectEntity projectEntity) {
ProjectInfo projectInfo = new ProjectInfo();
projectInfo.setName(projectEntity.getName());
projectInfo.setDescription(projectEntity.getDescription());
// Specific logic that forces me to define it myself
if (projectEntity.getId() != null) {
projectInfo.setId(projectEntity.getId());
}
if (projectEntity.getOrganisation() != null) {
projectInfo.setOrganisation(projectEntity.getOrganisation().getName());
}
return projectInfo;
}
}
It works just fine, but I also want Mapstruct's generated mappers, but they have to be defined in an interface, is there a way to group up both of these mapper types?
NOTE: Untested. I used the following solution once in a Spring-Boot project using MapStruct version 1.0.0.Final.
Customizing standard mapping process is fairly well documented.
One of the way to customize your mappings are 'AfterMapping' and 'BeforeMapping' hooks:
#Mapper
public abstract class ProjectMapperExtension {
#AfterMapping
public void mapProjectEntityToProjectInfo(ProjectEntity projectEntity, #MappingTarget ProjectInfo projectInfo) {
if (projectEntity.getId() != null) {
projectInfo.setId(projectEntity.getId());
}
if (projectEntity.getOrganisation() != null) {
projectInfo.setOrganisation(projectEntity.getOrganisation().getName());
}
}
}
Then annotate the standard mapper interface with uses and exclude the custom mapped fields from the standard mapping:
#Mapper(componentModel = "spring", uses = {ProjectMapperExtension.class})
public interface ProjectMapper {
#Mapping(target = "id", ignore = true)
#Mapping(target = "organisation", ignore = true)
ProjectInfo mapProjectEntityToProjectInfo(ProjectEntity projectEntity);
}

Categories