Mapping Hierrachical Beans using mapstruct - java

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.

Related

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.

How to map Collections in Mapstruct when generic type has Inheritance structure?

I've the following bean structure in my project.
public class Account{
// properties
// setters
// getters
}
public class AccountType1 extends Acccount{
// properties
// setters
// getters
}
public class AccountType3 extends Acccount{
// properties
// setters
// getters
}
public class CustomerProfile {
Customer customer;
List<Account> accounts;
List<Service> services;
}
I've similar structure for Customer and services. One parent and multiple implementation. My application is a middle ware application. I don't know what kind of run time objects our app gets from other webservice calls(Bean model is same across application). List can contain any implementation. It can either be Account or AccountType1 or AccountType2. Same is the case with Service. Parent will have common fields and each implementation will have specific fields. We will have a new flow for each new client i.e., consumer. Also field requirement is different. So we need to have separate CustomerProfile and corresponding Account and Service mappers. Now for client1, they may need generic Account or AccountType1 or AccountType2 or AccountTypeN or all of them. So code should be generic like whatever type of classes I give {AccountType1.class, AccounTypeN.class} in config, it should map those objects only from the list. Since AccountType1 extends Account, It should also take care of parent class fields. I'm currently doing this following way.
#Mapper(config = GlobalConfig.class)
public interface CustomerProfileMapper{
#Mappings({
#Mapping( target = "customer", source = "customer"),
#Mapping( target = "accounts", source = "accounts"),
#Mapping( target = "services", source = "services")
})
CustomerProfile mapCustomerProfile(CustomerProfile customerProfile);
#IterableMapping(qualifiedByName = "mapAccount")
List<Account> mapAccounts(List<Account> accounts);
#Named("mapAccount")
default Account mapAccount (Account account){
if(account instanceof AccountType1){
mapAccountType1((AccountType1)account);
}
else if(account instanceof AccountType2){
mapAccountType2((AccountType2)account);
}
else {
mapBaseAccount(account);
}
}
#Mappings{...}
AccountType1 mapAccountType1(AccountType1 account);
#Mappings{...}
AccountType2 mapAccountType2(AccountType2 account);
}
#Mappings{...}
Account mapBaseAccount(Account account);
}
But This code will be redundant as I've to write for each flow for different CustomerProfileMappers. I want code to be generic and can used as a configuration. Re-usability is the concern here. How to address this problem? Basically I want to do something like below.
#IterableMapping( mappingClasses= {AccountType1.class, AccountType2.class, Account.class})
List<Account> mapAccounts(List<Account> accounts);
#Mappings{...}
AccountType1 mapAccountType1(AccountType1 account);
#Mappings{...}
AccountType2 mapAccountType2(AccountType2 account);
}
#Mappings{...}
Account mapBaseAccount(Account account);
So mapStruct should generate code like how I handled this currently. It should generate the mapping method to handle all the specified classes defined in mappingClasses property. It should also look for individual class specific mapping methods in the mappers. If found call them or else generate a mapping method. This is required because I have similar thing with Customer and Service. I don't want too much hand written code in mappers and We have tens of CustomerProfileMappers different for each flow. And they keep increasing with each release. I've gone through the complete technical documentation of MapStruct. But i couldn't find a way to do this. Or this could be a new FR?
You could try to externalise the switch on account.
#Mapper(config = GlobalConfig.class)
public interface CustomerProfileMapper{
#Mappings({
#Mapping( target = "customer", source = "customer"),
#Mapping( target = "accounts", source = "accounts"),
#Mapping( target = "services", source = "services")
})
CustomerProfile mapCustomerProfile(CustomerProfile customerProfile, #Context MyMappingContext ctx);
#Mappings{...}
AccountType1 mapAccountType1(AccountType1 account);
#Mappings{...}
AccountType2 mapAccountType2(AccountType2 account);
}
#Mappings{...}
Account mapBaseAccount(Account account);
}
public class MyMappingContext {
// or whatever component model you prefer
CustomerProfileMapper mapper = CustomerProfileMapper.INSTANCE;
#AfterMaping
public void mapAccount (CustomerProfile source, #MappingTarget CustomerProfile target){
for ( Account account : source.getAccounts() ){
if(account instanceof AccountType1){
mapper.mapAccountType1((AccountType1)account);
}
else if(account instanceof AccountType2){
mapper.mapAccountType2((AccountType2)account);
}
else {
mapper.mapBaseAccount(account);
}
}
}
}
You could even do a call-back from this context to the mapper. If you want you could express the generic behaviour of the mapper itself as a generic mapper. So define the signatures in a CommonCustomerProfileMapper and let CustomerProfileMapper1 inherit from that one and define the mappings.
Wrt to a new feature in MapStruct: like said: I'm not sure how much common interest there is for such a feature, but you can always issue a feature request.

Mapstruct can't map properties while using ObjectFactory

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.

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