Mapstruct self defined mapper AND automatically generated one - java

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

Related

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.

Ambiguous mapping methods using java Mapstruct

I'm working with java Mapstruct to mapping Entities to DTOs
I want to use one mapper from other mapper and both implement the same method with the same signature and because of that I'm getting "Ambiguous mapping methods found for mapping property"
I have already tried to implement the shared method on an interface and then extend the interface on both mappers but the problem remains
I'm guessing I will need to use some kind of qualifier. I searched on google and in the official documentation but I can't figure it out how to apply this technic
// CHILD MAPPER ***
#Mapper(componentModel = "spring", uses = { })
public interface CustomerTagApiMapper {
CustomerTagAPI toCustomerTagApi(CustomerTag customerTag);
default OffsetDateTime fromInstant(Instant instant) {
return instant == null ? null : instant.atOffset(ZoneOffset.UTC);
}
}
// PARENT MAPPER ***
#Mapper(componentModel = "spring", uses = { CustomerTagApiMapper.class })
public interface CustomerApiMapper {
CustomerAPI toCustomerApi(Customer customer);
default OffsetDateTime frmInstant(Instant instant) {
return instant == null ? null : instant.atOffset(ZoneOffset.UTC);
}
}
Using a qualifier is one way to solve this. However, in your case the problem is the fromInstant method which is actually an util method.
Why don't you extract that method to some static util class and tell both mapper to use that class as well?
public class MapperUtils {
public static OffsetDateTime fromInstant(Instant instant) {
return instant == null ? null : instant.atOffset(ZoneOffset.UTC);
}
}
Then your mappers can look like:
#Mapper(componentModel = "spring", uses = { MapperUtils.class })
public interface CustomerTagApiMapper {
CustomerTagAPI toCustomerTagApi(CustomerTag customerTag);
}
#Mapper(componentModel = "spring", uses = { CustomerTagApiMapper.class, MapperUtils.class })
public interface CustomerApiMapper {
CustomerAPI toCustomerApi(Customer customer);
}

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.

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.

Jackson JSON, filtering properties by path

I need to filter bean properties dynamiclly on serialization.
The #JsonView isn't an option for me.
Assume my Bean (as Json notation):
{
id: '1',
name: 'test',
children: [
{ id: '1.1', childName: 'Name 1.1' },
{ id: '1.2', childName: 'Name 1.2' }
]
}
I want to write the JSON with the following properties:
// configure the ObjectMapper to only serialize this properties:
[ "name", "children.childName" ]
The expected JSON result is:
{
name: 'test',
children: [
{ childName: 'Name 1.1' },
{ childName: 'Name 1.2' }
]
}
Finally I will create an annotation (#JsonFilterProperties) to use with Spring in my RestControllers, something like this:
#JsonFilterProperties({"name", "children.childName"}) // display only this fields
#RequestMapping("/rest/entity")
#ResponseBody
public List<Entity> findAll() {
return serviceEntity.findAll(); // this will return all fields populated!
}
Well, it's tricky but doable. You can do this using Jacksons Filter feature (http://wiki.fasterxml.com/JacksonFeatureJsonFilter) with some minor alterations. To start, we are going to use class name for filter id, this way you won't have to add #JsonFIlter to every entity you use:
public class CustomIntrospector extends JacksonAnnotationIntrospector {
#Override
public Object findFilterId(AnnotatedClass ac) {
return ac.getRawType();
}
}
Next step, make that filter of super class will apply to all of its subclasses:
public class CustomFilterProvider extends SimpleFilterProvider {
#Override
public BeanPropertyFilter findFilter(Object filterId) {
Class id = (Class) filterId;
BeanPropertyFilter f = null;
while (id != Object.class && f == null) {
f = _filtersById.get(id.getName());
id = id.getSuperclass();
}
// Part from superclass
if (f == null) {
f = _defaultFilter;
if (f == null && _cfgFailOnUnknownId) {
throw new IllegalArgumentException("No filter configured with id '" + filterId + "' (type " + filterId.getClass().getName() + ")");
}
}
return f;
}
}
Custom version of ObjectMapper that utilizes our custom classes:
public class JsonObjectMapper extends ObjectMapper {
CustomFilterProvider filters;
public JsonObjectMapper() {
filters = new CustomFilterProvider();
filters.setFailOnUnknownId(false);
this.setFilters(this.filters);
this.setAnnotationIntrospector(new CustomIntrospector());
}
/* You can change methods below as you see fit. */
public JsonObjectMapper addFilterAllExceptFilter(Class clazz, String... property) {
filters.addFilter(clazz.getName(), SimpleBeanPropertyFilter.filterOutAllExcept(property));
return this;
}
public JsonObjectMapper addSerializeAllExceptFilter(Class clazz, String... property) {
filters.addFilter(clazz.getName(), SimpleBeanPropertyFilter.serializeAllExcept(property));
return this;
}
}
Now take a look at MappingJackson2HttpMessageConverter, you will see that it uses one instane of ObjectMapper internaly, ergo you cannot use it if you want different configurations simultaneously (for different requests). You need request scoped ObjectMapper and appropriate message converter that uses it:
public abstract class DynamicMappingJacksonHttpMessageConverter extends MappingJackson2HttpMessageConverter {
// Spring will override this method with one that provides request scoped bean
#Override
public abstract ObjectMapper getObjectMapper();
#Override
public void setObjectMapper(ObjectMapper objectMapper) {
// We dont need that anymore
}
/* Additionally, you need to override all methods that use objectMapper attribute and change them to use getObjectMapper() method instead */
}
Add some bean definitions:
<bean id="jsonObjectMapper" class="your.package.name.JsonObjectMapper" scope="request">
<aop:scoped-proxy/>
</bean>
<mvc:annotation-driven>
<mvc:message-converters>
<bean class="your.package.name.DynamicMappingJacksonHttpMessageConverter">
<lookup-method name="getObjectMapper" bean="jsonObjectMapper"/>
</bean>
</mvc:message-converters>
</mvc:annotation-driven>
And the last part is to implement something that will detect your annotation and perform actual configuration. For that you can create an #Aspect. Something like:
#Aspect
public class JsonResponseConfigurationAspect {
#Autowired
private JsonObjectMapper objectMapper;
#Around("#annotation(jsonFilterProperties)")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
/* Here you will have to determine return type and annotation value from jointPoint object. */
/* See http://stackoverflow.com/questions/2559255/spring-aop-how-to-get-the-annotations-of-the-adviced-method for more info */
/* If you want to use things like 'children.childName' you will have to use reflection to determine 'children' type, and so on. */
}
}
Personally, I use this in a different way. I dont use annotations and just do configuration manually:
#Autowired
private JsonObjectMapper objectMapper;
#RequestMapping("/rest/entity")
#ResponseBody
public List<Entity> findAll() {
objectMapper.addFilterAllExceptFilter(Entity.class, "name", "children");
objectMapper.addFilterAllExceptFilter(EntityChildren.class, "childName");
return serviceEntity.findAll();
}
P.S. This approach has one major flaw: you cannot add two different filters for one class.
There's Jackson plugin called squiggly for doing exactly this.
String filter = "name,children[childName]";
ObjectMapper mapper = Squiggly.init(this.objectMapper, filter);
mapper.writeValue(response.getOutputStream(), myBean);
You could integrate it into a MessageConverter or similar, driven by annotations, as you see fit.
If you have a fixed number of possible options, then there is a static solution too: #JsonView
public interface NameAndChildName {}
#JsonView(NameAndChildName.class)
#ResponseBody
public List<Entity> findAll() {
return serviceEntity.findAll();
}
public class Entity {
public String id;
#JsonView(NameAndChildName.class)
public String name;
#JsonView({NameAndChildName.class, SomeOtherView.class})
public List<Child> children;
}
public class Child {
#JsonView(SomeOtherView.class)
public String id;
#JsonView(NameAndChildName.class)
public String childName;
}

Categories