MapStruct: How to pass input object to expression? - mapstruct

In MapStruct version 1.1.0.Final, this was possible....
#Mappings({
#Mapping(target = "transaction.process.details", expression = "java(MappingHelper.mapDetails(request))"),
//more mappings
})
Response requestToResponse(Request request);
It was possible, since the mapDetails method was (by coincidence?) generated into the requestToResponse method. That's why request was not null.
Now, since 1.1.0.Final didn't work with Lombok, I had to upgrade to 1.2.0.CR2. With this version, the mapDetails will be generated into a separate method where request is not passed, so request is null within this method now and I get a NPE with the expression. (It's a sub-sub-method of requestToResponse now.)
Did I misuse the expression, so did it just work by coincidence, or does the new version has a bug? If no bug, how do I have to pass the request instance to the expression properly?

You were / are misusing the expression. What you need to do is to map your target to your source parameter.
#Mapper(uses = { MappingHelper.class })
public interface MyMapper {
#Mappings({
#Mapping(target = "transaction.process.details", source = "request"),
//more mappings
})
Response requestToResponse(Request request);
}
MapStruct then should create intermediary methods and use the MappingHelper and invoke the mapDetails method. In case you have multiple methods that map from Request to whatever type details are then you are going to need to used qualified mappings (see more here in the documentation).
It will look something like:
public class MappingHelper {
#Named("mapDetails") // or the better type safe one with the meta annotation #Qualifier
public static String mapDetails(Request request);
}
And your mapping will look like:
#Mapper(uses = { MappingHelper.class })
public interface MyMapper {
#Mappings({
#Mapping(target = "transaction.process.details", source = "request", qualifiedByName = "mapDetails"), //or better with the meta annotation #Qualifier qualifiedBy
//more mappings
})
Response requestToResponse(Request request);
}

Related

Java: Mapping DTOs hierarchy using MapStruct

I have Pet, Dog and Cat entity classes. Dog and Cat classes extend Pet.
Also I have PetDTO, DogDTO and CatDTO annotated with #JsonSubtype so Jackson resolves well the class of the dtos.
I want to write a mapper using MapStruct that takes a PetDTO entity (can be a DogDTO or a CatDTO) and returns a Dog or a Cat.
For me in this case, the main goal of using a mapping library is to avoid awful code using instanceof.
Any idea? Thanks!
Not currently possible out-of-the-box - see this ticket in mapstruct's GitHub: #366 Support for abstract class mapping or classes with base class. You can try to push it there or maybe contribute this feature yourself. Looks like a reasonable feature to ask for.
I guess that with the current state of affairs this is your best option:
#Mapper
public interface PetMapper {
default PetDTO toPetDto(Pet pet) {
if (pet instanceof Dog) {
return toDogDTO((Dog) pet);
}
if (pet instanceof Cat) {
return toCatDTO((Cat) pet);
}
throw new IllegalArgumentException("Unknown subtype of Pet");
}
default Pet toPetEntity(PetDTO petDTO) {
if (petDTO instanceof DogDTO) {
return toDogEntity((DogDTO) petDTO);
}
if (petDTO instanceof CatDTO) {
return toCatEntity((CatDTO) petDTO);
}
throw new IllegalArgumentException("Unknown subtype of PetDTO");
}
DogDTO toDogDTO(Dog dog);
Dog toDogEntity(DogDTO dogDTO);
CatDTO toCatDTO(Cat cat);
Cat toCatEntity(CatDTO catDTO);
}
The way I ended up implementing a Mapper for a similar case as above was using a combination of a switch-type, with MapStruct Update Existing and creation Mappers.
In my case a property on the source object dictated the subclass we had to generate.
I initially had different mappers for each subtype, but the duplication of the common mapped properties just seemed wrong. So I came up with the following, leveraging the ability of MapStruct to use updating mappers in order to tackle the common parent type properties:
import org.mapstruct.*;
#Mapper
#Named("QualifierPetMapper")
public interface PetMapper {
#Named("DelegatingPetMapper")
#BeanMapping(ignoreByDefault = true)
default PetTarget mapPet(PetSource petSource) {
switch (petSource.getPetType()) {
case "DOG":
DogTarget dogTarget = mapDog(petSource);
updatePet(dogTarget, petSource);
return (dogTarget);
case "CAT":
CatTarget catTarget = mapCat(petSource);
updatePet(catTarget, petSource);
return (catTarget);
default:
throw new CustomException("Unsupported Pet type: "+ petSource.getPetType());
}
}
#BeanMapping(ignoreByDefault = true)
// Specific mappings for Dog
#Mapping(target = "dogfood.name", source = "dogfoodName")
DogTarget mapDog(PetSource petSource);
#BeanMapping(ignoreByDefault = true)
// Specific mappings for Cat
#Mapping(target = "fish.name", source = "favoriteFish")
CatTarget mapCat(PetSource petSource);
#Named("RootPetMapper")
#BeanMapping(ignoreByDefault = true)
// Common properties for Pet
#Mapping(target = "weight.value", source = "weightValue")
#Mapping(target = "name.value", source = "petName")
#Mapping(target = "color", source = "mainColor")
void updatePet(#MappingTarget PetTarget petTarget, PetSource petSource);
}

using mapstruct how to map an object to a list of objects with spring component mappers

I am trying to map an object A to a list of object B.
I have a mapper which maps from object A to object B.
I have tried a number of different ways for example
Trying to create a list with one object A using 'expressions'
And 'qualifiedByName' but this does not work because I think
when you use expressions/qualifiedByName you can not use
Custom mappers ( I could be wrong here ?)
I also tried to call the mapper from the #aftermapper method using
The ‘mappers.getMapper’ to get a handle on the target mapper
But I found that the spring beans used in the mapper
Where not being populated. Mapping in the aftermapping makes
Sense in that I can call the target mapper with the source
And then add the target to the list. So I am hoping that there
is another Way to get a handle on the mapper component from my mapper.
All my mappers use
#Mapper(componentModel="spring",
All suggestions are welcome
Below is an code sample showing the problem.
Regards,
Declan
// SPECIESTOLOGSPECY.JAVA
// From this mapper I want to call SpecyToLogDeclarationMapperApi mapper
// to map ‘species’ to ‘logdeclarations’ which is a list of logdeclaration
// you can see want I am trying to do in the aftermapping method
// where I map species to logdeclaration and then put this into a list
// unfortunately I need other mapping components (ConfigMapperFromCode & SpecyToFishDeclarationMapperApi)
// to be autowired into SpecyToLogDeclarationMapperApi and this is not happening.
// is there another way to get a handle to the spring managed
// SpecyToLogDeclarationMapperApi mapper ?
#Mapper(componentModel="spring",
uses = {
ConfigMapperFromCode.class,
GeoInfoMapper.class,
SpecyToLogDeclarationMapperApi.class
})
public interface SpeciesToLogSpecy {
SpecyToLogDeclarationMapperApi MAPPER = Mappers.getMapper(SpecyToLogDeclarationMapperApi.class);
#Mappings(
{
#Mapping(target="createdate", expression = "java(java.sql.Timestamp.valueOf(java.time.LocalDateTime.now()))"),
#Mapping(target="speciesid", qualifiedByName={"ConfigMapperFromCode", "speciesIdFromCodeAsDecimal"}, source = "species.speciesCode"),
#Mapping(target="unitweight", constant = "1"),
#Mapping(target = "inactiveind", constant = "N"),
#Mapping(target = "unitdefaultind", constant = "Y"),
#Mapping(target = "sectiontypeid", expression = "java(new BigDecimal(ie.gov.agriculture.fisheries.logsheet.mapping.constants.MappingConstants.LOG_SPECIES_SECTION_TYPE_SHEETDECLARATION))"),
#Mapping(target = "unituomid", expression = "java(new BigDecimal(ie.gov.agriculture.fisheries.logsheet.mapping.constants.MappingConstants.LOGSHEET_CATCHUNITS_KG))"),
#Mapping(target="catchtypeid", qualifiedByName={"ConfigMapperFromCode", "returnCatchTypeId"}, source = "species.spType"),
#Mapping(target="legalfishsizetypeid", qualifiedByName={"ConfigMapperFromCode", "legalFishSizeTypeIdFromCode"}, source = "species.fishSizeClass"),
#Mapping(target="presenttypeid", qualifiedByName={"ConfigMapperFromCode", "presentationTypeIdFromCode"}, source = "species.presentation.presentationType"),
//#Mapping(target="logdeclarations", source = "species")
}
)
Logspecy speciesToLogspecy(Species species, #Context ExtraFields extraFields);
#AfterMapping
default void afterMap(#MappingTarget Logspecy logspecy, #Context ExtraFields extraFields){
Logdeclaration logDeclaration = MAPPER.SpeciesToLogDeclarations(species, extraFields);
List<Logdeclaration> logdeclarations = new ArrayList<Logdeclaration>();
logdeclarations.add(logDeclaration);
logSpecy.setLogdeclarations(logdeclarations);
{
// SPECYTOLOGDECLARATIONMAPPERAPI.JAVA
#Mapper(componentModel="spring",
uses = {
ConfigMapperFromCode.class,
SpecyToFishDeclarationMapperApi.class
}
)
public interface SpecyToLogDeclarationMapperApi {
#Mappings(
{
#Mapping(target="createdate", expression = "java(java.sql.Timestamp.valueOf(java.time.LocalDateTime.now()))"),
#Mapping(target="geartypeid", qualifiedByName={"ConfigMapperFromCode", "gearIdFromCode"}, source = "species.gearType"),
#Mapping(target="fishcount", source = "species.qty"),
#Mapping(target = "inactiveind", constant = "N"),
#Mapping(target="packagetypeid", qualifiedByName={"ConfigMapperFromCode", "packagingTypeIdFromCode"}, source = "species.presentation.packaging"),
#Mapping(target="packagecount", source = "species.presentation.pkgunit"),
#Mapping(target="avgpackageweight", source = "species.presentation.pkgUnitWeight"),
#Mapping(target="conversionfactor", source = "species.presentation.convFactor"),
#Mapping(target="fishdeclaration", source = "species.geoInfo")
}
)
Logdeclaration SpeciesToLogDeclarations (Species species, #Context ExtraFields extraFields);
The problem is that you are trying to map Species to List<Logdeclaration> and MapStruct cannot find such mapping method. In order to make it work you can add the following methods to your SpecyToLogDeclarationMapperApi:
default List<Logdeclaration> SpeciesToLogDeclarationsToList(Species species, #Context ExtraFields extraFields) {
if ( species == null ) {
return null;
}
Logdeclaration logDeclaration = SpeciesToLogDeclarations(species, extraFields);
List<Logdeclaration> logdeclarations = new ArrayList<Logdeclaration>();
logdeclarations.add(logDeclaration);
return logdeclarations;
}
This are some additional things, that I think you can do to improve your code and use MapStruct "more correctly":
You will need to remove the usage of Mappers#getMapper(Class) when the component model is not the default
If you don't want to use FQN in your expressions, you can use Mapper#imports and MapStruct will import them in the implementation.
When you have a single source parameter you don't have to use it's name in the mapping
e.g.
#Mapping(target="fishcount", source = "species.qty")
can be
#Mapping(target="fishcount", source = "qty")

Spring AOP : Interface method with Parameter annotation captured, but annotation is not present

I'm using Spring AOP to intercept a method execution.
I have an interface that looks like the following:
public interface MyAwesomeService {
public Response doThings(int id, #AwesomeAnnotation SomeClass instance);
}
Here is the implementation of the interface:
public class MyAwesomeServiceImpl implements MyAwesomeService {
public Response doThings(int id, SomeClass instance) {
// do something.
}
}
Now i would like any method which has a parameter annotated with #AwesomeAnnotation should be captured by Spring AOP.
So I wrote the following aspect which works.
#Aspect
#Component
public class MyAwesomeAspect {
#Around("myPointcut()")
public Object doAwesomeStuff(final ProceedingJoinPoint proceedingJoinPoint) {
final MethodSignature methodSignature = (MethodSignature) proceedingJoinPoint.getSignature();
Annotation[][] annotationMatrix = methodSignature.getMethod().getParameterAnnotations();
// annotationMatrix is empty.
}
#Pointcut("execution(public * *(.., #package.AwesomeAnnotation (package.SomeClass), ..))")
public void myPointcut() {}
}
However when I try to find the parameter annotations I don't get any annotations back. As mentioned above, the annotationMatrix is empty.
So here are my questions:
Why is the annotationMatrix empty? Probably because parameter annotations are not inherited from an interface.
Why I'm able to capture the method execution. Since Spring AOP is able match the pointcut, Spring somehow is able to see the method's parameter annotations but when I try to see that using methodSignature.getMethod().getParameterAnnotations() it doesn't work.
I also faced this issue with one of my parameter annotations. I was able to fix the same by making sure that the parameter annotation definition had RetentionPolicy as RUNTIME and Target as PARAMETER
#Retention(RetentionPolicy.RUNTIME)
#Target(ElementType.PARAMETER)
public #interface Param {
public String name();
}
The answers to your questions:
Parameter annotations are not inherited from interfaces to implementing methods. In fact, annotations are almost never inherited, only from class (not interface!) to subclass if the annotation type itself is annotated by #Inherited, see JDK API documentation.
Update: Because I have answered this question several times before, I have just documented the problem and also a workaround in Emulate annotation inheritance for interfaces and methods with AspectJ.
Because during compile or weave time AspectJ can match your pointcut against the interface method and thus sees the annotation.
You can fix the situation by adding the annotation to the parameter in your interface implementation, e.g. like this:
#Override
public Response doThings(int id, #AwesomeAnnotation SomeClass instance) {
// ...
}
Then with an aspect like this...
#Aspect
#Component
public class MyAwesomeAspect {
#Pointcut("execution(public * *..MyAwesomeService.*(*, #*..AwesomeAnnotation (*), ..)) && args(*, instance, ..)")
static void myPointcut(SomeClass instance) {}
#Around("myPointcut(instance)")
public Object doAwesomeStuff(Object instance, ProceedingJoinPoint proceedingJoinPoint) {
System.out.println(proceedingJoinPoint);
System.out.println(" instance = " + instance);
MethodSignature methodSignature = (MethodSignature) proceedingJoinPoint.getSignature();
Annotation[][] annotationMatrix = methodSignature.getMethod().getParameterAnnotations();
for (Annotation[] annotations : annotationMatrix) {
for (Annotation annotation : annotations) {
System.out.println(" annotation = " + annotation);
}
}
return proceedingJoinPoint.proceed();
}
}
... you get a console log similar to this:
execution(Response de.scrum_master.app.MyAwesomeServiceImpl.doThings(int, SomeClass))
instance = de.scrum_master.app.SomeClass#23fc625e
annotation = #de.scrum_master.app.AwesomeAnnotation()

mapstruct ambiguous exception when using helper class

Using mapstruct v1.0.0.Final, I'm facing an ambiguous mapping exception trying to map from SourceType to TargetType:
class TargetType {
List<TargetTypeChild> children;
boolean allResults;
}
class SourceType {
List<SourceTypeChild> children;
boolean allResults;
}
The mapping that I'm using is:
#Mapper(uses = B.class)
interface A {
#Mapping(target = "children", source = "children", qualifiedBy = ToTargetType.class)
TargetType toTargetType (SourceType source);
#Mapping(target = "children", source = "children", qualifiedBy = ToTargetTypeNoDetails.class)
TargetType toTargetTypeNoDetails (SourceType source);
}
interface B {
#Qualifier
#Target(ElementType.METHOD)
#Retention(RetentionPolicy.SOURCE)
public #interface ToTargetType {}
#Qualifier
#Target(ElementType.METHOD)
#Retention(RetentionPolicy.SOURCE)
public #interface ToTargetTypeNoDetails {}
#ToTargetType
#IterableMapping(qualifiedBy = ToTargetType.class)
List<TargetTypeChild> withDetails(List<SourceTypeChild> value);
#ToTargetTypeNoDetails
#IterableMapping(qualifiedBy = ToTargetTypeNoDetails.class)
List<TargetTypeChild> noDetails(List<SourceTypeChild> value);
#ToTargetType
#Mappings({
#Mapping(target = "details", source = "details"),
...rest of mapping
})
TargetTypeChild toTargetTypeChild(SourceTypeChild source);
#ToTargetTypeNoDetails
#Mappings({
#Mapping(target = "details", ignore = true),
...rest of mapping
})
TargetTypeChild toTargetTypeChildNoDetails(SourceTypeChild source);
}
This does not compile, giving the following exception in both interface A's methods:
Ambiguous mapping methods found for mapping property "List children" to List: List noDetails(List arg0), List withDetails(List arg0).
There is one workaround to this: put both interface A's methods in interface B. That compiles and works. But I need to separate them for business reasons.
Could anyone explain why the first approach doesn't work and the workaround does?
As a bonus question, if I only code 1 method for mapping (no qualifiers), I don't need to even declare the #IterableMapping method, mapstruct knows how to find the "children" methods.
Why?
Thank you all!
Anyone could explain why the first approach doesn't work and the workaround does?
Your qualifier annotations must at least have retention policy CLASS, only then they will be discovered. That's not needed if everything is defined within the same source file, in which case SOURCE is enough.
As a bonus question, if I only code 1 method for mapping (no qualifiers)
MapStruct will generate (private) iterable mapping methods as needed. Actually it should work also in your original case, seems like that's a glitch we need to fix. I've filed issue #707 for this.
Thanks for reporting this!

Spring MVC REST using #RequestBody List<?> returns HTTP 400 syntactically incorrect

I am using Spring 4 + Jackson 2 and have written a fully functional POST method using #RequestBody on a custom class. This method has no trouble unmarshalling the object.
#ResponseBody
#RequestMapping(value="store", method = RequestMethod.POST)
public ServiceResponse store(#RequestBody CustomClass list) {
...
}
// Request: { code: "A", amount: 200 }
When I attempted to add another method to handle a collection of the same class instead, my POST requests were returning with the following error.
HTTP Status 400: The request sent by the client was syntactically incorrect.
I note that this error typically occurs when the JSON submitted does not match the entity class. However, all I am doing is submitting an array of the same object instead of the object itself, which has already proven to work.
#ResponseBody
#RequestMapping(value="store-bulk", method = RequestMethod.POST)
public ServiceResponse storeBulk(#RequestBody List<CustomClass> list) {
...
}
// Request: [{ code: "A", amount: 200 }, { code: "B", amount: 400 }]
Am I missing something here?
In Java, type information for generics is erased at runtime, so Spring sees your List<CustomClass> object as List<Object> object, thus it cannot understand how to parse it.
One of ways to solve it, you could capture the type information by creating a wrapper class for your list, like this:
public class CustomClassList extends ArrayList<CustomClass> {
}
Sergey is right that the issue is due to type erasure. Your easiest way out is to bind to an array, so
#ResponseBody
#RequestMapping(value="store-bulk", method = RequestMethod.POST)
public ServiceResponse storeBulk(#RequestBody CustomClass[] object) {
...
}
The answer is that Spring 4 doesn't actually get rid of type erasure, contrary to what some other solutions suggest. While experimenting on debugging via manual unmarshalling, I decided to just handle that step myself instead of an implicit cast that I have no control over. I do hope someone comes along and proves me wrong, demonstrating a more intuitive solution though.
#ResponseBody
#RequestMapping(value="store-bulk", method = RequestMethod.POST)
public ServiceResponse storeBulk(#RequestBody String json) {
try {
List<CustomClass> list = new ObjectMapper().readValue(json, new TypeReference<List<CustomClass>>() { });
...
} catch (Exception e) {
...
}
}
Bonus: Right after I got this working, I bumped into this exception:
IllegalStateException: Already had POJO for id
If anyone gets this, it's because the objects in the list happen to reference some object that another item in the list already references. I could work around this since that object was identical for my entire collection, so I just removed the reference from the JSON side from all but the first object. I then added the missing references back after the JSON was unmarshalled into the List object.
Two-liner for the Java 8 users (the User object reference was the issue in my case):
User user = list.get(0).getUser();
list.stream().filter(c -> c.getUser() == null).forEach(t -> t.setUser(user));