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);
Related
i pass an object ObjectDTO as POST that have some org.joda.time.LocalDateTime parameter.
public class ObjectDTO dto {
...
private LocalDateTime dataesecuzione;
private LocalDateTime oraesecuzione;
public LocalDateTime getDataesecuzione() {
return dataesecuzione;
}
public void setDataesecuzione(LocalDateTime dataesecuzione) {
this.dataesecuzione = dataesecuzione;
}
public LocalDateTime getOraesecuzione() {
return oraesecuzione;
}
public void setOraesecuzione(LocalDateTime oraesecuzione) {
this.oraesecuzione = oraesecuzione;
}
...
}
#PutMapping("/api/get/{id}")
public ResponseEntity<Integer> update(#PathVariable("id") int id, #RequestBody ObjectDTO dto) {
...
}
The problem is that all the LocalDateTime parameter are reset to current date/time.
How can I solve the problem?
Thanks
To let Jackson deserialize the Joda Date Time, register JodaModule in the object mapper bean
#Bean
ObjectMapper objectMapper() {
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.registerModule(new JodaModule());
return objectMapper;
}
I'm writing REST service.
I want to get all records by date that I pass in #Path variable.
How Can I do that?
What I tried to do:
Model Class:
#Entity
#Table(name = "test")
public class Test {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private int id;
private String name;
#JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd")
private LocalDate beginDate;
#JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd")
private LocalDate endDate;
private String activity;
}
Repository:
#Repository
public interface TestRepository extends JpaRepository<Test, Integer> {
List<Test> findAllByName(String name);
List<Test> findAllByBeginDate(LocalDate date);
}
Service:
#Service
public class TestService {
#Autowired
private final TestRepository testRepository;
public TestService(TestRepository testRepository) {
this.testRepository = testRepository;
}
public List<Test> getAllTestsByBeginDate(LocalDate date) {
return testRepository.findAllByBeginDate(date);
}
}
Controller:
#RestController
#RequestMapping("/api/v1/")
public class TestController {
#GetMapping("test/all/{date}")
public List<Test> getAllTestsByBeginDate(#PathVariable ("date") #DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate date) {
return testService.getAllTestsByBeginDate(date);
}
}
When I pass date like this, I get errors:
This should work
#RestController
#RequestMapping("/api/v1/")
public class TestController {
#GetMapping("test/all/{date}")
public List<Test> getAllTestsByBeginDate(#PathVariable #DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate date) {
return testService.getAllTestsByBeginDate(date);
}
}
or this link will help
You can global configure any datetime format in application properties. Like:
spring.mvc.format.date=yyyy-MM-dd
spring.mvc.format.date-time=yyyy-MM-dd HH:mm:ss
spring.mvc.format.time=HH:mm:ss
I have the following DTO which has 2 fields that must be converted to OffsetDateTime:
#Data
public class AppointmentDTO {
private String id;
#NotNull
private String startTime;
#NotNull
private String endTime;
#NotNull
private String timeZoneStart;
#NotNull
private String timeZoneEnd;
// other fields
}
to
#Data
#Document
#NoArgsConstructor
#AllArgsConstructor
public class Appointment {
#Id
private String id;
private String timeZoneStart;
private String timeZoneEnd;
private OffsetDateTime startTime;
private OffsetDateTime endTime;
private OffsetDateTime createdTime;
// other fields
}
In order to convert, I need the DTO's timeZone fields plus a DateTimeFormatter. So, my attempt is this:
#Component
#Mapper(componentModel = "spring")
public interface IAppointmentMapper {
#Mapping(target = "createdTime", ignore = true)
Appointment convertAppointmentDTOToAppointment(AppointmentDTO dto, #Context OffsetDateTimeMapper offsetDateTimeMapper);
}
public class OffsetDateTimeMapper {
private String startTime;
private String endTime;
private String timeZoneStart;
private String timeZoneEnd;
private final DateTimeFormatter dateTimeFormatter;
public OffsetDateTimeMapper(DateTimeFormatter dateTimeFormatter) {
this.dateTimeFormatter = dateTimeFormatter;
}
#BeforeMapping
public void beforeStartTimeMapping(AppointmentDTO dto) {
this.startTime = dto.getStartTime();
this.timeZoneStart = dto.getTimeZoneStart();
}
#BeforeMapping
public void beforeEndTimeMapping(AppointmentDTO dto) {
this.endTime = dto.getEndTime();
this.timeZoneEnd = dto.getTimeZoneEnd();
}
#AfterMapping
public void startTimeMap(#MappingTarget Appointment appointment) {
LocalDateTime localDateTime = LocalDateTime.parse(startTime, dateTimeFormatter);
ZonedDateTime zonedDateTime = localDateTime.atZone(ZoneId.of(timeZoneStart));
appointment.setStartTime(zonedDateTime.toOffsetDateTime());
}
#AfterMapping
public void endTimeMap(#MappingTarget Appointment appointment) {
LocalDateTime localDateTime = LocalDateTime.parse(endTime, dateTimeFormatter);
ZonedDateTime zonedDateTime = localDateTime.atZone(ZoneId.of(timeZoneEnd));
appointment.setEndTime(zonedDateTime.toOffsetDateTime());
}
}
When I build, however, I get the following error: Error:(22,17) java: Can't map property "java.lang.String startTime" to "java.time.OffsetDateTime startTime". Consider to declare/implement a mapping method: "java.time.OffsetDateTime map(java.lang.String value)". If I put a default map() method on the interface, I don't get the error, but I don't have the proper #Context required.
/*default OffsetDateTime map(String value) {
LocalDateTime localDateTime = LocalDateTime.parse(value);
ZonedDateTime zonedDateTime = localDateTime.atZone(ZoneOffset.UTC);
return zonedDateTime.toOffsetDateTime();
}*/
Your approach is interesting. In order to work properly you would have to explicitly ignore the properties that you don't want MapStruct to automatically map.
In this case add:
#Mapping(target = "startTime", ignore = true)
#Mapping(target = "endTime", ignore = true)
However, in your case I would try to use Mapping method selection based on qualifiers and use the source parameters as source.
So something like:
#Component
#Mapper(componentModel = "spring")
public interface IAppointmentMapper {
#Mapping(target = "createdTime", ignore = true)
#Mapping(target = "startTime", source = "dto", qualifiedByName = "startTime")
#Mapping(target = "endTime", source = "dto", qualifiedByName = "endTime")
Appointment convertAppointmentDTOToAppointment(AppointmentDTO dto, #Context DateTimeFormatter dateTimeFormatter);
#Named("startTime")
default OffsetDateTime mapStartTime(AppointmentDTO dto, #Context DateTimeFormatter dateTimeFormatter) {
LocalDateTime localDateTime = LocalDateTime.parse(dto.getStartTime(), dateTimeFormatter);
ZonedDateTime zonedDateTime = localDateTime.atZone(ZoneId.of(dto. getTimeZoneStart()));
return zonedDateTime.toOffsetDateTime()
}
#Named("endTime")
default OffsetDateTime mapEndTime(AppointmentDTO dto, #Context DateTimeFormatter dateTimeFormatter) {
LocalDateTime localDateTime = LocalDateTime.parse(dto.getEndTime(), dateTimeFormatter);
ZonedDateTime zonedDateTime = localDateTime.atZone(ZoneId.of(dto. getTimeZoneEnd()));
return zonedDateTime.toOffsetDateTime()
}
}
Note: #Named is org.mapstruct.Named.
When I'm trying to test my Api Controller with hardcoded Object everything is fine unitil I try to add LocalDate parameter to Object.
My Test:
#RunWith(SpringRunner.class)
#WebMvcTest(ApiTransitController.class)
public class ApiTransitControllerTest {
#Autowired
private MockMvc mockMvc;
#MockBean
private TestService testService;
#MockBean
private ReportsService reportsService;
#MockBean
private TransitService transitService;
#Test
public void shouldCreateTransit() throws Exception {
Transit transit = new Transit("London", "Paris", 12L,
LocalDate.of(2018,10,12));
ObjectMapper objectMapper = new ObjectMapper();
String transitJsonString = objectMapper.writeValueAsString(transit);
this.mockMvc.perform(post("/api/transit")
.contentType(MediaType.APPLICATION_JSON)
.content(transitJsonString))
.andExpect(status().isCreated());
verify(transitService).addTransit(eq(new Transit("London", "Paris", 12L,
LocalDate.of(2018,10,12))));
}
}
Model:
#Entity
public class Transit {
#Column(name = "id")
#Id
#GeneratedValue
private Long id;
private String sourceAdress;
private String destinationAdress;
private Long price;
#DateTimeFormat(iso = DateTimeFormat.ISO.DATE)
private LocalDate date;
#JsonSerialize(using=DistanceSerializer.class)
private Long distance;
public Transit(String sourceAdress, String destinationAdress, Long price, LocalDate date) {
this.sourceAdress = sourceAdress;
this.destinationAdress = destinationAdress;
this.price = price;
this.date = date;
}
//getters and setters, equals and hashCode and toString
Api Controller:
#PostMapping("/api/transit")
#ResponseStatus(HttpStatus.CREATED)
public void createTransit(#RequestBody Transit transit){
LOG.info("Saving transit={}", transit);
transitService.addTransit(transit);
}
I tried adding DateTimeFormmater and few other ways, but still I cant pass the test. Thank you for your time.
Try changing this line
verify(transitService).addTransit(eq(new Transit("London", "Paris", 12L,
LocalDate.of(2018,10,12))));
to this:
verify(transitService).addTransit(eq(transit));
The two objects aren't equal, also you don't need to create a new object you can use already created one.
I added JsonSerializer to date of the Model:
Model:
#DateTimeFormat(iso = DateTimeFormat.ISO.DATE)
#JsonSerialize(using=DateSerializerNumberTwo.class)
private LocalDate date;
Serializer:
public class DateSerializerNumberTwo extends StdSerializer<LocalDate> {
private static DateTimeFormatter formatter =
DateTimeFormatter.ofPattern("yyyy-MM-dd");
public DateSerializerNumberTwo(){
this(null);
}
protected DateSerializerNumberTwo(Class<LocalDate> t){
super(t);
}
#Override
public void serialize(LocalDate value, JsonGenerator gen, SerializerProvider provider) throws IOException {
gen.writeString(formatter.format(value));
}
}
And the test passes without any changes in the test code. I think it's beacuse the Json default response of date is "yyyy, mm, dd", not like Local date (yyyy-mm-dd)".
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.