Part1
I am using the Java ModelMapper library (http://modelmapper.org/) to manage the mappings between my entities and DTOs. I have a Contact (entity) and a ContactView (DTO).
I have a string field in ContactView that doesn't exist in Contact called "type".
Its value should be just the name of the entity's subclass.
I have tried to make this custom mapping like this:
modelMapper.typeMap(Contact.class, ContactView.class).addMappings(mapper -> {
mapper.map(src -> src.getClass().getSimpleName(), ContactView::setType);
});
I get a compilation error at:
mapper.map(src -> src.getClass().getSimpleName(), ContactView::setType);
Illegal SourceGetter defined
1 error at
org.springframework.beans.factory.support.SimpleInstantiationStrategy.instantiate(SimpleInstantiationStrategy.java:185)
~[spring-beans-5.3.2.jar:5.3.2] at
org.springframework.beans.factory.support.ConstructorResolver.instantiate(ConstructorResolver.java:653)
~[spring-beans-5.3.2.jar:5.3.2] ... 33 common frames omitted
I even tried using a Converter, same result:
modelMapper.typeMap(Contact.class, ContactView.class).addMappings(mapper -> {
Converter<Class, String> toName = ctx -> ctx.getSource() == null ? null : ctx.getSource().getSimpleName();
mapper.using(toName).map(Contact::getClass, ContactView::setType);
});
Do you know how to solve this problem?
Part 2
Following up the proposed answer, I tried to add a Converter Class to the ModelMapper. This is where I configure the ModelMapper Bean:
#Configuration
public class Mapper {
#Autowired
private ContactTypeRepository contactTypeRepository;
#Bean
public ModelMapper getMapper() {
ModelMapper modelMapper = new ModelMapper();
modelMapper.getConfiguration()
.setMatchingStrategy(MatchingStrategies.STRICT);
modelMapper.typeMap(ContactTag.class, ReferenceEntityView.class).addMappings(mapper -> {
mapper.map(src -> src.getTag().getCode(), ReferenceEntityView::setCode);
mapper.map(src -> src.getTag().getValue(), ReferenceEntityView::setValue);
});
modelMapper.typeMap(Person.class, PersonView.class).addMappings(mapper -> {
mapper.skip(PersonView::setName);
mapper.map(Person::getName, PersonView::setLastName);
});
modelMapper.addConverter(new ContactConverter());
return modelMapper;
}
class ContactConverter implements Converter<Contact, ContactView> {
private ModelMapper localMapper = new ModelMapper();
#Override
public ContactView convert(MappingContext<Contact, ContactView> context) {
Contact contact = context.getSource();
ContactView contactView = localMapper.map(contact, ContactView.class);
ContactType contactType = contactTypeRepository.getByCode(context.getSource().getClass().getSimpleName().toLowerCase());
contactView.setType(localMapper.map(contactType, ReferenceEntityView.class));
return contactView;
}
}
}
This is where I use the ModelMapper Bean to generate my DTO:
#RestController
#RequestMapping(value = "/contacts")
public class ContactController {
#Autowired
private ContactRepository contactRepository;
#Autowired
private ModelMapper modelMapper;
#GetMapping(value = "/{id}")
#ResponseStatus(HttpStatus.OK)
public ContactView findById(#PathVariable("id") Long id){
Contact c = contactRepository.getOne(id);
ContactView cv = modelMapper.map(c, ContactView.class);
return cv;
}
}
For some reason, the convert method from the Converter is not called and the "type" field from the ContactView object is null. The other mappings on the ModelMapper Bean are working properly.
It happens because of the implementation of ModelMapper
public boolean isValid(M member) {
return !Modifier.isStatic(member.getModifiers()) && !member.isSynthetic();
}
And the documentation for isSynthetic method says
Returns true if this member was introduced by the compiler; returns
false otherwise. Returns: true if and only if this member was
introduced by the compiler.
I guess that is why it fails an exception.
For a similar case we introduced a specific mapper class use modelMapper as base mapper and set the other field:
class ContactMapper{
...
public ContactView toView(Contact contact){
ContactView contactView = modelMapper.map(contact,ContactView.class);
contactView.setType(contact.getClass().getSimpleName());
return contactView;
}
To make it consistent with the overall mapping you can define this as a converter and register it to your mapping like this
class ContactConverter implements Converter<Contact, ContactView> {
private final ModelMapper localMapper = new ModelMapper();
#Override
public ContactView convert(MappingContext<Contact, ContactView> context) {
Contact contact = context.getSource();
ContactView contactView = localMapper.map(contact, ContactView.class);
contactView.setType(contact.getClass().getSimpleName());
return contactView;
}
}
ModelMapper modelMapper = new ModelMapper();
modelMapper.addConverter(new ContactConverter());
Try my library beanknife. It's a annotation processor. It means jdk will generate the class for you before you compile the project. So it do the work at compile time. You can use the generated class like any other classes you write yourselives. And you can see the source of the generated class, So no more magic.
This library is able to generate the DTO class for you. You don't need change the original class. In addition to configuring annotations on the original class, you can also choose to create a new configuration class and configure annotations on top of that. The library supports copying and inheriting all properties of the original class, and removing modifying or adding properties base on that. For your question:
// this will generate a DTO class named "ContactView".
// (This is the default name which just append 'View')
// You can change this name using genName attribute.
#ViewOf(value=Contact.class, includePattern = ".*")
public class ContactViewConfigure {
// Add a new property.
// Make sure there is no property named 'type' already in Contact.
// Or you need use #OverrideViewProperty
// There are multi way to add new property.
// In this way, you use a public static method accept the original class instance as the unique argument.
// The new property name is decide by the attribute 'type' of #NewViewProperty.
// So the method name is not important.
#NewViewProperty("type")
public static String type(Contact contact) {
return contact.getClass().getSimpleName()
}
}
// This is the generated class.
public class ContactView {
// other properties
...
private String type;
// getters and setters (By default only getters are generated)
...
// many constructors
...
public static ContactView read(Contact source) {
ContactView out = new ContactView();
// initialize other properties
...
out.type = ContactViewConfigure.type(source);
// initialize other properties
...
return out;
}
// other read method, such as read list, set and map.
...
// other generated methods
...
}
// use like this.
Contact contact = ...
ContactView dto = ContactView.read(contact);
In some situations, beanknife is much powerful than ModelMapper. For example, if something wrong happened, you can check the source of the generated class (Usually located in /target/generated-source/annotations, may differ on IDE), and see why. If it really a bug, you can commit issue to the github, I will deal with it as soon as possible.
Here are more examples.
Related
In a Spring Boot application, Spring Boot is used to build a Properties object from a YAML file as follows:
YamlPropertiesFactoryBean yamlFactory = new YamlPropertiesFactoryBean();
yamlFactory.setResources(new DefaultResourceLoader().getResource("application.yml"));
Properties properties = yamlFactory.getObject();
The reason why Spring Boot's own parser is used is that it not only reads YAML-compliant settings, but also dot-notated properties like e.g:
artist.elvis.name: "Elvis"
artist.elvis.message: "Aloha from Hawaii"
Now that the Properties object is built, I want to map it into an object like the following for example:
#JsonIgnoreProperties(ignoreUnknown = true)
private record Artist(Elvis elvis) {
private record Elvis(String name, String message) { }
}
My question is:
How can this be done with Jackson? Or is there another/better solution for this?
Many thanks for any help
I saw functionality like that in Ratpack framework.
e.g.:
var propsFileUrl =
Thread.currentThread()
.getContextClassLoader()
.getResource("application.properties");
ApplicationProperties applicationProperties =
ConfigData.builder()
.props(propsFileUrl)
.build()
.get(ApplicationProperties.class);
under the hood it is indeed done by using jackson's object mapper, but the logic is not as trivial to post it here.
here's the library:
https://mvnrepository.com/artifact/io.ratpack/ratpack-core/2.0.0-rc-1
application.yml is the default yml file, so no custom configuration is required. Value annotation should be able to read the properties.
#Value("${artist.elvis.name}")
private String name;
Next part I am not sure about your requirements, but hope this is what you are looking for.
To bind to this object 'constructor' can be a good option.
Class for elvis
#Bean
public class Elvis {
private String name;
private String message;
public Elvis(#Value("${artist.elvis.name"}) final String name, #Value("${artist.elvis.message"}) final String message) {
this.name=name;
this.message=message
}
// getter setter for name and message
}
Now Autowire the created bean to Artist bean
#Bean("artists")
public class Artists {
#Autowired
private Elvis elvis
pubic Elvis getElvis() {
return elvis;
}
}
I map two classes which properties have ArrayLists and ModelMappers works well. I have ModelMapper 3.1.0 version and SpringBoot version 2.7.4. in my project. Here's the snippet of two models: VetClinic and VetCLinicDto:
public class VetClinic{
private ArrayList<Doctor> doctors;
...
}
public class VetClinic{
private ArrayList<DoctorDto> doctorDtos;
...
}
And for help ModelMapper with mapping specific fields like Collections I wrote the code:
#Override
protected void mapSpecificFieldsToModelFromDto(VetClinicDto source, VetClinic destination) {
if (source.getDoctors() != null)
destination.setDoctors(source.getDoctors()
.stream().map(doctorDto -> mapper.map(doctorDto, Doctor.class))
.toList());
...
}
This code snippet works correctly, but where I try to change ArrayList to List like this:
public class VetClinic{
private List<Doctor> doctors;
...
}
public class VetClinic{
private List<DoctorDto> doctorDtos;
...
}
The issue occurs when ModelMapper tries to map VetClinicDto to VetClinic but throws an error:
1) Failed to instantiate instance of destination java.util.List. Ensure that java.util.List has a non-private no-argument constructor.
I've tried to initialize List to ArrayList right away. Like this:
public class VetClinic{
private List<Doctor> doctors = new ArrayList<>();
...
}
public class VetClinic{
private List<DoctorDto> doctorDtos = new ArrayList<>();
...
}
But it doesn't work. I can't understand why does ModelMapper cannot map one List to another List.
The details that are missing to answer this question fully is in the "mapper.map(doctorDto, Doctor.class)"
But I would take a look at this link: https://www.baeldung.com/java-copy-list-to-another
It will give you some ideas on how to write your mapper to handle this. I would also suggest you look into using the mapstruct framework to do this mapping for you where it can easily handle mapping lists to lists and also if the objects have the same attributes, there is very little configuration needed.
#Mapper(componentModel = "spring")
public interface DoctorMapper {
List<DoctorDto> map(List<Doctor> doctors);
DoctorDto map(Doctor doctor);
}
I am using MapStruct to convert a database entity to Immutable model object. So Immutable object doesn't have setters but Mapstruct requires setters when mapping objects. So I created an explicit builder using Immutable object builder to provides to Mapstruct. Below are the snippets from code:
#Value.Immutable
#Value.Style(overshadowImplementation = true)
public interface CarModel {
#Nullable String getCarId();
}
#Mapper(uses = ImmutablesBuilderFactory.class)
public interface CarMapper {
CarMapper INSTANCE = Mappers.getMapper(CarMapper.class);
#Mapping(source = "id", target = "carId")
ImmutableCarModel.Builder toModel(CarEntity carEntity);
}
public class ImmutablesBuilderFactory {
public ImmutableCarModel.Builder createCarModelBuilder() {
return ImmutableCarModel.builder();
}
}
Below code was generated by Mapstruct:
public class CarMapperImpl implements CarMapper {
#Autowired
private final ImmutablesBuilderFactory immutablesBuilderFactory
#Override
public Builder toModel(CarEntity carEntity) {
if ( carEntity == null ) {
return null;
}
Builder builder = immutablesBuilderFactory.createCarModelBuilder();
if ( carEntity.getId() != null ) {
builder.carId( carEntity.getId() );
}
return builder;
}
}
I was able to convert an entity to Immutable model object but unit test is failing for this. It is throwing NPE at below line of code in CarMapperImpl class while calling CarMapper.INSTANCE.toModel(carEntity).build(); in unit test
Builder builder = immutablesBuilderFactory.createCarModelBuilder();
Does anyone have any idea what's going wrong here?
The reason for the NPE is because you are mixing the usage of the default and spring component model.
The Mappers#getMapper is only meant to be used with the default component model. When using a dependency injection framework you need to use the framework to get access to the mapper.
This was due to below property in MapStruct configuration
-Amapstruct.defaultComponentModel=spring
After removing this, Mapstruct was not autowiring and was able to create an instance of ImmutablesBuilderFactory.
I have the following classes:
public class MyEntity {
private Set<MyOtherEntity> other;
}
public class MyDTO {
private List<MyOtherDTO> other;
}
I created two PropertyMaps (using ModelMapper), one for each conversion from and to DTO
public class DTOToEntityPropertyMap extends PropertyMap<MyDTO, MyEntity> {
#Override
protected void configure() {
List<MyOtherDTO> myOtherDTOs = source.getOther();
Set<MyOtherEntity> myOtherEntities = new HashSet<>();
for (MyOtherDTO myOtherDTO : myOtherDTOs) {
MyOtherEntity myOtherEntity = ModelMapperConverterService.convert(myOtherDTO, MyOtherEntity.class);
myOtherEntities.add(myOtherEntity);
}
map().setOther(myOtherEntities);
}
}
public class EntityToDTOPropertyMap extends PropertyMap<MyEntity, MyDTO> {
#Override
protected void configure() {
Set<MyOtherEntity> myOtherEntities = source.getOther();
List<MyOtherDTO> myOtherDTOs = new ArrayList<>();
for (MyOtherEntity myOtherEntity : myOtherEntities) {
MyOtherDTO myOtherDTO = ModelMapperConverterService.convert(myOtherEntity, MyOtherDTO.class);
myOtherDTOs.add(myOtherDTO);
}
map().setOther(myOtherDTOs);
}
}
Adding the PropertyMaps to the ModelMapper creates the following error:
Caused by: org.modelmapper.ConfigurationException: ModelMapper
configuration errors:
1) Invalid source method java.util.List.add(). Ensure that method has
zero parameters and does not return void.
I guess I cannot use List.add() in the configuration of a PropertyMap.
Then, what is the best way to implement the conversion of a List to a Set and backwards in the ModelMapper?
The PropertyMap must use it to map properties. If you put something of different logic it will throw an exception.
So, you have the next options (I put the examples in one direction, the other is the same):
Option 1: Ensure PropertyMap match the properties
Using a property map and ensure ModelMapper will use it, take a look the rules of Matching Strategies:
Standard: by default.
Loose
Strict
Use the one which ensure your property list other will match with the destination property other. For example, if you use Strict Strategy and is not in the correct order it will not match it.
Then, if you are sure the property will match ModelMapper will map the property following its rules for you or if you need it, you would be able to create a PropertyMap whith source MyOtherEntity and destination MyOtherDTO and add it to your ModelMapper instance.
public class DTOToEntityPropertyMap extends PropertyMap<MyOtherEntity, MyOtherDTO> {
#Override
protected void configure() {
map().setPropertyOfOther(source.getPropertyOfOther());
//and so on...
}
}
modelMapper.addMappings(new DTOToEntityPropertyMap());
Note: if your parent mapping (In your case MyEntity to MyDto) have other PropertyMap you would need to add the PropertyMapping of the list classes before the parent, if not it will not use this property map.
Option 2: Create a converter of the lists and use it
Other option is to create a Converter of the classes of your lists (MyOtherEntity -> MyOtherDto) and use it in the parent PropertyMap.
Converter<Set<MyOtherEntity> , List<MyOtherDto>> toOtherDto = new Converter<Set<MyOtherEntity> , List<MyOtherDto>>() {
public String convert(MappingContext<Set<MyOtherEntity> , List<MyOtherDto>> context) {
Set<MyOtherEntity> source = context.getSource();
List<MyOtherDto> destination = context.getDestination();
//Convert it using the logic you want (by hand for example)
return destination;
}
};
Then you must use it in your Parent PropertyMap, as next:
public class EntityToDTOPropertyMap extends PropertyMap<MyEntity, MyDTO> {
Converter<Set<MyOtherEntity> , List<MyOtherDto>> toOtherDto = new Converter<Set<MyOtherEntity> , List<MyOtherDto>>() {
public String convert(MappingContext<Set<MyOtherEntity> , List<MyOtherDto>> context) {
Set<MyOtherEntity> source = context.getSource();
List<MyOtherDto> destination = context.getDestination();
//Convert it using the logic you want (by hand for example)
return destination;
}
};
#Override
protected void configure() {
using(toOtherDto).map(source.getOther()).setOther(null);
}
}
Then ensure this PropertyMap is added to your ModelMapper instance:
modelMapper.addMappings(new EntityToDTOPropertyMap());
I am using Spring Data for MongoDB and I need to be able to configure collection at runtime.
My repository is defined as:
#Repository
public interface EventDataRepository extends MongoRepository<EventData, String> {
}
I tried this silly example:
#Document(collection = "${mongo.event.collection}")
public class EventData implements Serializable {
but mongo.event.collection did not resolve to a name as it does with a #Value annotation.
A bit more debugging and searching and I tried the following:
#Document(collection = "#{${mongo.event.collection}}")
This produced an exception:
Caused by: org.springframework.expression.spel.SpelParseException: EL1041E:(pos 1): After parsing a valid expression, there is still more data in the expression: 'lcurly({)'
at org.springframework.expression.spel.standard.InternalSpelExpressionParser.doParseExpression(InternalSpelExpressionParser.java:129)
at org.springframework.expression.spel.standard.SpelExpressionParser.doParseExpression(SpelExpressionParser.java:60)
at org.springframework.expression.spel.standard.SpelExpressionParser.doParseExpression(SpelExpressionParser.java:32)
at org.springframework.expression.common.TemplateAwareExpressionParser.parseExpressions(TemplateAwareExpressionParser.java:154)
at org.springframework.expression.common.TemplateAwareExpressionParser.parseTemplate(TemplateAwareExpressionParser.java:85)
Perhaps I just don't know how to quite use SPel to access values from Spring's Property Configurer.
When stepping through the code, I see that there is a way to specify collection name or even expressions, however, I am not sure which annotation should be used for this purpose or how to do it.
Thanks.
-AP_
You can solve this problem by just using SPeL:
#Document(collection = "#{environment.getProperty('mongo.event.collection')}")
public class EventData implements Serializable {
...
}
Update Spring 5.x:
Since Spring 5.x or so you need an additional # before environment:
#Document(collection = "#{#environment.getProperty('mongo.event.collection')}")
public class EventData implements Serializable {
...
}
Docs:
SpEL: 4.2 Expressions in Bean Definitions
SpEL: 4.3.12 Bean References
PropertyResolver::getProperty
So, at the end, here is a work around that did the trick. I guess I really don't know how to access data from Spring Properties Configurer using the SPeL expressions.
In my #Configuration class:
#Value("${mongo.event.collection}")
private String
mongoEventCollectionName;
#Bean
public String mongoEventCollectionName() {
return
mongoEventCollectionName;
}
On my Document:
#Document(collection = "#{mongoEventCollectionName}")
This, appears to work and properly pick up the name configured in my .properties file, however, I am still not sure why I could not just access the value with $ as I do in the #Value annotation.
define your entity class like
#Document(collection = "${EventDataRepository.getCollectionName()}")
public class EventData implements Serializable {
Define a custom repository interface with getter and setter methods for "collectionName"
public interface EventDataRepositoryCustom {
String getCollectionName();
void setCollectionName(String collectionName);
}
provide implementation class for custom repository with "collectionName" implementation
public class EventDataRepositoryImpl implements EventDataRepositoryCustom{
private static String collectionName = "myCollection";
#Override
public String getCollectionName() {
return collectionName;
}
#Override
public void setCollectionName(String collectionName) {
this.collectionName = collectionName;
}
}
Add EventDataRepositoryImpl to the extends list of your repository interface in this it would look like
#Repository
public interface EventDataRepository extends MongoRepository<EventData, String>, EventDataRepositoryImpl {
}
Now in your Service class where you are using the MongoRepository set the collection name, it would look like
#Autowired
EventDataRepository repository ;
repository.setCollectionName("collectionName");
Entity Class
#Document // remove the parameters from here
public class EscalationCase
{
}
Configuration class
public class MongoDBConfiguration {
private final Logger logger = LoggerFactory.getLogger(MongoDBConfiguration.class);
#Value("${sfdc.mongodb.collection}") //taking collection name from properties file
private String collectionName;
#Bean
public MongoTemplate mongoTemplate(MongoDbFactory mongoDbFactory, MongoMappingContext context) {
MappingMongoConverter converter = new MappingMongoConverter(new DefaultDbRefResolver(mongoDbFactory), context);
converter.setTypeMapper(new DefaultMongoTypeMapper(null));
MongoTemplate mongoTemplate = new MongoTemplate(mongoDbFactory, converter);
if (!mongoTemplate.collectionExists(collectionName)) {
mongoTemplate.createCollection(collectionName); // adding the collection name here
}
return mongoTemplate;
}
}