Accept Arabic date as query parameter in Spring Boot - java

I want to accept Arabic date in the query parameter
ex:
٢٠٢١-٠٧-٢٠ the English version of this date is 2021-07-20
I have found this soltion
DecimalStyle defaultDecimalStyle
= DateTimeFormatter.ISO_LOCAL_DATE.getDecimalStyle();
DateTimeFormatter arabicDateFormatter = DateTimeFormatter.ISO_LOCAL_DATE
.withDecimalStyle(defaultDecimalStyle.withZeroDigit('\u0660'));
String encodedArabicDateStr = "%D9%A2%D9%A0%D9%A1%D9%A9-%D9%A0%D9%A4-%D9%A1%D9%A5";
String arabicDateStr
= URLDecoder.decode(encodedArabicDateStr, StandardCharsets.UTF_8);
LocalDate date = LocalDate.parse(arabicDateStr, arabicDateFormatter);
System.out.println("Parsed date: " + date);
And it works as expected
now, in my Spring Boot application, I handle the LocalDate with #DateTimeFormat with pattern
#DateTimeFormat(pattern = "yyyy-MM-dd") val fromDate: LocalDate
How I can tell the Spring Boot to use a custom method to parse the date?
The original problem is when someone send Arabic date the application it will crash with the below error:
org.springframework.validation.BeanPropertyBindingResult: 1 errors
Field error in object 'dateRange' on field 'fromDate': rejected value [٢٠٢١-٠٧-٢٠];
codes [typeMismatch.dateRange.fromDate,typeMismatch.fromDate,typeMismatch.java.time.LocalDate,typeMismatch];
arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [dateRange.fromDate,fromDate];
arguments [];
default message [fromDate]];
default message [Failed to convert value of type 'java.lang.String[]' to required type 'java.time.LocalDate';
nested exception is org.springframework.core.convert.ConversionFailedException: Failed to convert from type [java.lang.String] to type [#org.springframework.format.annotation.DateTimeFormat java.time.LocalDate] for value '٢٠٢١-٠٧-٢٠'; nested exception is java.lang.IllegalArgumentException: Parse attempt failed for value [٢٠٢١-٠٧-٢٠]]
I have tried:
update request values by filter but it does not work.
FormattingConversionService like this sample.

You can create your own custom deserializer for LocalDate which will contain the format field.
Creating of custom deserializer for LocalDate
public class CustomDateDeserializer extends JsonDeserializer<LocalDate> {
#Override
public LocalDate deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException, JsonProcessingException {
DecimalStyle defaultDecimalStyle = DateTimeFormatter.ISO_LOCAL_DATE.getDecimalStyle();
DateTimeFormatter arabicDateFormatter = DateTimeFormatter.ISO_LOCAL_DATE.withDecimalStyle(defaultDecimalStyle.withZeroDigit('\u0660'));
String arabicDateStr = URLDecoder.decode(jsonParser.getText(), StandardCharsets.UTF_8);
return LocalDate.parse(arabicDateStr, arabicDateFormatter);
}
}
Using the custom deserializer for LocalDate
#JsonSerialize(using = CustomDateDeserializer.class)
private LocalDate fromDate;

Since you need to parse the date from query parameter, you need to implement the logic as a Converter.
#Component
public class ArabicDateConverter implements Converter<String, LocalDate> {
private final DateTimeFormatter formatter = DateTimeFormatter.ISO_LOCAL_DATE
.withDecimalStyle(DateTimeFormatter.ISO_LOCAL_DATE.getDecimalStyle().withZeroDigit('\u0660'));
#Override
public LocalDate convert(String source) {
return LocalDate.parse(source, formatter);
}
}
Note #Component, it's used to register the converter.
Keep in mind that this converts only arabic dates, it will fail for other formats. If you need to parse others, you may need to keep a list of possible formats to iterate over until something matches.

Related

ZonedDateTime returning as Epoch time instead of standard String in Spring App

I am trying fetch some DateTime values stored in a local MySQL database in my Spring App. These dates are parsed into a ZoneDateTime and are then sent to a Client Front End as a json. I have an Object Mapper that specifies this conversion.
#Bean
public ObjectMapper objectMapper() {
JavaTimeModule javaTimeModule = new JavaTimeModule();
javaTimeModule.addSerializer(ZonedDateTime.class,
new ZonedDateTimeSerializer(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSXXX")));
return Jackson2ObjectMapperBuilder.json().featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) // ISODate
.modules(javaTimeModule).build();
}
However, on the front-end, the values I receive are in Epoch time instead of the format specified in the ObjectMapper. I have checked the value parsed into ZoneDateTime and it is parsed correctly. My guess is that there is some fault in the process mapping the ZoneDateTime object into the json String value.
What could be the fix of this?
Here is how to do it simply and efficiently:
#JsonFormat(shape= JsonFormat.Shape.STRING, pattern="EEE MMM dd HH:mm:ss Z yyyy")
#JsonProperty("created_at")
ZonedDateTime created_at;
This is a quote from a question: [Jackson deserialize date from Twitter to `ZonedDateTime`
Also, I don't think that you need to add a special serializer for this. It works for me without this definition just fine.
https://docs.spring.io/spring/docs/4.3.0.RC1_to_4.3.0.RC2/Spring%20Framework%204.3.0.RC2/org/springframework/http/converter/json/Jackson2ObjectMapperBuilder.html#timeZone-java.lang.String-
Docs says there is a timezone(String) method to override default timezone.
I suppose you could pass the timezone into this method while building the ObjectMapper
#Bean
public Jackson2ObjectMapperBuilderCustomizer init() {
return new Jackson2ObjectMapperBuilderCustomizer() {
#Override
public void customize(Jackson2ObjectMapperBuilder builder) {
builder.timeZone(TimeZone.getDefault());
}
};
}
You could use above code to override default timezone.
return Jackson2ObjectMapperBuilder.json().featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) // ISODate
.modules(javaTimeModule).timeZone(TimeZone.getDefault()).build();
You could try this.

custom deserializing a date with format

["last_modified"])] with root cause
java.time.format.DateTimeParseException: Text '2018-06-06T13:19:53+00:00' could not be parsed, unparsed text found at index 19
The inbound format is 2018-06-06T13:19:53+00:00
It's a weird format.
I have tried the following:
public class XYZ {
#DateTimeFormat(pattern = "yyyy-MM-ddTHH:mm:ss+00:00", iso = ISO.DATE_TIME)
private LocalDateTime lastModified;
}
There is nothing stopping you from creating your own deserializer. A very naive example could be the following:
public class LocalDateTimeDeserializer extends JsonDeserializer<LocalDateTime> {
private static final String PATTERN = "yyyy-MM-dd'T'HH:mm:ss+00:00";
private final DateTimeFormatter formatter;
public LocalDateTimeDeserializer() {
this.formatter = DateTimeFormatter.ofPattern(PATTERN);
}
#Override
public LocalDateTime deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
return LocalDateTime.parse(p.getText(), formatter);
}
}
The only thing you need to notice is that you'll need to escape the 'T' by adding single quote around it.
With the deserializer in place you can simply annotate the field like so:
#JsonDeserialize(using = LocalDateTimeDeserializer.class)
private LocalDateTime dateTime;
The inbound format is 2018-06-06T13:19:53+00:00
It's a weird format.
That's the ISO 8601 format, which is endorsed by the RFC 3339 and by the xkcd 1179:
The following should work as expected when receiving the values as query parameters:
#DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME)
private LocalDate dateTime;
As 2018-06-06T13:19:53+00:00 represents a date and time with an offset from UTC, you'd better use OffsetDateTime rather than LocalDateTime:
#DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME)
private OffsetDateTime dateTime;
Just ensure that + is encoded as %2B.
With Jackson, you could add the jackson-datatype-jsr310 dependency to your application. This module will provide you with serializers and deserializers for java.time types.
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
<version>${jackson.version}</version>
</dependency>
And then register the JavaTimeModule module in your ObjectMapper:
ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(new JavaTimeModule());
mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
Jackson will handle the serialization and deserialization for you.
If you are, for some reason, not interested in the offset from UTC and want to keep using LocalDateTime, you could extend the LocalDateTimeDeserializer provided by Jackson and use a custom DateTimeFormatter:
public class CustomLocalDateTimeDeserializer extends LocalDateTimeDeserializer {
public CustomLocalDateTimeDeserializer() {
super(DateTimeFormatter.ISO_OFFSET_DATE_TIME);
}
}
Then annotate the LocalDateTime field as shown below:
#JsonDeserialize(using = CustomLocalDateTimeDeserializer.class)
private LocalDateTime dateTime;
The inbound format is 2018-06-06T13:19:53+00:00
If you're able to set the Date Format on your entire ObjectMapper, you could do the following:
DateFormat df = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssXXX");
objectMapper.setDateFormat(df);
This is part of the examples from the SimpleDateFormat Javadocs

Spring: create LocalDate or LocalDateTime from #Value parameter

In a spring project, I'd like to create a LocalDate from an #Autowired constructor parameter whose value is in a .properties file. Two things I'd like to do:
1. If the property file contains the property my.date, the parameter should be created by parsing the property value
When the property is set, and when I use the following:
#DateTimeFormat(pattern = "yyyy-MM-dd") #Value("${my.date}") LocalDate myDate,
...
I get this error:
java.lang.IllegalStateException: Cannot convert value of type 'java.lang.String' to required type 'java.time.LocalDate': no matching editors or conversion strategy found
I have also used the iso = ... to use an ISO date with the same result.
2. If the property is not in the properties file, the parameter should be created using LocalDate.now()
I tried using a default value as such:
#Value("${my.date:#{T(java.time.LocalDate).now()}}") LocalDate myDate,
...
But I get the same error.
Forgive my ignorance with Spring, but how can I achieve the two objectives here?
I know two ways. One is generic for any object - to use #Value annotation on custom setter
#Component
public class Example {
private LocalDate localDate;
#Value("${property.name}")
private void setLocalDate(String localDateStr) {
if (localDateStr != null && !localDateStr.isEmpty()) {
localDate = LocalDate.parse(localDateStr);
}
}
}
The second is for LocalDate/LocalDateTime
public class Example {
#Value("#{T(java.time.LocalDate).parse('${property.name}')}")
private LocalDate localDate;
}
Sample property:
property.name=2018-06-20
Spring Boot 2.5, works perfect:
application.yaml
my.date: 2021-08-14
my.time: "11:00"
#Service
public class TestService {
#Value("${my.date}")
#DateTimeFormat(pattern = "yyyy-MM-dd")
LocalDate myDate;
#Value("${my.time}")
#DateTimeFormat(iso = DateTimeFormat.ISO.TIME)
LocalTime myTime;
}
If you want to specify the date format as well then use following on the field:
#Value("#{T(java.time.LocalDate).parse('${date.from.properties.file}', T(java.time.format.DateTimeFormatter).ofPattern('${date.format.from.properties.file}'))}")
Try to add this into your properties file:
spring.jackson.date-format=com.fasterxml.jackson.databind.util.ISO8601DateFormat
spring.jackson.time-zone=UTC
and remove #DateTimeFormat annotation
Concerning LocalDate.now() initialization. Try to use field injection this way:
#Value("${my.date}") LocalDate myDate = LocalDate.now();
As mentioned in other answer by Pavel there are two ways.
I am providing similar two ways with modification to handle 2nd Point by OP.
If the property is not in the properties file, the parameter should be
created using LocalDate.now()
#Component
public class Example {
private LocalDate localDate;
#Value("${property.name}")
private void setLocalDate(String localDateStr) {
if (localDateStr != null && !localDateStr.isEmpty()) {
localDate = LocalDate.parse(localDateStr, DateTimeFormatter.ofPattern("yyyy-MM-dd"));
}else{
localDate = LocalDate.now();
}
}
}
I Prefere 2nd way though...
public class Example {
#Value("#{T(java.time.LocalDate).parse('${property.name}', T(java.time.format.DateTimeFormatter).ofPattern('yyyy-MM-dd')) ?: T(java.time.LocalDate).now()}")
private LocalDate localDate;
}
Edit:- Fixed 2nd Way
#Value("#{ !('${date:}'.equals('')) ? T(java.time.LocalDate).parse('${date:}', T(java.time.format.DateTimeFormatter).ofPattern('MM-dd-yyyy')) " +
":T(java.time.LocalDate).now()}")
private LocalDate asOfDate;
For the first bit, you could create a converter:
#Component
#ConfigurationPropertiesBinding
public class LocalDateConverter implements Converter<String, LocalDate> {
#Override
public LocalDate convert(String s) {
if (s == null) {
return null;
}
return LocalDate.parse(s);
}
}
Your config Class will automatically use this for conversion.
For the 2nd you can just do:
if(my.date == null) iso = LocalDate.now()
There's an example of initializing a LocalDateTime value using annotations and configuration properties. I've tested that it works in Spring Boot 2.4.
MyComponent.kt fragment:
#Value("\${my.date}")
#DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME)
val myDate: LocalDateTime
application.yml:
my:
date: "2023-01-23T00:00:00"

why is the Object Mapper date fromat not used by message converters for date transform?

I am facing an issue with customised date formatting for JSON, where it of-course works in tests but fails on the deployed application. I want to use date pattern as dd-MM-yyyy pretty much standard and how it is expected here in India. There is a date formatter configured as well, and injected in the configuration like so
#Configuration
public class RepositoryWebConfiguration extends RepositoryRestMvcConfiguration {
private static final Logger LOGGER = LoggerFactory.getLogger(RepositoryWebConfiguration.class);
#Override
public void configureJacksonObjectMapper(ObjectMapper objectMapper) {
LOGGER
.debug("Configuring dd-MM-yyyy as default date pattern for all JSON representations in Rest DATA Repositories");
Jackson2ObjectMapperBuilder builder = new Jackson2ObjectMapperBuilder();
builder.indentOutput(true).dateFormat(new SimpleDateFormat("dd-MM-yyyy"));
objectMapper = builder.build();
}
}
Now this should work for JSON since I am injecting a specific date formatting, in my tests I first create a mapper with the same format
private ObjectMapper halObjectMapper() {
Jackson2ObjectMapperBuilder builder = new Jackson2ObjectMapperBuilder();
builder.indentOutput(true).dateFormat(new SimpleDateFormat("dd-MM-yyyy"));
ObjectMapper objectMapper = builder.build();
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
objectMapper.registerModule(new Jackson2HalModule());
return objectMapper;
}
I then use this mapper to generate the JSON for POST request. The JSON is generated all fine, I expected the format dd-MM-yyyy and I get exactly that
{
"id":null,
"name":"KABADI",
"seatsAvailable":40,
"workshopType":"KABADI FOUNDATION",
"date":"16-08-2015",
"venue":"http://localhost:8080/venues/2"
}
With the ObjectMapper registered I expect this JSON to be transformed to Workshop object without any issues & with the date format dd-MM-yyyy. However, the POST request fails due to format exception, and Jackson complains it cannot transform dd-MM-yyyy to Date as the available formats are "only"
("yyyy-MM-dd'T'HH:mm:ss.SSSZ", "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", "EEE,
dd MMM yyyy HH:mm:ss zzz", "yyyy-MM-dd")
here's the log
Caused by: com.fasterxml.jackson.databind.exc.InvalidFormatException: Can not construct instance of java.util.Date from String value '16-08-2015': not a valid representation (error: Failed to parse Date value '16-08-2015': Can not parse date "16-08-2015": not compatible with any of standard forms ("yyyy-MM-dd'T'HH:mm:ss.SSSZ", "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", "EEE, dd MMM yyyy HH:mm:ss zzz", "yyyy-MM-dd"))
at [Source: HttpInputOverHTTP#54153158; line: 5, column: 53] (through reference chain: com.agilityroots.doi.workshop.entity.Workshop["date"])
at com.fasterxml.jackson.databind.exc.InvalidFormatException.from(InvalidFormatException.java:55)
at com.fasterxml.jackson.databind.DeserializationContext.weirdStringException(DeserializationContext.java:810)
at com.fasterxml.jackson.databind.deser.std.StdDeserializer._parseDate(StdDeserializer.java:740)
at com.fasterxml.jackson.databind.deser.std.DateDeserializers$DateBasedDeserializer._parseDate(DateDeserializers.java:176)
at com.fasterxml.jackson.databind.deser.std.DateDeserializers$DateDeserializer.deserialize(DateDeserializers.java:262)
at com.fasterxml.jackson.databind.deser.std.DateDeserializers$DateDeserializer.deserialize(DateDeserializers.java:246)
at com.fasterxml.jackson.databind.deser.SettableBeanProperty.deserialize(SettableBeanProperty.java:538)
at com.fasterxml.jackson.databind.deser.impl.MethodProperty.deserializeAndSet(MethodProperty.java:99)
at com.fasterxml.jackson.databind.deser.BeanDeserializer.vanillaDeserialize(BeanDeserializer.java:238)
at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:118)
at com.fasterxml.jackson.databind.ObjectMapper._readMapAndClose(ObjectMapper.java:3066)
at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:2221)
at org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter.readJavaType(AbstractJackson2HttpMessageConverter.java:205)
... 46 common frames omitted
never had to look too much into these overrides earlier as in the usual Spring WebMVC + Boot scenario, this property used to the do the trick
spring.jackson.date-format=dd-MM-yyyy
So I might as well be missing something here, or I am configuring objectMapper in the wrong way in the sense that it is not injected? How can I get JSON transformers to accept dd-MM-yyyy format?
Was doing too much, this fixed the test for now. Object to JSON is still not in dd-MM-yyyy format, gets interpreted as a separate date for example 16-08-2015 returns as February X in 22 will look into that
#Override
public void configureJacksonObjectMapper(ObjectMapper objectMapper) {
objectMapper.setDateFormat(new SimpleDateFormat("dd-MM-yyyy"));
}
Update:
The other problem was annotation the date field in Workshop with #Temporal(TemporalType.DATE) which caused the date to be saved as 2015-08-16, removed that annotation too.

custom deserializer isn't called despite the #JsonDeserializer annotation

I am using Jackson with Jersey in Java.
Using the web API I am trying to send a POJO via a JSON file that also consists of a Joda DateTime object.
Because that is not a native Object, I built a custom serializer and deserializer for it. However, because the deserialiser does not seem to work, I send a POST request, but it isn't received on the server. Of course, when I move the date field from the JSON, it works perfectly.
The JSON being sent:
{"loc": [-0.1300836,51.5124623],
"visibility":"Public",
"date": "06 January 2014 09:51"}
The POST method receiving it:
#POST
#Timed
public String createTadu(#Valid Tadu tadu) throws JsonParseException, JsonMappingException, IOException {
Tadu createdTadu = new Tadu(tadu);
taduCollection.insert(createdTadu);
return String.format("{\"status\":\"success\", \"id\":\"%s\"}", tadu.getId());
}
The POJO DateTime declaration:
private DateTime date;
#JsonSerialize(using = CustomDateSerializer.class)
public DateTime getDate() {
return date;
}
#JsonDeserialize(using = CustomDateDeserializer.class)
public void setDate(DateTime date) {
this.date = date;
}
and the custom serializer/deserializer:
public class CustomDateSerializer extends JsonSerializer<DateTime> {
private static DateTimeFormatter formatter = DateTimeFormat
.forPattern("dd MMMM yyyy HH:mm"); // 31 December 2013 16:22
#Override
public void serialize(DateTime value, JsonGenerator gen,
SerializerProvider arg2) throws IOException,
JsonProcessingException {
gen.writeString(formatter.print(value));
}
}
public class CustomDateDeserializer extends JsonDeserializer<DateTime>
{
#Override
public DateTime deserialize(JsonParser jsonparser,
DeserializationContext deserializationcontext) throws IOException, JsonProcessingException {
DateTimeFormatter formatter = DateTimeFormat.forPattern("dd MMMM yyyy HH:mm");
String date = jsonparser.getText();
return formatter.parseDateTime(date);
}
}
It is clear to me that the POST method (createTadu) is not being called because a String called "date" is sent via the JSON, but it is not a valid Tadu object because that is not a DateTime called "date". I was hoping that the custom deserializer annotation would fix that.
Thanks!
I couldn't understand why this method was not working. I ended up downloading the jackson-datatype-joda module.
The only issue with this module is that it yet does not allow custom formatters of date time and only operates with ISO 8601.
Another issue is that in the database the date time is converted to an epoch UTC time, with an added 4 zeros in the end (why the zeros..). So when fetching it back to the client a conversion from epoch to a presentable date needed to be done.

Categories