Mapping Date Strings to Dates including Time with Jackson in Spring - spring

I'm using Spring Boot 1.4 and the following works. I have this #Bean defined:
#Bean
public MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter() {
ObjectMapper mapper = new ObjectMapper();
mapper.setDateFormat(new SimpleDateFormat("yyyy-MM-dd"));
mapper.registerModule(new JodaModule());
return new MappingJackson2HttpMessageConverter(mapper);
}
And I have this DTO defined:
public class ReportRequest implements Serializable {
private LocalDate startDate;
private LocalDate endDate;
// additional fields and getters/setters omitted
}
I'm submitting this data into a controller with #RequestBody ReportRequest with the following json in the body of the request:
{
"startDate": "2016-09-01",
"endDate": "2016-09-12"
}
Works great. However, I need to also include the time. So I changed everything to look like this:
mapper.setDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
private LocalDateTime startDate;
private LocalDateTime endDate;
{
"startDate": "2016-09-01 02:00:00",
"endDate": "2016-09-12 23:59:59"
}
This does not work. I'm getting:
Could not read document: Invalid format: \"2016-09-01 02:00:00\" is malformed at \" 02:00:00\" (through reference chain: com.hightouchinc.dto.ReportRequest[\"startDate\"]); nested exception is com.fasterxml.jackson.databind.JsonMappingException: Invalid format: \"2016-09-01 02:00:00\" is malformed at \" 02:00:00\" (through reference chain: com.hightouchinc.dto.ReportRequest[\"startDate\"])
Update: I modified the following:
mapper.setDateFormat(new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss"));
And can send "2016-09-01T02:00:00" and it works. But removing the T from both continues to break.

It doesn't appear that LocalDateTimeDeserializer respects the value passed to setDateFormat(), as can be seen in JodaModule:
addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer());
What you can do is override the default deserializer in the module before registering it:
JodaModule jodaModule = new JodaModule();
JacksonJodaDateFormat format = new JacksonJodaDateFormat("yyyy-MM-dd HH:mm:ss");
jodaModule.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(format)));
mapper.registerModule(jodaModule);
and that should use the correct pattern to deserialize your LocalDateTime instances.

Related

#Pattern annotation is not validating the format

I have a projectDTO which accepts a project name and start date. See below
#Data
#AllArgsConstructor
#NoArgsConstructor
public class ProjectDTO {
#NotBlank(message = "project name is required")
private String projectName;
#NotNull(message = "project start date is required")
#Pattern(regexp = "^\\d{4}\\-(0[1-9]|1[012])\\-(0[1-9]|[12][0-9]|3[01])$", message = "date format should be yyyy-MM-dd")
private LocalDate startDate;
}
Below is my ExceptionHandler class
#RestControllerAdvice
public class ApplicationExceptionHandler {
#ExceptionHandler(MethodArgumentNotValidException.class)
private ResponseEntity<Object> handleMethodArgumentNotValid(MethodArgumentNotValidException ex) {
List<String> errors = ex.getBindingResult()
.getFieldErrors()
.stream()
.map(DefaultMessageSourceResolvable::getDefaultMessage)
.collect(Collectors.toList());
ApiException apiException = ApiException.builder()
.errors(errors)
.httpStatus(HttpStatus.BAD_REQUEST)
.timestamp(ZonedDateTime.now(ZoneId.of("Z")))
.build();
return new ResponseEntity<>(apiException, HttpStatus.BAD_REQUEST);
}
}
If I add a null value for the project name it validates as expected and give the custom error response as below.
{
"errors": [
"project name is required"
],
"httpStatus": "BAD_REQUEST",
"timestamp": "2023-02-17T05:06:08.9362836Z"
}
But if I provide a wrong format for the start date (e.g. - 2023/12-17) the validation is not working. Getting below error
{
"timestamp": "2023-02-17T05:21:07.004+00:00",
"status": 400,
"error": "Bad Request",
"path": "/api/projects"
}
In the console
Resolved [org.springframework.http.converter.HttpMessageNotReadableException: JSON parse error: Cannot deserialize value of type `java.time.LocalDate` from String "2023/02-17": Failed to deserialize java.time.LocalDate: (java.time.format.DateTimeParseException) Text '2023/02-17' could not be parsed at index 4; nested exception is com.fasterxml.jackson.databind.exc.InvalidFormatException: Cannot deserialize value of type `java.time.LocalDate` from String "2023/02-17": Failed to deserialize java.time.LocalDate: (java.time.format.DateTimeParseException) Text '2023/02-17' could not be parsed at index 4<LF> at [Source: (PushbackInputStream); line: 5, column: 25] (through reference chain: com.theravado.donationservice.dto.ProjectRequest["projects"]->java.util.ArrayList[0]->com.theravado.donationservice.dto.ProjectDTO["startDate"])]
Can you give my some input on how to get this date format validation can be fixed for #Pattern so that I can output an informative error message like in project name?
Cheers
Edited
Thanks #times29. I feel like my validation is not applied at all for the start date here. exquisitely meantioning that "date format should be yyyy-MM-dd" rather than JSON parse error: Cannot deserialize value of type java.time.LocalDate from String "2023/02-17"
#Pattern(regexp = "^\\d{4}\\-(0[1-9]|1[012])\\-(0[1-9]|[12][0-9]|3[01])$", message = "date format should be yyyy-MM-dd")
You need a separate #ExceptionHandler for the HttpMessageNotReadableException. Your current #ExceptionHandler(MethodArgumentNotValidException.class) will only handle MethodArgumentNotValidException exceptions, but when a HttpMessageNotReadableException occurrs like in your case, it won't be handled by the handleMethodArgumentNotValid method.
#ExceptionHandler(HttpMessageNotReadableException.class)
private ResponseEntity<Object> handleHttpMessageNotReadableException(HttpMessageNotReadableException ex) {
List<String> errors = ...; // Extract info from the exception
ApiException apiException = ApiException.builder()
.errors(errors)
.httpStatus(HttpStatus.BAD_REQUEST)
.timestamp(ZonedDateTime.now(ZoneId.of("Z")))
.build();
return new ResponseEntity<>(apiException, HttpStatus.BAD_REQUEST);
}
Actually the issue was #Pattern cannot be used for LocalDate. When I changed the type to String in my projectDTO this started working
#NotNull(message = "project start date is required")
#Pattern(regexp = "^\\d{4}\\-(0[1-9]|1[012])\\-(0[1-9]|[12][0-9]|3[01])$", message = "date format should be yyyy-MM-dd")
private String startDate;

Spring Jackson validate input field is a valid JSON

Have a pojo which is suppled via a rest call. One of the fields of the pojo is a JSON. The JSON can be any valid JSON. It is a Run Time value supplied by a couple of different inputs. The goal is to throw an exception if the JSON field is not a valid JSON
Have tried:
In POJO
#JsonFormat(shape = JsonFormat.Shape.OBJECT)
private JsonNode json;
Have also tried using
#JsonRawValue
However when I supply anything it picks it up as a node of some type (TestNode, IntNode) etc.
anyway to do the validation without introduction of extra code? If not how to check at the controller when the object comes in?
One way to do this is use org.springframework.validation.Validator
You can annotate your controller with
#PostMapping("/abc")
public Test test((#Validated(YourValidator.class)
#RequestBody YourPojo pojo)) {
return test;
}
Now you can write your own validation
public class YourValidator implements org.springframework.validation.Validator {
#Override
public void validate(Object target, Errors errors) {
YourPojo pojo = (YourPojo) target;
boolean validJson =false;
try {
final ObjectMapper mapper = new ObjectMapper(); //jackson library
String jsonInString = pojo.getJsonField() ;
mapper.readTree(jsonInString);
validJson= true;
} catch (IOException e) {
validJson= false;
}
errors.rejectValue("jsonField", "notValid");
}
}

Spring cloud open Feign Not ignoring null Values while Encoding

I am working on a Spring boot application. We are using Spring cloud open Feign for making rest calls. We are using the default GsonEncoder(), but for some reason gson is not excluding the null properties while encoding the payload.
Config:
return Feign.builder()
.options(ApiOptions())
.encoder(new GsonEncoder())
.decoder(new GsonDecoder())
.target(ApiClient.class, "URL");
Client:
#FunctionalInterface
#FeignClient(
value = "apiTest",
url = "urlHere"
)
public interface ApiClient {
#PostMapping("path/to/service")
AiResponse getDetails(ApiRequest apiRequest);
}
ApiRequest.java:
public class ApiRequest {
private String userName;
private String userId;
private String password;
//#setters and getters
}
But while making the request, the Request Body is :
{
"userName" : "test,
"userId" : null,
"password": "password"
}
My Understanding is that Gson should automatically remove the null while serializing the Request Body. But i can see null properties exist in request.
I even tried with Custom Encoders (Jackson from below):
https://github.com/OpenFeign/feign/blob/master/jackson/src/main/java/feign/jackson/JacksonEncoder.java
As per below, it should not include null while serialzing the requestBody, but still i can see null values being passed in request.
https://github.com/OpenFeign/feign/blob/master/jackson/src/main/java/feign/jackson/JacksonEncoder.java#L39
Below are the dependencies:
Spring clou version : 2020.0.2
org.springframework.cloud:spring-cloud-starter-openfeign
io.github.openfeign:feign-gson:9.5.1
Any suggestions would be really helpful. Thanks in Advance.!
You need to provide OpenFeign with custom Encoder to let it skip nulls.
In my case this beans helps me.
#Configuration
public class ExternalApiConfiguration {
#Bean
public Feign.Builder feignBuilder() {
return Feign.builder()
.contract(new SpringMvcContract());
}
#Bean("feignObjectMapper")
public ObjectMapper feignObjectMapper() {
final ObjectMapper objectMapper = new ObjectMapper();
objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
return objectMapper;
}
#Bean
public Encoder feignEncoder(#Qualifier("feignObjectMapper") ObjectMapper objectMapper) {
HttpMessageConverter jacksonConverter = new MappingJackson2HttpMessageConverter(objectMapper);
ObjectFactory<HttpMessageConverters> objectFactory = () -> new HttpMessageConverters(jacksonConverter);
return new SpringEncoder(objectFactory);
}
And use this configuration if FeignClient
#FeignClient(configuration = ExternalApiConfiguration.class)

Get items from a single payload using a Flux

I have a method which queries a remote service. This service returns a single payload which holds many items.
How do I get those items out using a Flux and a flatMapMany?
At the moment my "fetch from service" method looks like:
public Flux<Stack> listAll() {
return this.webClient
.get()
.uri("/projects")
.accept(MediaType.APPLICATION_JSON)
.exchange()
.flatMapMany(response -> response.bodyToFlux(Stack.class));
}
a Stack is just a POJO which looks like:
public class Stack {
String id;
String name;
String title;
String created;
}
Nothing special here, but I think my deserializer is wrong:
protected Stack deserializeObject(JsonParser jsonParser, DeserializationContext deserializationContext, ObjectCodec objectCodec, JsonNode jsonNode) throws IOException {
log.info("JsonNode {}", jsonNode);
return Stack.builder()
.id(nullSafeValue(jsonNode.findValue("id"), String.class))
.name(nullSafeValue(jsonNode.findValue("name"), String.class))
.title(nullSafeValue(jsonNode.findValue("title"), String.class))
.created(nullSafeValue(jsonNode.findValue("created"), String.class))
.build();
}
What I've noticed happening is the first object is serialized correctly, but then it seems to get serialized again, rather than the next object in the payload.
The payload coming in follows standard JSON API spec and looks like:
{
"data":[
{
"type":"stacks",
"id":"1",
"attributes":{
"name":"name_1",
"title":"title_1",
"created":"2017-03-31 12:27:59",
"created_unix":1490916479
}
},
{
"type":"stacks",
"id":"2",
"attributes":{
"name":"name_2",
"title":"title_2",
"created":"2017-03-31 12:28:00",
"created_unix":1490916480
}
},
{
"type":"stacks",
"id":"3",
"attributes":{
"name":"name_3",
"title":"title_3",
"created":"2017-03-31 12:28:01",
"created_unix":1490916481
}
}
]
}
I've based this pattern on the spring-reactive-university
Any help as to where I've gone wrong would be awesome;
Cheers!
I think I solved it, still using a Flux.
public Flux<Stack> listAllStacks() {
return this.webClient
.get()
.uri("/naut/projects")
.accept(MediaType.APPLICATION_JSON)
.exchange()
.flatMap(response -> response.toEntity(String.class))
.flatMapMany(this::transformPayloadToStack);
}
Converts the incoming payload to a String where I can then parse it using a jsonapi library
private Flux<Stack> transformPayloadToStack(ResponseEntity<String> payload) {
ObjectMapper objectMapper = new ObjectMapper();
ResourceConverter resourceConverter = new ResourceConverter(objectMapper, Stack.class);
List<Stack> stackList = resourceConverter.readDocumentCollection(payload.getBody().getBytes(), Stack.class).get();
return Flux.fromIterable(stackList);
}
Which returns a Flux. Thanks to the library, I don't need to create a bunch of domains either, I can still work with my simple Stack POJO
#Data
#NoArgsConstructor
#AllArgsConstructor
#JsonIgnoreProperties(ignoreUnknown = true)
#Type("stacks")
public class Stack {
#com.github.jasminb.jsonapi.annotations.Id
String id;
String name;
String title;
String created;
}
And this in turn is called from the controller
#GetMapping("/stacks")
#ResponseBody
public Flux<Stack> findAll() {
return this.stackService.listAllStacks();
}
I've not tested if this is blocking or not yet, but seems to work okay.
You json doesn't exactly match with your model class i.e. Stack. Together with Stack create another class like this
public class Data {
List<Stack> data;
// Getters and Setters....
}
Now in your webclient you can do like this
Mono<Data> listMono = webClient
.get()
.uri("/product/projects")
.exchange()
.flatMap(clientResponse -> clientResponse.bodyToMono(Data.class));
Now if you do listMono.block() you will get Data object which will have all Stack objects.

Using multiple template resolvers with Spring 3.2 and Thymeleaf 2.1.3 for emails

I have problem defining a ClassLoaderTemplateResolver for emails and one ServletContextTemplateResolver for web views. I getting the following error when trying to send emails:
HTTP Status 500 - Request processing failed; nested exception is
org.thymeleaf.exceptions.TemplateProcessingException: Resource resolution by ServletContext with
org.thymeleaf.resourceresolver.ServletContextResourceResolver can only be performed when context
implements org.thymeleaf.context.IWebContext [current context: org.thymeleaf.context.Context]
My WebMvcConfig looks like this:
private static final String VIEWS_PATH = "/WEB-INF/views/";
private static final String MAIL_PATH = "mail/";
#Bean
public ServletContextTemplateResolver templateResolver() {
final ServletContextTemplateResolver resolver = new ServletContextTemplateResolver();
resolver.setPrefix(VIEWS_PATH);
resolver.setSuffix(".html");
resolver.setTemplateMode("HTML5");
resolver.setCharacterEncoding("UTF-8");
resolver.setOrder(2);
resolver.setCacheable(false);
return resolver;
}
#Bean
public ClassLoaderTemplateResolver emailTemplateResolver() {
final ClassLoaderTemplateResolver resolver = new ClassLoaderTemplateResolver();
resolver.setPrefix(MAIL_PATH);
resolver.setSuffix(".html");
resolver.setTemplateMode("HTML5");
resolver.setCharacterEncoding("UTF-8");
resolver.setOrder(1);
return resolver;
}
#Bean
public SpringTemplateEngine templateEngine() {
final SpringTemplateEngine engine = new SpringTemplateEngine();
final Set<TemplateResolver> templateResolvers = new HashSet<TemplateResolver>();
templateResolvers.add(templateResolver());
templateResolvers.add(emailTemplateResolver());
engine.setTemplateResolvers(templateResolvers);
engine.addDialect(new SpringSocialDialect());
engine.addDialect(new SpringSecurityDialect());
return engine;
}
And my EmailService like this:
#Service
public class EmailService {
#Autowired
private JavaMailSender mailSender;
#Autowired
private TemplateEngine templateEngine;
/*
* Send HTML mail with inline image
*/
public void sendEmailToBookSeller(
final ContactBookSellerForm form,
final Locale locale) throws MessagingException {
boolean multipart = true;
boolean isHtml = true;
// Prepare the evaluation context
final Context ctx = new Context(locale);
ctx.setVariable("message", form.getMessage());
ctx.setVariable("bookTitle", form.getBookTitle());
ctx.setVariable("email", form.getToEmail());
ctx.setVariable("logo", "logo");
ctx.setVariable("logoOnlyText", "logoOnlyText");
// Prepare message
final MimeMessage mimeMessage = mailSender.createMimeMessage();
final MimeMessageHelper message = new MimeMessageHelper(mimeMessage, multipart, "UTF-8");
message.setSubject("Regarding your book on Mimswell - " + form.getBookTitle());
message.setFrom(form.getFromEmail());
message.setTo(form.getToEmail());
// Create the HTML body using Thymeleaf
final String htmlContent = templateEngine.process("email-buy-book.html", ctx);
message.setText(htmlContent, isHtml);
message.addInline("logo", new ClassPathResource("WEB-INF/views/mail/logo130130red.png"), "image/png");
message.addInline("logoOnlyText", new ClassPathResource("WEB-INF/views/mail/logo_only_text.png"), "image/png");
// Send mail
this.mailSender.send(mimeMessage);
}
}
The error occours on the following line:
final String htmlContent = templateEngine.process("email-buy-book.html", ctx);
Where it is using ServletContextResourceResolver instead of my other resolver. I want it to use ClassLoaderTemplateResolver since it can handle plain Context objects instead of having to use WebContext. However, I could try to use a WebContext instead since it implements the IWebContext and only use one resolver. But then I need a HttpServletRequest, HttpServletResponse and a ServletContext as parameters which seems to messy.
My structure :
Any idea whats wrong in my code?
I gave up this and went for the WebContext approach instead, even though i'm stuck needing the request, response and servletcontext every time sending something. This is how I did it:
1. Get the servlet context:
#Autowired
ServletContext servletContext;
2. Get the request and response as parameters to the sendmail method:
HttpServletRequest request,
HttpServletResponse response
3. Create the WebContext instead:
final WebContext ctx = new WebContext(request, response, servletContext, locale);
It worked from now on.
Since you (correctly) set the ClassLoaderTemplateResolver to have priority over the ServletContextTemplateResolver, Thymeleaf tries to use the correct order but fails to resolve the view with former and then tries latter.
I believe that the problem is with the prefix and suffix parameters you set combined with the view name you pass to templateEngine.process method. Thymeleaf will construct your view name by concatenating suffix + viewname + suffix resulting to "mail/email-buy-book.html.html".
Try to pass only "email-buy-book" and see if it solves the problem.
Since you're using the ClassLoaderTemplateResolver, Spring is going to use the prefix and append it to WEB-INF/classes. So the thing to check is whether Maven (or whatever build tool you're using) copied the html file to WEB-INF/classes/mail/email-buy-book.html. If it didn't, try copying it manually and give it a go. Looking at your screenshot, I don't see the "mail" folder under "classes" so this could be the issue.
Also, only pass "email-buy-book" and leave out the extension as #grid mentioned.
final String htmlContent = templateEngine.process("email-buy-book", ctx);
I have it working with XML config and not Java config, but I don't see why that should matter for you.

Resources