is it possitble to do mappingTarget Map from Object in mapstruct? - java

The application I used has lots of json fields in entity.
So I should update only the fields I know.
You can see the below code.
#Mapper(
config = CommonMapper.class,
nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE,
nullValueCheckStrategy = NullValueCheckStrategy.ALWAYS)
public interface ProductMapper {
#Mapping(target = "id", source = "id") // it is not working
void updateMapFromProduct(#MappingTarget Map<String, Object> map, Product product);
}
so I want to see the ProductMapperImpl like this.
is it possible?
#Generated(
value = "org.mapstruct.ap.MappingProcessor",
date = "2022-09-02T10:04:50+0900",
comments = "version: 1.4.2.Final, compiler: IncrementalProcessingEnvironment from gradle-language-java-7.5.jar, environment: Java 11.0.11 (AdoptOpenJDK)"
)
#Component
public class ProductMapperImpl implements ProductMapper {
#Override
public void updateMapFromProduct(Map<String, Object> map, Product product) {
if ( map == null ) {
return null;
}
map.put("id", product.getId());
}
}

Mapping from a Bean into a Map is currently not supported in MapStruct. There is an open feature request that you can follow to track this functionality.

Related

Hibernate Search JsonB indexing

I am struggling with indexing jsonB column into Elasicsearch backend, using Hibernate Search 6.0.2
This is my entity:
#Data
#NoArgsConstructor
#Entity
#Table(name = "examples")
public class Example {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private UUID id;
#NotNull
#Column(name = "fields")
#Type(type = "jsonb")
private Map<String, Object> fields;
}
and this is my programmatic mapping of elasticsearch backend for Hibernate Search:
#Configuration
#RequiredArgsConstructor
public class ElasticsearchMappingConfig implements HibernateOrmSearchMappingConfigurer {
private final JsonPropertyBinder jsonPropertyBinder;
#Override
public void configure(HibernateOrmMappingConfigurationContext context) {
var mapping = context.programmaticMapping();
var exampleMapping = mapping.type(Example.class);
exampleMapping.indexed();
exampleMapping.property("fields").binder(jsonPropertyBinder);
}
}
I based my custom property binder implementation on Hibernate Search 6.0.2 documentation.
#Component
public class JsonPropertyBinder implements PropertyBinder {
#Override
public void bind(PropertyBindingContext context) {
context.dependencies().useRootOnly();
var schemaElement = context.indexSchemaElement();
var userMetadataField = schemaElement.objectField("metadata");
context.bridge(Map.class, new Bridge(userMetadataField.toReference()));
}
#RequiredArgsConstructor
private static class Bridge implements PropertyBridge<Map> {
private final IndexObjectFieldReference fieldReference;
#Override
public void write(DocumentElement target, Map bridgedElement, PropertyBridgeWriteContext context) {
var map = target.addObject(fieldReference);
((Map<String, Object>) bridgedElement).forEach(map::addValue);
}
}
}
I am aware that documentation defines multiple templates for what an Object in Map can be (like in MultiTypeUserMetadataBinder example), but I really do not know what can be inside. All I know, it is a valid json and my goal is to put it into Elasticsearch as valid json structure under "fields": {...}
In my case jsonB column may contain something like this:
{
"testString": "298",
"testNumber": 123,
"testBoolean": true,
"testNull": null,
"testArray": [
5,
4,
3
],
"testObject": {
"testString": "298",
"testNumber": 123,
"testBoolean": true,
"testNull": null,
"testArray": [
5,
4,
3
]
}
but it throws an exception:
org.hibernate.search.util.common.SearchException: HSEARCH400609: Unknown field 'metadata.testNumber'.
I have also set dynamic_mapping to true in my spring application:
...
spring.jpa.properties.hibernate.search.backend.hosts=127.0.0.3:9200
spring.jpa.properties.hibernate.search.backend.dynamic_mapping=true
...
Any other ideas how can I approach this problem? Or maybe I made an error somewhere?
I am aware that documentation defines multiple templates for what an Object in Map can be (like in MultiTypeUserMetadataBinder example), but I really do not know what can be inside. All I know, it is a valid json and my goal is to put it into Elasticsearch as valid json structure under "fields": {...}
If you don't know what the type of each field is, Hibernate Search won't be able to help much. If you really want to stuff that into your index, I'd suggest declaring a native field and pushing the JSON as-is. But then you won't be able to apply predicates to the metadata fields easily, except using native JSON.
Something like this:
#Component
public class JsonPropertyBinder implements PropertyBinder {
#Override
public void bind(PropertyBindingContext context) {
context.dependencies().useRootOnly();
var schemaElement = context.indexSchemaElement();
// CHANGE THIS
IndexFieldReference<JsonElement> userMetadataField = schemaElement.field(
"metadata",
f -> f.extension(ElasticsearchExtension.get())
.asNative().mapping("{\"type\": \"object\", \"dynamic\":true}");
)
.toReference();
context.bridge(Map.class, new Bridge(userMetadataField));
}
#RequiredArgsConstructor
private static class Bridge implements PropertyBridge<Map> {
private static final Gson GSON = new Gson();
private final IndexFieldReference<JsonElement> fieldReference;
#Override
public void write(DocumentElement target, Map bridgedElement, PropertyBridgeWriteContext context) {
// CHANGE THIS
target.addValue(fieldReference, GSON.toJsonTree(bridgedElement));
}
}
}
Alternatively, you can just declare all fields as strings. Then all features provided by Hibernate Search on string types will be available. But of course things like range predicates or sorts will lead to strange results on numeric values (2 is before 10, but "2" is after "10").
Something like this:
#Component
public class JsonPropertyBinder implements PropertyBinder {
#Override
public void bind(PropertyBindingContext context) {
context.dependencies().useRootOnly();
var schemaElement = context.indexSchemaElement();
var userMetadataField = schemaElement.objectField("metadata");
// ADD THIS
userMetadataField.fieldTemplate(
"userMetadataValueTemplate_default",
f -> f.asString().analyzer( "english" )
);
context.bridge(Map.class, new Bridge(userMetadataField.toReference()));
}
#RequiredArgsConstructor
private static class Bridge implements PropertyBridge<Map> {
private final IndexObjectFieldReference fieldReference;
#Override
public void write(DocumentElement target, Map bridgedElement, PropertyBridgeWriteContext context) {
var map = target.addObject(fieldReference);
// CHANGE THIS
((Map<String, Object>) bridgedElement).forEach(entry -> map.addValue( entry.getKey(), String.valueOf(entry.getValue())));
}
}
}

How to Enforce MyBatis PSQL custom TypeHandler on property level

I am using a DTO which contains Set<UUID> and Set<String>.
public class MyBatisDTO implements Serializable{
// other attributes
private Set<UUID> uuidSet;
private Set<String> stringSet;
.....
}
A typeHandler has already been registered for Generic-Type Set<T>.
#MappedJdbcTypes(JdbcType.OTHER)
#MappedTypes(Set.class)
public class SetTypeHandler extends BaseTypeHandler<Set<T>> {
#Override
public Set<T> getNullableResult(ResultSet rs, String columnName) throws SQLException {
return mapFromJson(rs.getString(columnName));
}
.......
Now the problem is that when above mentioned MyBatisDTO.java is being mapped from DB values where both columns uuidSet and stringset are stored as jsonb values, typeHandler picks the Generic type as String -> T extends String for Set<UUID> uuidSet; as well which is leading to auto-type conversion to all items of uuidSet as String. This is not an error but in debugging it can be seen that uuidSet contains String values.
Later on, which also leads in de-serialization of JAX-RS response but thats another topic which is not important at the moment.
My question is Is there a way that we can enforce a typeHandler on property's level? so I am looking for a solution in which I may attach custom TypeHandler on property level, something like this
//SUDO_CODE
public class MyBatisDTO implements Serializable{
// other attributes
#TypeHandler ("com.mybatis.handlers.typeHandler.SetUUIDTypeHandler")
private Set<UUID> uuidSet;
#TypeHandler ("com.mybatis.handlers.typeHandler.SetStringTypeHandler")
private Set<String> stringSet;
.....
}
OR if there is a way to mention these typeHandlers on Mapper-Level, something like this?
#Mapper
public interface MyBatisMapper {
//TYPE HANDLER ATTACHMENT/CONFIGURATION HERE ..???
#Select("SELECT uuidSet, stringSet FROM MyBatisDTO WHERE param = #{param}")
Cursor<MyBatisDTO> getData(#Param("param") final UUID param);
.....
}
You can do it with a result map:
#Mapper
public interface MyBatisMapper {
#Select("SELECT uuidSet, stringSet FROM MyBatisDTO WHERE param = #{param}")
#Results({
#Result(column = "uuidSet", property="uuidSet", typeHandler = "com.mybatis.handlers.typeHandler.SetUUIDTypeHandler"),
#Result(column = "stringSet", property="stringSet", typeHandler = "com.mybatis.handlers.typeHandler.SetStringTypeHandler")
})
Cursor<MyBatisDTO> getData(#Param("param") final UUID param);
}

Mapstruct can't map properties while using ObjectFactory

I need some community help to point me out where I'm wrong in my code... :)
I try to use mapstruct to map fields between 2 entities with the help of an #ObjectFactory.
Entity 1:
public class ScimUser {
#JsonProperty("addresses")
#Valid
private List<UserAddress> addresses = null;
}
Entity 2:
public class User {
#JsonProperty("postalAddress")
private PostalAddress postalAddress = null;
}
Mapper:
#Mapper(componentModel = "spring", uses = { AddressFactory.class })
public interface ScimUserMapper {
#Mapping(target = "postalAddress", source = "scimUser.addresses")
User toUser(ScimUser scimUser);
#Mapping(target = "addresses", source = "user.postalAddress")
ScimUser toScimUser(User user);
}
ObjectFactory:
#Component
public class AddressFactory {
#Autowired
private CountryMapper countryMapper;
#Autowired
private CountryRepository countryRepository;
#ObjectFactory
public PostalAddress toPostalAddress(List<UserAddress> addresses, #TargetType Class<PostalAddress> type) {
PostalAddress postalAddress = new PostalAddress();
if (addresses != null && !addresses.isEmpty()) {
UserAddress userAddress = addresses.stream().filter(UserAddress::isPrimary).findFirst().orElse(null);
if (userAddress == null) {
userAddress = addresses.get(0);
}
postalAddress.setAddressLine1(userAddress.getStreetAddress());
postalAddress.setPostCode(userAddress.getPostalCode());
postalAddress.setState(userAddress.getRegion());
postalAddress.setCity(userAddress.getLocality());
CountryJpa countryJpa = countryRepository.getCountryByIso2Code(userAddress.getCountry());
if (countryJpa != null) {
Country country = countryMapper.fromJPA(countryJpa);
postalAddress.setCountry(country);
}
}
return postalAddress;
}
#ObjectFactory
public List<UserAddress> toUserAddressList(PostalAddress address, #TargetType Class<List<UserAddress>> type) {
UserAddress userAddress = new UserAddress();
userAddress.setCountry(address.getCountry().getIso2());
userAddress.setFormatted("?");
userAddress.setLocality(address.getCity());
userAddress.setPostalCode(address.getPostCode());
userAddress.setPrimary(true);
userAddress.setRegion(address.getState());
userAddress.setStreetAddress(address.getAddressLine1());
userAddress.setType("?");
return Collections.singletonList(userAddress);
}
}
The code above gets me this error during source code generation:
Can't map property "java.util.List addresses" to "PostalAddress postalAddress". Consider to declare/implement a mapping method: "PostalAddress map(java.util.List value)".
Can't map property "PostalAddress postalAddress" to "java.util.List addresses". Consider to declare/implement a mapping method: "java.util.List map(PostalAddress value)".
It's not the first time that I struggle with using these object factories and I really don't get what I am doing wrong.
So if someone has an idea, I'd be glad to read it. :)
You are using the #ObjectFactory wrong. What you want to achieve is a custom mapping method.
#ObjectFactory needs to be used to create the target instance object. In your case if you just remove #ObjectFactory and #TargetType from your method then it should work correctly.
I have to stress out that you are doing quite some manual mapping there. You can easily provide methods for mapping between a single UserAddress and PostalAddress and just add wrappers for the collections.

Mapping Hierrachical Beans using mapstruct

This is an extension to this question.
class Customer{
// distinct properties
}
class RetailCustomer extends Customer{
// distinct properties
}
class WholeSaleCustomer extends Customer{
// distinct properties
}
class CustomerDO {
// String custType ; // flag used to determine if Customer is wholeSale or Retail
//few properties same as Customer/WholeSaleCustomer/RetailCustomer
// few distinct properties
}
#Mapper
public interface CustomerMapper{
default Customer toCustomer(CustomerDO customerDO) {
String custType = customerDO.getCustType();
if("W".equalsIgnoreCase(custType)){
return toWholeSaleCustomer(customerDO);
}
else {
return toRetailCustomer(CustomerDO);
}
}
#Mappings({
#Mapping(source="a", target="b"),
#Mapping(source="c", target="d"),
#Mapping(source="m", target="m")
})
WholeSaleCustomer toWholeSaleCustomer(CustomerDO customerDO);
#Mappings({
#Mapping(source="e", target="f"),
#Mapping(source="g", target="h"),
#Mapping(source="n", target="n")
})
RetailCustomer toRetailCustomer(CustomerDO customerDO);
}
I need to map from CustomerDO to WholeSaleCustomer/RetailCustomer based on custType flag in CustomerDO. But above defined mapper doesn't work. It gives me below error while compiling
CustomerMapper.java:[23,34] Ambiguous mapping methods found for mapping property "com.domain.CustomerDO customerDO" to com.role.Customer: com.role.Customer: toCustomer
r(com.domain.CustomerDO customerDO), com.role.WholeSaleCustomer toWholeSaleCustomer(com.domain.CustomerDO wsCustomer), com.role.RetailCustomer toRetailCustomer(com.domain.CustomerDO wsCustomer)
But if I change toCustomer(CustomerDo customerDO) signature to toCustomer(Object customerDO) and remove either of toWholeSaleCustomer/toRetailCustomer, it works. It will only map either of two types. But I want both. I've similar case for Service Bean. There are serveral child Services. I should be able to map them all whenever they are required
What you are looking for is Mapping method selection based on qualifiers.
So if your customer objects look like:
class WholeSaleCustomer extends Customer {
// distinct properties
}
class CustomerDO {
// String custType ; // flag used to determine if Customer is wholeSale or Retail
//few properties same as Customer/WholeSaleCustomer/RetailCustomer
// few distinct properties
private CustomerDO customerDO;
}
Then you would have to tell MapStruct which method it needs to use to perform the mapping. So your mapper would look like:
#Mapper
public interface CustomerMapper {
#Named("baseCustomer")
default Customer toCustomer(CustomerDO customerDO) {
String custType = customerDO.getCustType();
if("W".equalsIgnoreCase(custType)){
return toWholeSaleCustomer(customerDO);
}
else {
return toRetailCustomer(CustomerDO);
}
}
#Mappings({
#Mapping(source="customerDO", qualifiedByName = "baseCustomer"),
#Mapping(source="c", target="d"),
#Mapping(source="m", target="m")
})
WholeSaleCustomer toWholeSaleCustomer(CustomerDO customerDO);
#Mappings({
#Mapping(source="customerDO", qualifiedByName = "baseCustomer"),
#Mapping(source="g", target="h"),
#Mapping(source="n", target="n")
})
RetailCustomer toRetailCustomer(CustomerDO customerDO);
}
#Named should come from org.mapstruct.Named.

Mapstruct self defined mapper AND automatically generated one

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);
}

Categories