ModelMapper: matches multiple source property hierarchies - java

I cannot resolve modelMapper error. Do you have any ideas where is the issue?
NB: In view java.sql.Time doesn't have non-argument constructor I didn't find the better way than to write converter
org.modelmapper.ConfigurationException: ModelMapper configuration errors:
1) The destination property
biz.models.CarWash.setSecondShift()/java.util.Date.setTime() matches
multiple source property hierarchies:
biz.dto.CarWashDTO.getFirstShift()/java.time.LocalTime.getSecond()
biz.dto.CarWashDTO.getSecondShift()/java.time.LocalTime.getSecond()
The error was made by this code
#SpringBootTest
#RunWith(SpringRunner.class)
public class CarWashDTO2CarWash {
#Autowired
protected ModelMapper modelMapper;
#Test
public void testCarWashDTO2CarWash_allFiledShouldBeConverted(){
CarWashDTO dto = CarWashDTO.builder()
.name("SomeName")
.address("SomeAddress")
.boxCount(2)
.firstShift(LocalTime.of(9, 0))
.secondShift(LocalTime.of(20, 0))
.phoneNumber("5700876")
.build();
modelMapper.addConverter((Converter<CarWashDTO, CarWash>) mappingContext -> {
CarWashDTO source = mappingContext.getSource();
CarWash destination = mappingContext.getDestination();
destination.setId(source.getId());
destination.setFirstShift(source.getFirstShift() == null ? null : Time.valueOf(source.getFirstShift()));
destination.setSecondShift(source.getSecondShift() == null ? null : Time.valueOf(source.getSecondShift()));
destination.setEnable(true);
destination.setAddress(source.getAddress());
destination.setBoxCount(source.getBoxCount());
destination.setName(source.getName());
destination.setDateOfCreation(source.getDateOfCreation());
return destination;
});
final CarWash entity = modelMapper.map(dto, CarWash.class);
assertNotNull(entity);
assertEquals(2, entity.getBoxCount().intValue());
assertEquals("SomeAddress", entity.getAddress());
assertEquals("SomeName", entity.getName());
}
}
The modelmapper bean is built by the next configuration
#Bean
public ModelMapper modelMapper(){
return new ModelMapper();
}
Dto:
public class CarWashDTO {
private Long id;
private String name;
private String address;
private String phoneNumber;
private Integer boxCount;
private LocalTime firstShift;
private LocalTime secondShift;
private LocalDateTime dateOfCreation;
}
Entity (firstShift and secondShift have java.sql.Time type):
public class CarWash {
private Long id;
private String name;
private String address;
private String phoneNumber;
private Integer boxCount;
private Time firstShift;
private Time secondShift;
private LocalDateTime dateOfCreation;
private Boolean enable;
private Owner owner;
}

try modelMapper.getConfiguration().setMatchingStrategy(MatchingStrategies.STRICT)

This resolved my problem:
modelMapper.getConfiguration().setAmbiguityIgnored(true);

You need to customize ModelMapper configuration during Bean initialization with the help of a PropertyMap:
http://modelmapper.org/user-manual/property-mapping/
#Bean
public ModelMapper modelMapper(){
ModelMapper mm = new ModelMapper();
PropertyMap<CarWashDTO, CarWash> propertyMap = new PropertyMap<CarWashDTO, CarWash> (){
protected void configure() {
map(source.getId()).setId(null);
}
}
mm.addMappings(propertyMap);
return mm;
}

I am not sure how it was with ModelMapper when question was asked but using converter should be straightforward. Instead of implementing a converter for the whole class implement it to the types that actually need conversion. So like:
public static Converter<LocalTime, Time> timeConverter = new AbstractConverter<>() {
#Override
protected Time convert(LocalTime source) {
return null == source ? null : Time.valueOf(source);
}
};
Then it is just to:
mm.addConverter(timeConverter);
Guess if using Spring or EJB you know howto add this into your configuration.

Related

Spring Boot findById not working for MongoDB

I'm trying to do a simple get query on springboot using mongodb as database engine
I have tried with several stuff(sending the data as ObjectId and even changing the repository)
public ResponseEntity<Track> get(String trackId) {
Track find = mongoTemplate.findById(new ObjectId(trackId), Track.class);
Optional<Track> track = tracksRepository.findById(trackId);
if (track.isPresent()) {
return new ResponseEntity<>(track.get(), HttpStatus.OK);
}
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
}
with mongo config
#Configuration
#EnableMongoRepositories(basePackages = "data.store.repositories")
public class MongoConfig extends AbstractMongoClientConfiguration {
private final Logger LOGGER = Logger.getLogger(this.getClass().getSimpleName());
#Primary
#Bean
#Override
public MongoClient mongoClient() {
return MongoClients.create(MongoClientSettings.builder()
.applyToClusterSettings(builder -> builder.hosts(Arrays.asList(new ServerAddress(host, port))))
.build());
}
private MongoCredential mongoCredentials() {
return MongoCredential.createCredential(username, database, password.toCharArray());
}
#Bean
public MongoTemplate mongoTemplate() {
MongoTemplate mongoTemplate = new MongoTemplate(mongoClient(), getDatabaseName());
mongoTemplate.setReadPreference(ReadPreference.secondaryPreferred());
return mongoTemplate;
}
protected String getDatabaseName() {
return database;
}
#Override
public boolean autoIndexCreation() {
return false;
}
}
EDIT: Adding class for context
#Document("track")
public class Track {
#Id
#Field(ATTR_ID)
#JsonProperty(ATTR_ID)
public String id;
public static final String ATTR_ID = "id";
}
and getting always null, with existing keys on my database. could you help me find the issue?
Thanks in advance
I tried this with similar configuration class and found the following worked fine creating/accessing data using MongoTemplate.
The POJO class:
public class Test {
#MongoId(FieldType.OBJECT_ID)
private String id;
private String name;
public Test() {
}
public Test(String s) {
super();
this.name = s;
}
// get, set methods
public String toString( ) {
return id + " - " + name;
}
}
From Spring's CommandLineRunner.run():
// Insert a document into the database
Test t1 = new Test("alpha");
t1 = mt.insert(t1);
System.out.println(t1); // 61e7de9f5aadc2077d9f4a58 - alpha
// Query from the database using the _id
ObjectId id = new ObjectId("61e7de9f5aadc2077d9f4a58");
Test t2 = mt.findById(id, Test.class);
System.out.println(t2);
Note that you need to do this from the class where you are running the code:
#Autowired private MongoTemplate mt;
You can use the #MongoId or #Id annotations in our POJO class to represent MongoDB _id field. The type of the field can be a String or ObjectId. It depends upon how you define.
See this from Spring Data MongoDB documentation on How the _id Field is Handled in the Mapping Layer using:
#MongoId
#Id
Solution is to add to MongoId annotation field type object id
#MongoId(FieldType.OBJECT_ID)
private String id;

Convert Complex Entity to DTO With ModelMapper

i'm working in a rest API using Spring boot.
when i wanted to return my entity from an End Point i realized that the Properties are different from what i need on my response so i tried to use Model Mapper to return a DTO.
My entity is like this:
public class RuleEntity {
private String ruleId;
private String bankDecision;
private String aggregatorFunctionType;
private String limitOperatorType;
private double limitRule;
private Integer windowMinutes;
private Integer layer;
private String expressionRule;
private String status;
private List<GroupingKeyName> groupingKeyNames;
private List<RuleFilter> ruleFilters;
}
And the DTO that i need Must Be Like this:
public class RuleDTO {
private String ruleId;
private String bankDecision;
private String aggregatorFunctionType;
private String limitOperatorType;
private double limitRule;
private Integer windowMinutes;
private Integer layer;
private String expressionRule;
private String status;
private List<String> groupingKeyNames;
private List<String> ruleFilters;
}
The only change is that the last two lists are of String instead of The Object
The Objects groupingKeyNames and ruleFilters have a Name and an ID, and i only need the name on the list of DTO so it is a List of Strings
I tried using Model Mapper like this:
ModelMapper modelMapper = new ModelMapper();
RuleSetModel ruleSetModel = modelMapper.map(ruleSetEntity, RuleSetModel.class);
it works, with all the properties but in the Lists it is returning something like:
groupingKeyNames=[GroupingKeyName(groupingKeyId=1, name=cardHash)], ruleFilters=[RuleFilter(ruleFilterId=1, name=status)]
What could i do so i get a result like this:
groupingKeyNames=[cardHash], ruleFilters=[status]
Thanks in advance!
Create a method into your RuleEntity to do it
public RuleDTO dto() {
// config to skip
PropertyMap<RuleEntity, RuleDTO> propertyMap = new PropertyMap<RuleEntity, RuleDTO>() {
#Override
protected void configure() {
skip(destination.getGroupingKeyNames());
skip(destination.getRuleFilters());
}
};
RuleDTO ruleDTO = new RuleDTO();
ModelMapper modelMapper = new ModelMapper();
modelMapper.getConfiguration().setPropertyCondition(Conditions.isNotNull());
modelMapper.addMappings(propertyMap);
modelMapper.map(this,ruleDTO);
if (!this.groupingKeyNames.isEmpty()) {
ruleDTO.getGroupingKeyNames().clear();
List<String> tmpGroupingKeyNames = new ArrayList<>();
this.getGroupingKeyNames().forEach(itemDTO -> {
tmpGroupingKeyNames.add(itemDTO.name);
});
ruleDTO.getGroupingKeyNames().addAll(tmpGroupingKeyNames);
}
if (!this.ruleFilters.isEmpty()) {
ruleDTO.getRuleFilters().clear();
List<String> tmpRuleFilters = new ArrayList<>();
this.getRuleFilters().forEach(itemDTO -> {
tmpRuleFilters.add(itemDTO.name);
});
ruleDTO.getRuleFilters().addAll(tmpRuleFilters);
}
return ruleDTO;
}

Not Able to Successfully USE Validations in Spring REST Webservice

I am trying to apply validations on my SPRING REST-API but i am getting this exception:
Apr 10, 2020 12:05:26 PM org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver doResolveHandlerMethodExceptionWARNING: Failed to invoke #ExceptionHandler method: public com.luv2code.springdemo.exceptionhandling.RestFieldErrorValidation com.luv2code.springdemo.exceptionhandling.GlobalExceptionHandler.processValidationError(org.springframework.web.bind.MethodArgumentNotValidException)org.springframework.http.converter.HttpMessageNotWritableException: No converter found for return value of type: class com.luv2code.springdemo.exceptionhandling.RestFieldErrorValidation at org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodProcessor.writeWithMessageConverters(AbstractMessageConverterMethodProcessor.java:226)
Entity Class:
#Entity#Table(name="customer")
public class Customer {
#Id
#GeneratedValue(strategy=GenerationType.IDENTITY)
#Column(name="id")
private int id;
#Column(name="first_name")
#NotNull(message = "Firstname is necessary")
#Size(min=1,message="This field is required")
private String firstName;
#Column(name="last_name")
#NotNull(message = "Lastname is necessary")
#Size(min=1,message="This field is required")
private String lastName;
#Column(name="email")
private String email;
// getters and setters
}
FieldValidation Handler classes:
public class RestFieldError {
private String field;
private String message;
public RestFieldError() {
}
// getters and setters
}
and
public class RestFieldErrorValidation {
private List<RestFieldError> fieldErrors = new ArrayList<>();
public RestFieldErrorValidation() {
}
public void addFieldError(String path, String message) {
RestFieldError error = new RestFieldError(path, message);
fieldErrors.add(error);
}
}
RestController Code:
#RestController
#RequestMapping("/api")
public class CustomerRestController {
// autowire the CustomerService
#Autowired
private CustomerService customerService;
#InitBinder
public void initBinder(WebDataBinder dataBinder) {
System.out.println("Entered init binder");
StringTrimmerEditor stringTrimmerEditor = new StringTrimmerEditor(true);
dataBinder.registerCustomEditor(String.class, stringTrimmerEditor);
}
// add the mapping for POST/customers (add a new customer)
#PostMapping("/customers")
#ResponseBody
public Customer addCustomer(#Valid #RequestBody Customer theCustomer) {
System.out.println("theCustomer :"+theCustomer.getFirstName());
theCustomer.setId(0);
customerService.saveCustomer(theCustomer);
return theCustomer;
}
}
Exception handler Class:
#ControllerAdvice
public class GlobalExceptionHandler {
// Adding Validation Support on REST APIs--------------------------------------------------------->
private MessageSource messageSource;
#Autowired
public GlobalExceptionHandler(MessageSource messageSource) {
this.messageSource = messageSource;
}
#ExceptionHandler(MethodArgumentNotValidException.class)
#ResponseStatus(HttpStatus.BAD_REQUEST)
#ResponseBody
public RestFieldErrorValidation processValidationError(MethodArgumentNotValidException ex) {
BindingResult result = ex.getBindingResult();
List<FieldError> fieldErrors = result.getFieldErrors();
return processFieldErrors(fieldErrors);
}
private RestFieldErrorValidation processFieldErrors(List<FieldError> fieldErrors) {
RestFieldErrorValidation dto = new RestFieldErrorValidation();
for (FieldError fieldError: fieldErrors) {
String localizedErrorMessage = resolveLocalizedErrorMessage(fieldError);
dto.addFieldError(fieldError.getField(), localizedErrorMessage);
}
return dto;
}
private String resolveLocalizedErrorMessage(FieldError fieldError) {
Locale currentLocale = LocaleContextHolder.getLocale();
String localizedErrorMessage = messageSource.getMessage(fieldError, currentLocale);
//If the message was not found, return the most accurate field error code instead.
//You can remove this check if you prefer to get the default error message.
if (localizedErrorMessage.equals(fieldError.getDefaultMessage())) {
String[] fieldErrorCodes = fieldError.getCodes();
localizedErrorMessage = fieldErrorCodes[0];
}
return localizedErrorMessage;
}
}
Here is the google drive link of the project if you can check the code:
https://drive.google.com/open?id=1QSFVMi3adHGkc7BqXsqAY0P_tO2UfT2I
Here is the Article that i followed:
https://www.petrikainulainen.net/programming/spring-framework/spring-from-the-trenches-adding-validation-to-a-rest-api/
I'm assuming you are using plain Spring here, not Spring Boot.
The question is: To what exactly do you want to convert your RestFieldErrorValidation object? XML? JSON?
For either, you need an appropriate third-party library on your classpath, so Spring can do the conversion automatically.
In the case of JSON, you might want to add this dependency to your project.
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.10.2</version>
</dependency>

ModelMapper and LocalDate - Spring Boot

currently I'm trying to map a dto to a class that also contains LocalDate attribute. Merely I have no success here and the local date field always remains null. So I built a short example, where I followed the pretty helpful hints from Modelmapper to convert from String to LocalDate
So I have a ModelMapper class like this :
#Bean
public ModelMapper createMapper() {
ModelMapper modelMapper = new ModelMapper();
modelMapper.createTypeMap(String.class, LocalDate.class);
Provider<LocalDate> localDateProvider = new AbstractProvider<LocalDate>() {
#Override
public LocalDate get() {
return LocalDate.now();
}
};
Converter<String, LocalDate> toStringDate = new AbstractConverter<String, LocalDate>() {
#Override
protected LocalDate convert(String source) {
DateTimeFormatter format = DateTimeFormatter.ofPattern("yyyy-MM-dd");
LocalDate localDate = LocalDate.parse(source, format);
return localDate;
}
};
modelMapper.getTypeMap(String.class, LocalDate.class).setProvider(localDateProvider);
modelMapper.addConverter(toStringDate);
return modelMapper;
}
Furthermore I have a POJO that only has 2 fields, an id and a local date (w/o getters and setters for the sake of readability.
public class JsonLocalDate {
private Long id;
#JsonFormat(pattern = "yyyy-MM-dd")
private LocalDate ld;
public Long getId() {
return id;
}
And I created a test class where I tried to mock the json part by a LinkedHashMap as it comes in the web services I have implemented :
#RunWith(SpringRunner.class)
#SpringBootTest(webEnvironment = WebEnvironment.NONE)
public class ModelMapperTest {
#Autowired
ModelMapper mapper;
String jsonLd = "2018-06-11";
LinkedHashMap<String, String> lhm;
#Before
public void init() {
lhm = new LinkedHashMap<>();
lhm.put("id", "1");
lhm.put("ld", jsonLd);
}
#Test
public void checkModelMapper() {
assertNotNull(mapper);
Collection<TypeMap<?, ?>> c = mapper.getTypeMaps();
assertNotNull(c);
for (TypeMap<?, ?> typeMap : c) {
System.out.println("TypeMap : " + typeMap.getConverter().toString());
}
}
#Test
public void testLocalDate() {
LocalDate ld = mapper.map(jsonLd, LocalDate.class);
assertNotNull(ld);
assertEquals(ld, LocalDate.parse(jsonLd,
DateTimeFormatter.ofPattern("yyyy-MM-dd")));
}
#Test
public void testLocalDateInObject() {
JsonLocalDate jld = mapper.map(jsonLd, JsonLocalDate.class);
assertNotNull(jld);
assertEquals(jld.getLd(), LocalDate.parse(jsonLd,
DateTimeFormatter.ofPattern("yyyy-MM-dd")));
}
}
testLocalDate where I just map the String to LocalDate works fine, while the testLocalDateInObject fails.
Has anybody any idea how I have to deal with LocalDate fields to get them mapped ?
Thanks in advance !
Cheers
Joern
The test failed is because you are trying to map a String to an Object and ModelMapper doesn't know how to map a String to an Object.
So you should try Property Mapping
modelMapper.typeMap(String.class, JsonLocalDate.class)
.addMapping(src -> src, JsonLocalDate::setLd);

MongoDB No converter found capable of converting from type [java.lang.String] to type [java.time.LocalDateTime]

MongoDB, Spring Data, findAll() method error:
No converter found capable of converting from type [java.lang.String]
to type [java.time.LocalDateTime]
public class EntityName {
#Id
private String id;
private Map<LocalDateTime, Integer> statistic;
}
I am able to save entity, but not able to load it. Any quick fixes?
This solved the problem:
#Configuration
public class MongoConfiguration extends AbstractMongoConfiguration {
#Value("${spring.data.mongodb.database:test}")
private String database;
#Value("${spring.data.mongodb.host:localhost}:${spring.data.mongodb.port:27017}")
private String host;
#Autowired
private MappingMongoConverter mongoConverter;
// Converts . into a mongo friendly char
#PostConstruct
public void setUpMongoEscapeCharacterConversion() {
mongoConverter.setMapKeyDotReplacement("_");
}
#Override
protected String getDatabaseName() {
return database;
}
#Override
public Mongo mongo() throws Exception {
return new MongoClient(host);
}
#Bean
#Override
public CustomConversions customConversions() {
List<Converter<?, ?>> converterList = new ArrayList<Converter<?, ?>>();
converterList.add(new MongoLocalDateTimeFromStringConverter());
return new CustomConversions(converterList);
}
private static final class MongoLocalDateTimeFromStringConverter implements Converter<String, LocalDateTime> {
#Override
public LocalDateTime convert(String source) {
return source == null ? null : LocalDateTime.parse(source);
}
}
}
#Maksym's way has helped me as well. I had to adjust it a little bit for Spring Framework 5 and Spring Boot 2:
#Bean
#Primary
public MappingMongoConverter mongoConverter(
#Autowired MongoMappingContext mongoMappingContext,
#Autowired MongoDbFactory mainMongoFactory,
#Autowired MongoCustomConversions conversions
) {
DbRefResolver dbRefResolver = new DefaultDbRefResolver(mainMongoFactory);
MappingMongoConverter mongoConverter = new MappingMongoConverter(dbRefResolver, mongoMappingContext);
mongoConverter.setMapKeyDotReplacement("#");
mongoConverter.afterPropertiesSet();
mongoConverter.setCustomConversions(conversions);
return mongoConverter;
}
#Bean
public MongoMappingContext mongoMappingContext() {
MongoMappingContext context = new MongoMappingContext();
context.setSimpleTypeHolder(new SimpleTypeHolder(new HashSet<>(Arrays.asList(
DateTime.class,
LocalDateTime.class
)), MongoSimpleTypes.HOLDER));
return context;
}
#Bean
public MongoCustomConversions customConversions() {
List<Converter<?, ?>> converterList = new ArrayList<Converter<?, ?>>();
converterList.add(new MongoLocalDateTimeFromStringConverter());
converterList.add(new MongoDateTimeFromStringConverter());
return new MongoCustomConversions(converterList);
}
private static final class MongoLocalDateTimeFromStringConverter implements Converter<String, LocalDateTime> {
#Override
public LocalDateTime convert(String source) {
return source == null ? null : LocalDateTime.parse(source);
}
}
private static final class MongoDateTimeFromStringConverter implements Converter<String, DateTime> {
#Override
public DateTime convert(String source) {
return source == null ? null : DateTime.parse(source);
}
}
Ref - https://github.com/lordofthejars/nosql-unit#dataset-format
If you want to use ISODate function or any other javascript function
you should see how MongoDB Java Driver deals with it. For example in
case of ISODate:
In your json file use $date for conversion
"bornAt":{ "$date" : "2011-01-05T10:09:15.210Z"}

Categories