How to Map to Generic Type? - mapstruct

PageInfoDto
public class PageInfoDto<T> implements Serializable {
private int currentPageNum;
private int totalPageNum;
private int perPageNum;
private int totalItemNum;
private List<T> list;
}
Page
public class Page<T> implements Serializable {
private int current;
private int total;
private int size;
private int items;
private List<T> list;
}
say, i have a school list and a student list.
i want to map Page to PageInfoDto and map Page to PageInfoDto。
this is how my mapper looks like;
#Mapper( config = CentralConfig.class, uses = {StudentMapper.class, SchoolMapper.class}, componentModel = "spring")
public interface PageInfoMapper {
#Mappings({
#Mapping(target = "list", source = "pageInfo.records"),
#Mapping(target = "currentPageNum", source = "pageInfo.current"),
#Mapping(target = "totalPageNum", source = "pageInfo.pages"),
#Mapping(target = "perPageNum", source = "pageInfo.size"),
#Mapping(target = "totalItemNum", source = "pageInfo.total"),
})
PageInfoDto<SchoolDto> toPageInfoDto1(Page<School> pageInfo);
#Mappings({
#Mapping(target = "list", source = "pageInfo.records"),
#Mapping(target = "currentPageNum", source = "pageInfo.current"),
#Mapping(target = "totalPageNum", source = "pageInfo.pages"),
#Mapping(target = "perPageNum", source = "pageInfo.size"),
#Mapping(target = "totalItemNum", source = "pageInfo.total"),
})
PageInfoDto<StudentDto> toPageInfoDto2(Page<Student> pageInfo);
}
this obviously not a clever way to do . is there a better way to do this?

Mapstruct is a code generator. So it needs to know which types to construct in order to generate a method implementation. Having said that, you could do this smarter by using a base mapping method on which you define all the #Mapping annotations and ignore the generic type mapping. You still have the methods above but you just specify #InheritConfiguration
Alternatively you could consider playing around with an objectfactory using #TargetType to construct the proper generic type. I'm not sure whether that works with a generic mapping method signature. I'm not in the position to check it, but let me know if that works
E.g.
public interface BasePageMapper<S, DTO> {
#Mapping(target = "list", source = "records"),
#Mapping(target = "currentPageNum", source = "current"),
#Mapping(target = "totalPageNum", source = "pages"),
#Mapping(target = "perPageNum", source = "size"),
#Mapping(target = "totalItemNum", source = "total"),
PageInfoDto<DTO> toPageInfoDto(Page<S> pageInfo);
DTO toDto(S source);
}
#Mapper( config = CentralConfig.class, uses = StudentMapper.class, componentModel = "spring")
public interface StudentMapper extends BasePageMapper<Student, StudentDto> {
}
#Mapper( config = CentralConfig.class, uses = SchoolMapper.class, componentModel = "spring")
public interface SchoolMapper extends BasePageMapper<School, SchoolDto> {
}

Related

How to use #ConfigProperty in a MapStruct mapper?

I need a #ConfigProperty in my Mapper.
I cannot inject it, since the Mapper is an interface.
How can I solve this?
#ConfigProperty(name = "limit") // <- Does not work here
int limit;
#Mapping(target = "myTarget", source = "mySource", qualifiedByName = "myLimitMapper")
MyDto toDTO(Entity entity);
#Named(value = "myLimitMapper")
default int mapLimit(int number) {
if (number >= limit) return limit;
else return number;
}
I'm assuming you're using Quarkus, seeing the #ConfigProperty. But you can define an abstract mapper and use the cdi componentModel to let MapStruct create an #ApplicationScoped CDI bean. This is described in the dependency injection section of the MapStruct docs.
E.g. you have 2 classes to map:
public record UserModel(String name, int number) {}
public record UserDto(String name, int number) {}
In which limit is configured in the application.properties as 100.
Your mapper will look like:
#Mapper(componentModel = "cdi")
public abstract class UserMapper {
#ConfigProperty(name = "limit")
int limit;
#Mapping(target = "number", source = "number", qualifiedByName = "myLimitMapper")
abstract UserDto mapToDto(UserModel userModel);
#Named(value = "myLimitMapper")
int mapLimit(int number) {
if (number >= limit) return limit;
else return number;
}
}
You could run this as a #QuarkusTest to verify:
#QuarkusTest
public class LimitTest {
#Inject
UserMapper userMapper;
#Test
public void testMapping() {
UserModel userModel = new UserModel("John", 150);
UserDto userDto = userMapper.mapToDto(userModel);
assertEquals("John", userDto.name());
assertEquals(100, userDto.number());
}
}

Can I control mapping order with MapStruct?

I have a use case where I want to map objects to a ByteBuffer for transmission....
#Mapper
public static interface ByteBufferMapper {
public static ByteBufferMapper INSTANCE = Mappers.getMapper(ByteBufferMapper.class);
default byte toByte(ByteBuffer buffer) {
byte b = buffer.get();
return b;
}
}
public static class Dto {
public byte b;
public byte bb;
...
}
#Mapper(uses = ByteBufferMapper.class)
public static interface DtoMapper {
public static DtoMapper INSTANCE = Mappers.getMapper(DtoMapper.class);
#Mapping(source = "buffer", target = "bb")
#Mapping(source = "buffer", target = "b")
Dto byteBufferToDto(ByteBuffer buffer);
}
public static void main( String[] args ) {
ByteBuffer buffer = ByteBuffer.allocate(2).put((byte) 0xFF).put((byte) 0x00).flip();
System.out.println(DtoMapper.INSTANCE.byteBufferToDto(buffer));
}
Is there a way I can control MapStructs mapping order so the b variable gets populated with the 0xFF and bb gets populated with the 0x00 value?
Yes you can by means of #Mapping.dependsOn.
Like this:
#Mappings({
#Mapping(target = "surName", source = "lastName", dependsOn = "middleName"),
#Mapping(target = "middleName", dependsOn = "givenName"),
#Mapping(target = "givenName", source = "firstName")
})
AddressDto addressToDto(Address address);

What are the "built-in" filter options for ag-grid?

I see a reference to a built-in filter for "empty" in the doc below. Where can I find documentation for all built-in filter options?
https://www.ag-grid.com/javascript-grid-filtering/#adding-custom-filter-options
I couldn't find it in the documentation either.
However, you can look at the definition of the BaseFilter of the ag-grid here on the GitHub and get all the built-in filters.
export abstract class BaseFilter<T, P extends IFilterParams, M> extends Component implements IFilterComp {
public static EMPTY = 'empty';
public static EQUALS = 'equals';
public static NOT_EQUAL = 'notEqual';
public static LESS_THAN = 'lessThan';
public static LESS_THAN_OR_EQUAL = 'lessThanOrEqual';
public static GREATER_THAN = 'greaterThan';
public static GREATER_THAN_OR_EQUAL = 'greaterThanOrEqual';
public static IN_RANGE = 'inRange';
public static CONTAINS = 'contains'; //1;
public static NOT_CONTAINS = 'notContains'; //1;
public static STARTS_WITH = 'startsWith'; //4;
public static ENDS_WITH = 'endsWith'; //5;
// .....
}

MapStruct specifie sub mapping

This is my example.
public class Company {
....
private String companyName;
....
}
public class Employee {
....
private String name;
....
}
public class EmployeeDto {
....
private String name;
private String companyName;
....
}
#Mapper(componentModel = "spring")
public interface EmployeeDtoMapper {
#Mapping(target = "name", source = "source1.name")
#Mapping(target = "companyName", source = "source2.companyName")
EmployeeDto toDto(Employee source1, Company source2);
List<EmployeeDto> toDtos(List<Employee> sources, #Context Company source2);
}
I want that the method toDtos use toDto to map Employee to EmployeeDto but mapstruct generate a employeeDtoToEmployeeDto method.
How can I fix it ?
thanks
Mapstruct only allows for selecting submappings based on 1 source and 1 target. Hence the #Context annotation. This will effectively ignore that parameter, only handed it down to submapping..
But.. you can tackle your problem like this..
#Mapper(componentModel = "spring")
public interface EmployeeDtoMapper {
#Mapping(target = "name", source = "source1.name")
EmployeeDto toDto(Employee source1, #Context Company source2);
#AfterMapping
default afterToDto(#MappingTarget EmployeeDto target, #Context Company source2) {
target.setCompanyName( source2.getCompanyName();
}
List<EmployeeDto> toDtos(List<Employee> sources, #Context Company source2);
}
Note if you have multiple parameters in mapping source2, and you like to use MapStruct for that as well, you can define a new interface method mapping EmployDTO toDo(Company source) and call that from your default method.

How to reference an entity with inheritance in Spring Data REST when POSTing new entity?

I have entities with joined inheritance:
Supporter
#Entity
#Inheritance(strategy=InheritanceType.JOINED)
#JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "supporterType")
#JsonSubTypes({
#JsonSubTypes.Type(value = PersonSupporterEntity.class, name = "PERSON"),
#JsonSubTypes.Type(value = CompanySupporterEntity.class, name = "COMPANY")
})
#DiscriminatorColumn(name="supporter_type")
#Table(name = "supporter")
public class SupporterEntity extends UpdatableEntity {
private long id;
private SupporterType supporterType;
private PartnerEntity partner;
...
}
PersonSupporter
#Entity
#DiscriminatorValue("PERSON")
#Table(name = "person_supporter")
public class PersonSupporterEntity extends SupporterEntity {
...
}
CompanySupporter
#Entity
#DiscriminatorValue("COMPANY")
#Table(name = "company_supporter")
public class CompanySupporterEntity extends SupporterEntity {
...
}
I have another entity which references SupporterEntity
#Entity
#Table(name = "contact")
public class ContactEntity extends UpdatableEntity {
private long id;
private SupporterEntity supporter;
...
#ManyToOne // same error with #OneToOne
#JoinColumn(name = "supporter_id", referencedColumnName = "id", nullable = false)
public SupporterEntity getSupporter() {
return supporter;
}
...
}
Repositories
#Transactional
#RepositoryRestResource(collectionResourceRel = "supporters", path = "supporters")
public interface SupporterEntityRepository extends JpaRepository<SupporterEntity, Long> {
#Transactional(readOnly = true)
#RestResource(path = "by-partner", rel = "by-partner")
public Page<SupporterEntity> findByPartnerName(#Param("name") String name, Pageable pageable);
}
#Transactional
#RepositoryRestResource(collectionResourceRel = "person_supporters", path = "person_supporters")
public interface PersonSupporterEntityRepository extends JpaRepository<PersonSupporterEntity, Long> {
}
#Transactional
#RepositoryRestResource(collectionResourceRel = "company_supporters", path = "company_supporters")
public interface CompanySupporterEntityRepository extends JpaRepository<CompanySupporterEntity, Long> {
}
#Transactional
#RepositoryRestResource(collectionResourceRel = "contacts", path = "contacts")
public interface ContactEntityRepository extends JpaRepository<ContactEntity, Long> {
#Transactional(readOnly = true)
#RestResource(path = "by-supporter", rel = "by-supporter")
public ContactEntity findBySupporterId(#Param("id") Long id);
}
I use Spring Boot, Spring Data REST, Spring Data JPA, Hibernate, Jackson. When I try to create a new ContactEntity with a post request like this:
{
"supporter":"/supporters/52",
"postcode":"1111",
"city":"Test City 1",
"address":"Test Address 1",
"email":"test1#email.com",
"newsletter":true
}
I get this exception:
Caused by: com.fasterxml.jackson.databind.JsonMappingException: Unexpected token (VALUE_STRING), expected FIELD_NAME: missing property 'supporterType' that is to contain type id (for class com.facer.domain.supporter.SupporterEntity)
at [Source: HttpInputOverHTTP#4321c221; line: 1, column: 2] (through reference chain: com.facer.domain.supporter.ContactEntity["supporter"])
at com.fasterxml.jackson.databind.JsonMappingException.from(JsonMappingException.java:148) ~[jackson-databind-2.4.4.jar:2.4.4]
After 2 days of debugging I found a way, but I kinda guessed it. So if I post it like this:
{
"supporter":{
"supporterType":"PERSON",
"id":"52"
},
"postcode":"1111",
"city":"Test City 1",
"address":"Test Address 1",
"email":"test1#email.com",
"newsletter":true
}
It works, but I don't know why. What's wrong with the other request? It works like that everywhere else when the referenced entity does not have inheritance.
Just another workaround using a RelProvider:
Do not use #JsonTypeInfo
Create a RelProvider for SupporterEntity sub-classes
#Component
#Order(Ordered.HIGHEST_PRECEDENCE)
public class SupporterEntityRelProvider implements RelProvider {
#Override
public String getCollectionResourceRelFor(final Class<?> type) {
return "supporters";
}
#Override
public String getItemResourceRelFor(final Class<?> type) {
return "supporter";
}
#Override
public boolean supports(final Class<?> delimiter) {
return org.apache.commons.lang3.ClassUtils.isAssignable(delimiter, SupporterEntity.class);
}
}
See also:
https://jira.spring.io/browse/DATAREST-344
http://docs.spring.io/spring-hateoas/docs/current/reference/html/#configuration.at-enable
It looks like a Jackson problem. To be specific, it's the following code in com.fasterxml.jackson.databind.deser.SettableBeanProperty:
if (_valueTypeDeserializer != null) {
return _valueDeserializer.deserializeWithType(jp, ctxt, _valueTypeDeserializer);
}
return _valueDeserializer.deserialize(jp, ctxt);
Without inheritance _valueDeserializer.deserialize would be called which in turn runs some Spring code to convert the URI to a Supporter.
With inheritance _valueDeserializer.deserializeWithType is called and vanilla Jackson, of course, expects an object, not a URI.
If supporter was nullable you could first POST to /contacts and then PUT the supporter's URI to /contacts/xx/supporter. Unfortunately I am not aware of any other solution.
You should be able to workaround this by setting #JsonTypeInfo(use= JsonTypeInfo.Id.NONE) at the property/method level e.g.
Try with this:
#ManyToOne // same error with #OneToOne
#JoinColumn(name = "supporter_id", referencedColumnName = "id", nullable = false)
#JsonTypeInfo(use= JsonTypeInfo.Id.NONE)
public SupporterEntity getSupporter() {
return supporter;
}