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);
}
Related
I have a source class that defines string attributes as CharSequence (unfortunately).
So the following:
#Mapper(source="charSeq", target="str")
gives me:
Can't map property "java.lang.CharSequence charSeq" to "java.lang.String str". Consider to declare/implement a mapping method: "java.lang.String map(java.lang.CharSequence value)"
How can I implement this mapper method and make it available to all my mappers so that I do it once and for all?
Create a String-CharSequence mapper:
#Mapper
public interface CharSequenceMapper {
default String map(CharSequence charSequence) {
return charSequence.toString();
}
default CharSequence map(String string) {
return string;
}
}
And use it with your mapper:
#Mapper(uses = CharSequenceMapper.class)
public interface MyMapper {
// some code
}
IMHO CharSequence-String conversion should be built into the framework. Consider filing a a feature request at https://github.com/mapstruct/mapstruct/issues.
NOTE: This is unlike other questions on StackOverflow because they resolve this issue by mapping the two classes manually. Since ScheduleSource and ScheduleTarget are exactly the same classes, I want them to be mapped automatically.
Hi,
I have 2 classes ScheduleSource and ScheduleTarget. They have exactly the same properties.
When I try to use MapStruct to map from ScheduleSource to ScheduleTarget, I get the error:
Can't map property "java.util.Optional<java.time.LocalDate> startDate" to "java.time.LocalDate startDate". Consider to declare/implement a mapping method: "java.time.LocalDate map(java.util.Optional<java.time.LocalDate> value)
I have attached the two files. Can you please help?
Files are:
ScheduleSource, ScheduleTarget - the two Java Beans
ScheduleMapper - the mapping class.
ScheduleMapper.java
package testStructMap;
import org.mapstruct.*;
import org.mapstruct.factory.*;
#Mapper
public interface ScheduleMapper {
ScheduleMapper INSTANCE = Mappers.getMapper( ScheduleMapper.class );
ScheduleTarget scheduleSourceToScheduleTarget(ScheduleSource scheduleSource);
}
ScheduleSource.java, ScheduleTarget.java - same structure
package testStructMap;
import java.time.LocalDate;
import java.time.LocalTime;
import java.util.Optional;
import javax.validation.constraints.*;
public class ScheduleSource {
#FutureOrPresent
#NotNull
private LocalDate startDate;
#NotBlank
private String repeatType;
#Positive
private Integer occurrences;
public Optional<LocalDate> getStartDate() {
return Optional.ofNullable(startDate);
}
public void setStartDate(LocalDate startDate) {
this.startDate = startDate;
}
public String getRepeatType() {
return repeatType;
}
public void setRepeatType(String repeatType) {
this.repeatType = repeatType;
}
public Optional<Integer> getOccurrences() {
return Optional.ofNullable(occurrences);
}
public void setOccurrences(Integer occurrences) {
this.occurrences = occurrences;
}
}
In 1.3.0.beta1 the following is supported:
package testStructMap;
import org.mapstruct.*;
import org.mapstruct.factory.*;
#Mapper
public interface ScheduleMapper {
ScheduleMapper INSTANCE = Mappers.getMapper( ScheduleMapper.class );
ScheduleTarget scheduleSourceToScheduleTarget(ScheduleSource scheduleSource);
default <T> T unwrapOptional(Optional<T> optional) {
return optional.orElse(null);
}
}
I'm not familiar with mapstruct, but I can guess it maps different objects :)
If your source and target classes have the same structure then the problem is
public Optional<LocalDate> getStartDate();
public void setStartDate(LocalDate startDate);
So it gets the Optional object and tries to pass it to a method accepting a LocalDate.
So your possible ways of action are
change getter to return a simple object
change setter to accept an optional (which is fine I guess, but
seems a bit off)
declare a mapper method
Map target Optional to source which is not Optional how can we make it follow the example below:
#Named("HelperClass")
class Helper {
#Named("convertToOptional")
public Optional<KontaktpersonVerwandtschaftsgradTyp> convertToOptional(KontaktpersonVerwandtschaftsgradTyp optional) {
return optional != null ? Optional.of(optional) : Optional.empty();
};
}
#Mapping(target = "kontaktpersonVerwandtschaftsgrad", source = "tdfFall.kontaktpersonVerwandtschaftsgrad", qualifiedByName = { "HelperClass",
"convertToOptional" })
and also we need to add uses and the name of the Helper class
#Mapper(componentModel = "spring", uses = { Helper.class })
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);
}
Is there a simple way to use spring data couchbase with documents that do not have _class attribute?
In the couchbase I have something like this in my sampledata bucket:
{
"username" : "alice",
"created" : 1473292800000,
"data" : { "a": 1, "b" : "2"},
"type" : "mydata"
}
Now, is there any way to define mapping from this structure of document to Java object (note that _class attribute is missing and cannot be added) and vice versa so that I get all (or most) automagical features from spring couchbase data?
Something like:
If type field has value "mydata" use class MyData.java.
So when find is performed instead of automatically adding AND _class = "mydata" to generated query add AND type = "mydata".
Spring Data in general needs the _class field to know what to instantiate back when deserializing.
It's fairly easy in Spring Data Couchbase to use a different field name than _class, by overriding the typeKey() method in the AbsctractCouchbaseDataConfiguration.
But it'll still expect a fully qualified classname in there by default
Getting around that will require quite a bit more work:
You'll need to implement your own CouchbaseTypeMapper, following the model of DefaultCouchbaseTypeMapper. In the super(...) constructor, you'll need to provide an additional argument: a list of TypeInformationMapper. The default implementation doesn't explicitly provide one, so a SimpleTypeInformationMapper is used, which is the one that puts FQNs.
There's an alternative implementation that is configurable so you can alias specific classes to a shorter name via a Map: ConfigurableTypeInformationMapper...
So by putting a ConfigurableTypeInformationMapper with the alias you want for specific classes + a SimpleTypeInformationMapper after it in the list (for the case were you serialize a class that you didn't provide an alias for), you can achieve your goal.
The typeMapper is used within the MappingCouchbaseConverter, which you'll also need to extend unfortunately (just to instantiate your typeMapper instead of the default.
Once you have that, again override the configuration to return an instance of your custom MappingCouchbaseConverter that uses your custom CouchbaseTypeMapper (the mappingCouchbaseConverter() method).
You can achive this e.g. by creating custom annotation #DocumentType
#DocumentType("billing")
#Document
public class BillingRecordDocument {
String name;
// ...
}
Document will look like:
{
"type" : "billing"
"name" : "..."
}
Just create following classes:
Create custom AbstractReactiveCouchbaseConfiguration or AbstractCouchbaseConfiguration (depends which varian you use)
#Configuration
#EnableReactiveCouchbaseRepositories
public class CustomReactiveCouchbaseConfiguration extends AbstractReactiveCouchbaseConfiguration {
// implement abstract methods
// and configure custom mapping convereter
#Bean(name = BeanNames.COUCHBASE_MAPPING_CONVERTER)
public MappingCouchbaseConverter mappingCouchbaseConverter() throws Exception {
MappingCouchbaseConverter converter = new CustomMappingCouchbaseConverter(couchbaseMappingContext(), typeKey());
converter.setCustomConversions(customConversions());
return converter;
}
#Override
public String typeKey() {
return "type"; // this will owerride '_class'
}
}
Create custom MappingCouchbaseConverter
public class CustomMappingCouchbaseConverter extends MappingCouchbaseConverter {
public CustomMappingCouchbaseConverter(final MappingContext<? extends CouchbasePersistentEntity<?>,
CouchbasePersistentProperty> mappingContext, final String typeKey) {
super(mappingContext, typeKey);
this.typeMapper = new TypeBasedCouchbaseTypeMapper(typeKey);
}
}
and custom annotation #DocumentType
#Persistent
#Inherited
#Retention(RetentionPolicy.RUNTIME)
#Target({ElementType.TYPE})
public #interface DocumentType {
String value();
}
Then create TypeAwareTypeInformationMapper which will just check if an entity is annoatated by #DocumentType if so, use value from that annotation, do the default if not (fully qualified class name)
public class TypeAwareTypeInformationMapper extends SimpleTypeInformationMapper {
#Override
public Alias createAliasFor(TypeInformation<?> type) {
DocumentType[] documentType = type.getType().getAnnotationsByType(DocumentType.class);
if (documentType.length == 1) {
return Alias.of(documentType[0].value());
}
return super.createAliasFor(type);
}
}
Then register it as following
public class TypeBasedCouchbaseTypeMapper extends DefaultTypeMapper<CouchbaseDocument> implements CouchbaseTypeMapper {
private final String typeKey;
public TypeBasedCouchbaseTypeMapper(final String typeKey) {
super(new DefaultCouchbaseTypeMapper.CouchbaseDocumentTypeAliasAccessor(typeKey),
Collections.singletonList(new TypeAwareTypeInformationMapper()));
this.typeKey = typeKey;
}
#Override
public String getTypeKey() {
return typeKey;
}
}
In your couchbase configuration class you just need to have :
#Override
public String typeKey() {
return "type";
}
Unfortunately for query derivation (n1ql) the _class or type are still using the class name.Tried spring couch 2.2.6 and it's minus point here.
#Simon, are you aware that something has changed and the support to have the possibility to have custom _class/type value in next release(s)?
#SimonBasle
Inside of class N1qlUtils and method createWhereFilterForEntity we have access to the CouchbaseConverter. On line:
String typeValue = entityInformation.getJavaType().getName();
Why not use the typeMapper from the converter to get the name of the entity when we want to avoid using the class name? Otherwise you have to annotate each method in your repository as follows:
#Query("#{#n1ql.selectEntity} WHERE `type`='airport' AND airportname = $1")
List<Airport> findAirportByAirportname(String airportName);
If createWhereFilterForEntity used the CouchbaseConverter we could avoid annotating with the #Query.
I've got classes which use an #JsonTypeIdResolver to add a custom type field to the output. This code was working as expected. I've now added an PropertyFilter to my mapper object. This is where the #JsonTypeIdResolver stopped working. The factory is not being called anymore.
Working code:
ObjectMapper mapper = new ObjectMapper();
mapper.writeValue(outputStream,myObject);
Not working code:
ObjectMapper mapper = new ObjectMapper();
PropertyFilter myfilter=new SimpleBeanFilter() {
protected boolean include(BeanPropertyWriter writer) {
return true;
}
protected boolean include(PropertyWriter writer) {
return true;
}
}
FilterProvider filters=new SimpleFilterProvider().addFilter("myFilter",myFilter);
mapper.writer(filter).writeValue(outputStream,myObject);
As the filter is just useless (accepts anything) the output should be the same. Why does my type field not get serialized anymore?
Seems like Jackson doesn't deal with inheritance the right way. My test setup was like
#JsonTypeInfo( use = JsonTypeInfo.Id.CLASS, include = As.PROPERTY, property = "_type" )
abstract class Base {
String somefield;
...
}
class ChildA extends Base {
...
}
class ChildB extends Base {
...
}
If I write a custom serializer, which explicitely casts ChildA and ChildB to Base before serializing, it works as expected. So the basic issue is that jackson does not recognize annotations on parent objects, if not explicitly told to do so.