Mapstruct constructor injection (spring) - mapstruct

In mapstruct 1.3.0.Final we have dependency injection via constructor. Documentation says:
The generated mapper will inject all classes defined in the uses
attribute
(...)
For abstract classes or decorators setter injection should be
used.
I have following example:
#Mapper
public abstract class VehicleMapper {
#Autowired
private CarMapper carMapper;
#Autowired
private BikeMapper bikeMapper;
#Override
public VehicleDTO toDto(final Vehicle source) {
if (source instanceof Car) {
return carMapper.toDto((Car) source);
} else if (source instanceof Bike) {
return bikeMapper.toDto((Bike) source);
} else {
throw new IllegalArgumentException();
}
}
(...)
So in my case it should look like this (componentModel defined in maven):
#Mapper
public abstract class VehicleMapper {
private CarMapper carMapper;
private BikeMapper bikeMapper;
#Autowired
public void setCarMapper(final CarMapper carMapper) {
this.carMapper = carMapper;
}
#Autowired
public void setBikeMapper(final BikeMapper bikeMapper) {
this.bikeMapper = bikeMapper;
}
#Override
public VehicleDTO toDto(final Vehicle source) {
if (source instanceof Car) {
return carMapper.toDto((Car) source);
} else if (source instanceof Bike) {
return bikeMapper.toDto((Bike) source);
} else {
throw new IllegalArgumentException();
}
}
(...)
Question:
So it is not possible to inject carMapper and bikeMapper via constructor ? does injectionStrategy = CONSTRUCTOR works only for classes declared in #Mapper(uses = {}) ?

I think that injectionStrategy = CONSTRUCTOR works on the interface that has the #Mapper annotation. I don't think it works with abstract classes. I'm sure it will not work when you define your own fields (instance variables). How would MapStruct know what user defined fields to initialise in the constructor?

Related

Mapping a field using existing target value (Mapstruct)

i have a custom case that some of my dto's have a field of type X, and i need to map this class to Y by using a spring service method call(i do a transactional db operation and return an instance of Y). But in this scenario i need to use existing value of Y field. Let me explain it by example.
// DTO
public class AnnualLeaveRequest {
private FileInfoDTO annualLeaveFile;
}
//ENTITY
public class AnnualLeave {
#OneToOne
private FileReference annualLeaveFile;
}
#Mapper
public abstract class FileMapper {
#Autowired
private FileReferenceService fileReferenceService;
public FileReference toFileReference(#MappingTarget FileReference fileReference, FileInfoDTO fileInfoDTO) {
return fileReferenceService.updateFile(fileInfoDTO, fileReference);
}
}
//ACTUAL ENTITY MAPPER
#Mapper(uses = {FileMapper.class})
public interface AnnualLeaveMapper {
void updateEntity(#MappingTarget AnnualLeave entity, AnnualLeaveRequest dto);
}
// WHAT IM TRYING TO ACHIEVE
#Component
public class MazeretIzinMapperImpl implements tr.gov.hmb.ikys.personel.izinbilgisi.mazeretizin.mapper.MazeretIzinMapper {
#Autowired
private FileMapper fileMapper;
#Override
public void updateEntity(AnnualLeave entity, AnnualLeaveUpdateRequest dto) {
entity.setAnnualLeaveFile(fileMapper.toFileReference(dto.getAnnualLeaveFile(), entity.getAnnualLeaveFile()));
}
}
But mapstruct ignores the result of "FileReference toFileReference(#MappingTarget FileReference fileReference, FileInfoDTO fileInfoDTO) " and does not map the result of it to the actual entity's FileReference field. Do you have any idea for resolving this problem?
Question
How do I replace the annualLeaveFile property while updating the AnnualLeave entity?
Answer
You can use expression to get this result. For example:
#Autowired
FileMapper fileMapper;
#Mapping( target = "annualLeaveFile", expression = "java(fileMapper.toFileReference(entity.getAnnualLeaveFile(), dto.getAnnualLeaveFile()))" )
abstract void updateEntity(#MappingTarget AnnualLeave entity, AnnualLeaveRequest dto);
MapStruct does not support this without expression usage. See the end of the Old analysis for why.
Alternative without expression
Instead of fixing it in the location where FileMapper is used, we fix it inside the FileMapper itself.
#Mapper
public abstract class FileMapper {
#Autowired
private FileReferenceService fileReferenceService;
public void toFileReference(#MappingTarget FileReference fileReference, FileInfoDTO fileInfoDTO) {
FileReference wanted = fileReferenceService.updateFile(fileInfoDTO, fileReference);
updateFileReference(fileReference, wanted);
}
// used to copy the content of the service one to the mapstruct one.
abstract void updateFileReference(#MappingTarget FileReference fileReferenceTarget, FileReference fileReferenceFromService);
}
Old analysis
The following is what I notice:
(Optional) your FileMapper class is not a MapStruct mapper. This can just be a normal class annotated with #Component, since it does not have any unimplemented abstract methods. (Does not affect code generation of the MazeretIzinMapper implementation)
(Optional, since you have this project wide configured) you do not have componentModel="spring" in your #Mapper definition, maybe you have this configured project wide, but that is not mentioned. (required for the #Autowired annotation, and #Component on implementations)
Without changing anything I already get a working result as you want it to be, but for non-update methods (not listed in your question, but was visible on the gitter page where you also requested help) the FileMapper as is will not be used. It requires an additional method that takes only 1 argument: public FileReference toFileReference(FileInfoDTO fileInfoDTO)
(Edit) to get rid of the else statement with null value handling you can add nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE to the #Mapper annotation.
I've run a test and with 1.5.0.Beta2 and 1.4.2.Final I get the following result with the thereafter listed FileMapper and MazeretIzinMapper classes.
Generated mapper implementation
#Generated(
value = "org.mapstruct.ap.MappingProcessor",
date = "2022-03-11T18:01:30+0100",
comments = "version: 1.4.2.Final, compiler: Eclipse JDT (IDE) 1.4.50.v20210914-1429, environment: Java 17.0.1 (Azul Systems, Inc.)"
)
#Component
public class MazeretIzinMapperImpl implements MazeretIzinMapper {
#Autowired
private FileMapper fileMapper;
#Override
public AnnualLeave toEntity(AnnualLeaveRequest dto) {
if ( dto == null ) {
return null;
}
AnnualLeave annualLeave = new AnnualLeave();
annualLeave.setAnnualLeaveFile( fileMapper.toFileReference( dto.getAnnualLeaveFile() ) );
return annualLeave;
}
#Override
public void updateEntity(AnnualLeave entity, AnnualLeaveRequest dto) {
if ( dto == null ) {
return;
}
if ( dto.getAnnualLeaveFile() != null ) {
if ( entity.getAnnualLeaveFile() == null ) {
entity.setAnnualLeaveFile( new FileReference() );
}
fileMapper.toFileReference( entity.getAnnualLeaveFile(), dto.getAnnualLeaveFile() );
}
}
}
Source classes
Mapper
#Mapper( componentModel = "spring", uses = { FileMapper.class }, nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE )
public interface MazeretIzinMapper {
AnnualLeave toEntity(AnnualLeaveRequest dto);
void updateEntity(#MappingTarget AnnualLeave entity, AnnualLeaveRequest dto);
}
FileMapper component
#Mapper
public abstract class FileMapper {
#Autowired
private FileReferenceService fileReferenceService;
public FileReference toFileReference(#MappingTarget FileReference fileReference, FileInfoDTO fileInfoDTO) {
return fileReferenceService.updateFile( fileInfoDTO, fileReference );
}
public FileReference toFileReference(FileInfoDTO fileInfoDTO) {
return toFileReference( new FileReference(), fileInfoDTO );
}
// other abstract methods for MapStruct mapper generation.
}
Why the exact wanted code will not be generated
When generating the mapping code MapStruct will use the most generic way to do this.
An update mapper has the following criteria:
The #MappingTarget annotated argument will always be updated.
It is allowed to have no return type.
the generic way to update a field is then as follows:
// check if source has the value.
if (source.getProperty() != null) {
// Since it is allowed to have a void method for update mappings the following steps are needed:
// check if the property exists in the target.
if (target.getProperty() == null) {
// if it does not have the value then create it.
target.setProperty( new TypeOfProperty() );
}
// now we know that target has the property so we can call the update method.
propertyUpdateMappingMethod( target.getProperty(), source.getProperty() );
// The arguments will match the order as specified in the other update method. in this case the #MappingTarget annotated argument is the first one.
} else {
// default behavior is to set the target property to null, you can influence this with nullValuePropertyMappingStrategy.
target.setProperty( null );
}

Mapstruct #Mapper's uses attribute not working

I am facing an issue with one of my Mapstruct mappers not using another mapper with #Mapper(uses =
Our ValidationSupportNeedMapper maps from entities to DTOs. One ValidationSupportNeedEntity contains an ActivityEntity property and I am trying to map from this property to an Activity DTO.
The issue is therefore with a nested object i.e. ActivityEntity to Activity.
Here is the source code:
From ValidationSupportNeedMapper.java:
#Mapper(uses = {LifecycleErrorMessagesMapper.class, ActivityMapper.class})
public interface ValidationSupportNeedMapper {
ValidationSupportNeed toValidationSupportNeed(ValidationSupportNeedEntity source);
...
From ActivityMapper.java:
#Component
public class ActivityMapper {
public Activity toActivity(ActivityEntity activity) {
//Implementation
}
public ActivityEntity toActivityEntity(Activity activity) {
//Implementation
}
}
From ValidationSupportNeedEntity.java (Entity)
public class ValidationSupportNeedEntity {
private ActivityEntity activityEntity;
From ValidationSupportNeed.java (DTO)
public class ValidationSupportNeed implements AutoValidated {
private Activity validationActivity;
However Mapstruct seems to ignore the uses= attribute on the #Mapper annotation and goes ahead and generates its own mapping method as follows:
#Override
public ValidationSupportNeed toValidationSupportNeed(ValidationSupportNeedEntity source) {
if ( source == null ) {
return null;
}
ValidationSupportNeed validationSupportNeed = new ValidationSupportNeed();
validationSupportNeed.setValidationActivity( validationSupportNeedEntityToActivity( source ) );
...
}
protected Activity validationSupportNeedEntityToActivity(ValidationSupportNeedEntity validationSupportNeedEntity) {
if ( validationSupportNeedEntity == null ) {
return null;
}
Activity activity = new Activity();
activity.setCode( validationSupportNeedEntity.getValidationActivityCode() );
return activity;
}
What am I missing? Can someone please help?
edit: ActivityMapper is not autowired into the ValidationSupportNeedMapper implementation.
Adding a mapping annotation sorted the issue:
#Mapping(source = "activityEntity", target = "validationActivity")
ValidationSupportNeed toValidationSupportNeed(ValidationSupportNeedEntity source);
Notice the names of the attributes are different.

How to change my DAO Bean from EJB to pure CDI?

I want to reuse my AbstractDAO in a new project, except this time I don't want to use EJB annotations - just CDI ones.
So far, I've been using it like this:
public abstract class AbstractDAO<T> {
#PersistenceContext(unitName = "myUnit")
private EntityManager entityManager;
private Class<T> entityClass;
public AbstractDAO(Class<T> entityClass) {
this.entityClass = entityClass;
}
protected EntityManager getEntityManager() {
return entityManager;
}
public void save(T entity) {
entityManager.persist(entity);
}
public void update(T entity) {
entityManager.merge(entity);
}
public void remove(T entity) {
entityManager.remove(entityManager.merge(entity));
}
public T findById(Object id) {
return entityManager.find(entityClass, id);
}
public List<T> findBy(String attrName, Object attrValue) {
// Impl here
}
// [...] Many more search methods
}
And I've been creating a DAO for each Entity, like this for example:
#Stateless
public class UserDAO extends AbstractDAO<User> {
public UserDAO() {
super(User.class);
}
public User findByUsername(String username) {
if (username != null) {
return super.findOneBy("username", username.toLowerCase());
}
return null;
}
}
Now I would like to get rid of the #Stateless annotation. But simply replacing it with a #RequestScoped one won't work because of the non-private constructor with no parameters requirement of JSR-346
How can I refactor my DAO to a pure CDI one ?
Two issues here: CDI beans are not transaction aware by default -unlike EJBs, so you will have to use #Transactional qualifier if you want to do saves/updates...
Second, your no-arg constructor: you only need to pass the entity class to your abstract class even though you also specify it as a generic argument. You can infer the actual class like this:
public class AbstractDAO<T> {
private transient Class<T> entityClass;
#SuppressWarnings("unchecked")
public AbstractDAO() {
Type generSuperCls = getClass().getGenericSuperclass();
if (generSuperCls instanceof Class) {
generSuperCls = ((Class<?>) generSuperCls).getGenericSuperclass();
}
ParameterizedType parameterizedType = (ParameterizedType) generSuperCls;
Type type = parameterizedType.getActualTypeArguments()[0];
if (type instanceof Class) {
this.entityClass = (Class<T>) type;
} else if (type instanceof ParameterizedType) {
this.entityClass = (Class<T>) ((ParameterizedType) type).getRawType();
}
}
#PersistenceContext
private EntityManager em;
public T getById(Object id) throws ServiceException {
return getEm().find(entityClass, id);
}
// other methods follow
}
As a side note, why do you want to get rid of EJBs? Benchmarks show you get better performance using pooled slsb than cdi and they together very well(every EJB beans is also a CDI bean in jee container).

Why does my sub-dependency not get set in Dagger?

I am having a hard time figuring out how to inject CachedRithms into my RithmioManager and CachedKamms into my KamilManager?
I have the following files:
AppScopeModule:
#Module
(
library = true,
complete = false,
injects = {
KamilApplication.class,
KamilManager.class
}
)
public class AppScopeModule {
/* package */ static Context sApplicationContext = null;
private final Context mApplicationContext;
AppScopeModule(Context applicationContext) {
KamilManager.initInstance(applicationContext);
mApplicationContext = applicationContext;
}
#Provides
#Singleton
KamilManager provideKamilManager() {
return KamilManager.getInstance();
}
}
KamilApplication:
public class KamilApplication extends Application implements Injector {
private ObjectGraph mObjectGraph;
#Inject
KamilManager KamilManager;
#Override
public void onCreate() {
super.onCreate();
AppScopeModule sharedAppModule = new AppScopeModule(this);
// bootstrap. So that it allows no-arg constructor in AppScopeModule
sharedAppModule.sApplicationContext = this.getApplicationContext();
List<Object> modules = new ArrayList<Object>();
modules.add(sharedAppModule);
modules.add(new AuthModule());
modules.addAll(getAppModules());
mObjectGraph = ObjectGraph.create(modules.toArray());
mObjectGraph.inject(this);
}
}
KamilManager
public class KamilManager {
#Inject
CachedKamms mCachedKamms;
private static KamilManager instance;
private boolean mWearIsConnectedToMobile;
private KamilManager() {
Log.d(TAG, "KamilManager private constructor");
}
public static void initInstance(Context appContext) {
if (instance == null) {
instance = new KamilManager();
.....doing more things here...
}
}
public static KamilManager getInstance() {
return instance;
}
}
But mCAchedKamms is always blank when I initialize the app. Any idea what I'm doing wrong?
You need to call ObjectGraph.inject(this) somewhere in KamilManager.
I suggest you to add this code to your KamilApplication class:
public ObjectGraph getObjectGraph() {
return mObjectGraph;
}
After that you need to somehow get instance of KamilApplication(pass it via constructor maybe?) in KamilManager and call:
kamilApplication.getObjectGraph.inject(this);
after this call every field in class KamilManager annotated with #Inject should be injected.
OR
Just annotate constructor of CachedKamms with #Inject
Extra:
Avoid of using library = true and complete = false unless you know what are you doing. With this settings you disable some validations at compile time.

Parameterless implementation for Ninject mappings

i am creating custom membership provider using ninject for binding to sql classes.
my class in looks like
public MyMembershipProvider(IUsersRepository userRepository)
{
this.userRepository = userRepository;
}
How can i create from this parameterless constructor?
kernel.Bind<IUsersRepository>().To<UsersRepository>();
kernel.Bind<MembershipProvider>().To<MyMembershipProvider>();
and then:
public class AccountController : Controller
{
private readonly MembershipProvider _membershipProvider;
public AccountController(MembershipProvider membershipProvider)
{
_membershipProvider = membershipProvider;
}
public ActionResult Foo()
{
// TODO: Use the membership provider to do some processing
return View();
}
}