Skip to content

Commit 00539c3

Browse files
authored
fix: add support for Java Time types for Element properties (#17105)
Allows POJOs with Java Time members to be serialized as Element JSON properties. To avoid breaking changes, the Object mapper WRITE_DATES_AS_TIMESTAMPS feature is left as default, so some types such as LocalTime, LocaDate and LocalDateTime are serialized as arrays of numbers, as documented in Jackson JavaTimeModule. Fixes #13317
1 parent 27d80df commit 00539c3

File tree

4 files changed

+124
-0
lines changed

4 files changed

+124
-0
lines changed

flow-server/pom.xml

+5
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,11 @@
9696
<artifactId>jackson-databind</artifactId>
9797
<version>${jackson.databind.version}</version>
9898
</dependency>
99+
<dependency>
100+
<groupId>com.fasterxml.jackson.datatype</groupId>
101+
<artifactId>jackson-datatype-jsr310</artifactId>
102+
<version>${jackson.version}</version>
103+
</dependency>
99104

100105
<!-- Jsoup for BootstrapHandler, Template, ... -->
101106
<dependency>

flow-server/src/main/java/com/vaadin/flow/internal/JsonUtils.java

+5
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333

3434
import com.fasterxml.jackson.core.JsonProcessingException;
3535
import com.fasterxml.jackson.databind.ObjectMapper;
36+
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
3637

3738
import elemental.json.Json;
3839
import elemental.json.JsonArray;
@@ -57,6 +58,10 @@ public final class JsonUtils {
5758

5859
private static final ObjectMapper objectMapper = new ObjectMapper();
5960

61+
static {
62+
objectMapper.registerModule(new JavaTimeModule());
63+
}
64+
6065
/**
6166
* Collects a stream of JSON values to a JSON array.
6267
*

flow-server/src/test/java/com/vaadin/flow/dom/ElementTest.java

+60
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,16 @@
77
import java.lang.reflect.Modifier;
88
import java.net.URI;
99
import java.net.URISyntaxException;
10+
import java.time.Duration;
11+
import java.time.LocalDate;
12+
import java.time.LocalDateTime;
13+
import java.time.LocalTime;
14+
import java.time.ZoneId;
15+
import java.time.ZonedDateTime;
1016
import java.util.ArrayList;
1117
import java.util.Arrays;
1218
import java.util.Collections;
19+
import java.util.Date;
1320
import java.util.HashMap;
1421
import java.util.HashSet;
1522
import java.util.List;
@@ -20,6 +27,7 @@
2027
import java.util.concurrent.atomic.AtomicInteger;
2128
import java.util.concurrent.atomic.AtomicReference;
2229
import java.util.stream.Collectors;
30+
import java.util.stream.DoubleStream;
2331

2432
import net.jcip.annotations.NotThreadSafe;
2533
import org.junit.Assert;
@@ -33,6 +41,7 @@
3341
import com.vaadin.flow.component.internal.UIInternals.JavaScriptInvocation;
3442
import com.vaadin.flow.component.page.PendingJavaScriptResult;
3543
import com.vaadin.flow.dom.impl.BasicElementStateProvider;
44+
import com.vaadin.flow.internal.JsonUtils;
3645
import com.vaadin.flow.internal.NullOwner;
3746
import com.vaadin.flow.internal.StateNode;
3847
import com.vaadin.flow.internal.nodefeature.ComponentMapping;
@@ -553,6 +562,24 @@ public double getDbl() {
553562
}
554563
}
555564

565+
public static class BeanWithTemporalFields {
566+
567+
public LocalTime localTime = LocalTime.of(10, 23, 55);
568+
569+
public LocalDate localDate = LocalDate.of(2023, 6, 26);
570+
571+
public LocalDateTime localDateTime = localDate.atTime(localTime);
572+
573+
public java.sql.Date sqlDate = java.sql.Date.valueOf(localDate);
574+
575+
public Date date = new Date(sqlDate.getTime());
576+
577+
public ZonedDateTime zonedDateTime = ZonedDateTime.of(localDateTime,
578+
ZoneId.of("Europe/Rome"));
579+
580+
public Duration duration = Duration.ofSeconds(10);
581+
}
582+
556583
@Test
557584
public void propertyRawValues() {
558585
Element element = ElementFactory.createDiv();
@@ -633,6 +660,33 @@ public void propertyNames() {
633660
Assert.assertEquals(0, element.getPropertyNames().count());
634661
}
635662

663+
@Test
664+
public void setProperty_javaTimeObject() {
665+
BeanWithTemporalFields bean = new BeanWithTemporalFields();
666+
Element element = ElementFactory.createDiv();
667+
668+
element.setPropertyBean("bean", bean);
669+
JsonObject json = (JsonObject) element.getPropertyRaw("bean");
670+
671+
Assert.assertTrue("LocalTime not serialized as expected",
672+
JsonUtils.jsonEquals(createNumberArray(10, 23, 55),
673+
json.getArray("localTime")));
674+
Assert.assertTrue("LocalDate not serialized as expected",
675+
JsonUtils.jsonEquals(createNumberArray(2023, 6, 26),
676+
json.getArray("localDate")));
677+
Assert.assertTrue("LocalDateTime not serialized as expected",
678+
JsonUtils.jsonEquals(createNumberArray(2023, 6, 26, 10, 23, 55),
679+
json.getArray("localDateTime")));
680+
Assert.assertEquals("ZonedDateTime not serialized as expected",
681+
bean.zonedDateTime.toEpochSecond(),
682+
json.getNumber("zonedDateTime"), 0);
683+
Assert.assertEquals("ZonedDateTime not serialized as expected",
684+
bean.sqlDate.getTime(), json.getNumber("sqlDate"), 0);
685+
Assert.assertEquals("ZonedDateTime not serialized as expected",
686+
bean.date.getTime(), json.getNumber("date"), 0);
687+
Assert.assertEquals(10.0, json.getNumber("duration"), 0);
688+
}
689+
636690
private static Element createPropertyAssertElement(Object value) {
637691
Element element = ElementFactory.createDiv();
638692

@@ -2534,4 +2588,10 @@ private void assertEquals(JavaScriptInvocation expected,
25342588
actual.getParameters().toArray());
25352589

25362590
}
2591+
2592+
private static JsonArray createNumberArray(double... items) {
2593+
return DoubleStream.of(items).mapToObj(Json::create)
2594+
.collect(JsonUtils.asArray());
2595+
}
2596+
25372597
}

flow-server/src/test/java/com/vaadin/flow/internal/JsonUtilsTest.java

+54
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,15 @@
1515
*/
1616
package com.vaadin.flow.internal;
1717

18+
import java.time.Duration;
19+
import java.time.LocalDate;
20+
import java.time.LocalDateTime;
21+
import java.time.LocalTime;
22+
import java.time.ZoneId;
23+
import java.time.ZonedDateTime;
1824
import java.util.ArrayList;
1925
import java.util.Collections;
26+
import java.util.Date;
2027
import java.util.HashMap;
2128
import java.util.List;
2229
import java.util.Map;
@@ -117,6 +124,11 @@ private static JsonArray createTestArray2() {
117124
.collect(JsonUtils.asArray());
118125
}
119126

127+
private static JsonArray createNumberArray(double... items) {
128+
return DoubleStream.of(items).mapToObj(Json::create)
129+
.collect(JsonUtils.asArray());
130+
}
131+
120132
@Test
121133
public void collectEmptyStream() {
122134
Stream<JsonValue> jsonValueStream = Stream.empty();
@@ -276,6 +288,24 @@ public String getChildValue() {
276288
}
277289
}
278290

291+
public static class BeanWithTemporalFields {
292+
293+
public LocalTime localTime = LocalTime.of(10, 23, 55);
294+
295+
public LocalDate localDate = LocalDate.of(2023, 6, 26);
296+
297+
public LocalDateTime localDateTime = localDate.atTime(localTime);
298+
299+
public java.sql.Date sqlDate = java.sql.Date.valueOf(localDate);
300+
301+
public Date date = new Date(sqlDate.getTime());
302+
303+
public ZonedDateTime zonedDateTime = ZonedDateTime.of(localDateTime,
304+
ZoneId.of("Europe/Rome"));
305+
306+
public Duration duration = Duration.ofSeconds(10);
307+
}
308+
279309
public static class ListAndMapBean {
280310
private Map<String, Integer> integerMap = new HashMap<>();
281311
private Map<String, ChildBean> childBeanMap = new HashMap<>();
@@ -335,6 +365,30 @@ public void nestedBeanToJson() {
335365
Assert.assertEquals("child", child.getString("childValue"));
336366
}
337367

368+
@Test
369+
public void beanWithTimeFields() {
370+
BeanWithTemporalFields bean = new BeanWithTemporalFields();
371+
JsonObject json = JsonUtils.beanToJson(bean);
372+
373+
Assert.assertTrue("LocalTime not serialized as expected",
374+
JsonUtils.jsonEquals(createNumberArray(10, 23, 55),
375+
json.getArray("localTime")));
376+
Assert.assertTrue("LocalDate not serialized as expected",
377+
JsonUtils.jsonEquals(createNumberArray(2023, 6, 26),
378+
json.getArray("localDate")));
379+
Assert.assertTrue("LocalDateTime not serialized as expected",
380+
JsonUtils.jsonEquals(createNumberArray(2023, 6, 26, 10, 23, 55),
381+
json.getArray("localDateTime")));
382+
Assert.assertEquals("ZonedDateTime not serialized as expected",
383+
bean.zonedDateTime.toEpochSecond(),
384+
json.getNumber("zonedDateTime"), 0);
385+
Assert.assertEquals("ZonedDateTime not serialized as expected",
386+
bean.sqlDate.getTime(), json.getNumber("sqlDate"), 0);
387+
Assert.assertEquals("ZonedDateTime not serialized as expected",
388+
bean.date.getTime(), json.getNumber("date"), 0);
389+
Assert.assertEquals(10.0, json.getNumber("duration"), 0);
390+
}
391+
338392
@Test
339393
public void nullChildBean() {
340394
ParentBean bean = new ParentBean();

0 commit comments

Comments
 (0)