I'm exploring mapstruct to map JPA entities and DTO objects. Entities and DTOs have abstract base classes that contain id and version fields that I'd like to keep private so that they can not be modified (public getter, no setter for both types). I made a most simple reproducer to demonstrate the idea. Abstract Base class has a private field name. To copy the field values back and forth Base defines a constructor that has a Base parameter. The constructor picks the private field from the parameter and assigns it to it's own private field:
package de.ruu.lab.map.read_only_field_in_base;
public abstract class Base
{
private String name;
public Base(String name) { this.name = name; }
protected Base(Base source) { name = source.name; }
public String getName() { return name; }
}
These are the subclasses of Base:
package de.ruu.lab.map.read_only_field_in_base;
import de.ruu.lab.map.read_only_field_in_base.SimpleMapper.Default;
public class Source extends Base
{
public Source(String name) { super(name); }
@Default
public Source(Base base) { super(base); }
}
package de.ruu.lab.map.read_only_field_in_base;
import de.ruu.lab.map.read_only_field_in_base.SimpleMapper.Default;
public class Target extends Base
{
public Target(String name) { super(name); }
@Default
public Target(Base base) { super(base); }
}
I have to annotate the default constructor to resolve constructor ambiguity for mapstruct. The mapper looks like this:
package de.ruu.lab.map.read_only_field_in_base;
import static java.lang.annotation.ElementType.CONSTRUCTOR;
import static java.lang.annotation.RetentionPolicy.CLASS;
import java.lang.annotation.Retention;
import org.mapstruct.Mapper;
import org.mapstruct.Qualifier;
import org.mapstruct.factory.Mappers;
@Mapper
public interface SimpleMapper
{
SimpleMapper INSTANCE = Mappers.getMapper(SimpleMapper.class);
Source toSource(Target target);
Target toTarget(Source source);
@Qualifier // make sure that this is the MapStruct qualifier annotation
@java.lang.annotation.Target(CONSTRUCTOR)
@Retention(CLASS)
public @interface Default { }
}
The first problem is that mapstruct warns that there is an unmapped target property "base". What does that mean? Which target property is not mapped? Wouldn't it be possible to print the name of the property in the warning? I use eclipse as IDE, maybe the behaviour is different with other tools?
I tried annotating the mapping methods with
@Mapping(target="name", ignore = true)
but that does not let the warning disappear.
Because mapstruct just makes a warning I hoped everything would be ok and I created a tiny test class:
package de.ruu.lab.map.read_only_field_in_base;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;
import org.junit.jupiter.api.Test;
class SimpleMapperTest
{
@Test void shouldMapSourceToTarget()
{
Source source = new Source("map me");
Target target = SimpleMapper.INSTANCE.toTarget(source);
assertThat(target.getName(), is(source.getName()));
}
@Test void shouldMapTargetToSource()
{
Target target = new Target("map me");
Source source = SimpleMapper.INSTANCE.toSource(target);
assertThat(source.getName(), is(target.getName()));
}
}
Both tests fail with a NPE because of some strange code mapstruct generated:
package de.ruu.lab.map.read_only_field_in_base;
import javax.annotation.processing.Generated;
@Generated(
value = "org.mapstruct.ap.MappingProcessor",
date = "2022-09-18T11:08:20+0200",
comments = "version: 1.5.2.Final, compiler: Eclipse JDT (IDE) 1.4.200.v20220802-0458, environment: Java 17.0.2 (GraalVM Community)"
)
public class SimpleMapperImpl implements SimpleMapper {
@Override
public Source toSource(Target target) {
if ( target == null ) {
return null;
}
Base base = null;
Source source = new Source( base );
return source;
}
@Override
public Target toTarget(Source source) {
if ( source == null ) {
return null;
}
Base base = null;
Target target = new Target( base );
return target;
}
}
Obviously code like this causes the NPE:
Base base = null;
Source source = new Source( base );
IMO this would be correct ("target" is the name of the method's parameter):
Source source = new Source( target );
Maybe this can be solved in an upcoming version. Meanwhile, is there any recommendation how to deal with this now?
Thanks!