Skip to content

Commit 6a84ba9

Browse files
authored
Add support for java.util.Calendar as epoch millis or string format with timezone (#342)
* Add support for java.util.Calendar as epoch millis or string format with timezone * Fix format
1 parent 8964b66 commit 6a84ba9

File tree

6 files changed

+159
-18
lines changed

6 files changed

+159
-18
lines changed
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package org.example.customer.datetime;
2+
3+
import io.avaje.jsonb.Json;
4+
5+
import java.util.Calendar;
6+
7+
@Json
8+
public record MyCalData(Calendar cal) {
9+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
package org.example.customer.datetime;
2+
3+
import io.avaje.jsonb.JsonType;
4+
import io.avaje.jsonb.Jsonb;
5+
import org.junit.jupiter.api.Test;
6+
7+
import java.time.ZoneId;
8+
import java.time.ZonedDateTime;
9+
import java.util.GregorianCalendar;
10+
import java.util.TimeZone;
11+
12+
import static org.assertj.core.api.Assertions.assertThat;
13+
14+
class CalendarTest {
15+
16+
@Test
17+
void calendarAsEpochMillis() {
18+
Jsonb jsonb = Jsonb.builder().calendarAsString(false).build();
19+
JsonType<MyCalData> type = jsonb.type(MyCalData.class);
20+
21+
var zdt = ZonedDateTime.of(2001, 3, 23, 0, 1, 2, 0, ZoneId.of("Pacific/Auckland"));
22+
var calendar = GregorianCalendar.from(zdt);
23+
var data = new MyCalData(calendar);
24+
var asJson = type.toJson(data);
25+
var fromJson = type.fromJson(asJson);
26+
27+
assertThat(fromJson.cal().toInstant()).isEqualTo(data.cal().toInstant());
28+
// deserialized as UTC
29+
assertThat(fromJson.cal().getTimeZone()).isEqualTo(TimeZone.getTimeZone(ZoneId.of("UTC")));
30+
}
31+
32+
@Test
33+
void calendarAsString() {
34+
Jsonb jsonb = Jsonb.builder().calendarAsString(true).build();
35+
JsonType<MyCalData> type = jsonb.type(MyCalData.class);
36+
37+
var zdt = ZonedDateTime.of(2001, 3, 23, 0, 1, 2, 0, ZoneId.of("Pacific/Auckland"));
38+
// note calendar deserialized using ISO8601 with
39+
// setFirstDayOfWeek(MONDAY) and setMinimalDaysInFirstWeek(4);
40+
var calendar = GregorianCalendar.from(zdt);
41+
var data = new MyCalData(calendar);
42+
var asJson = type.toJson(data);
43+
var fromJson = type.fromJson(asJson);
44+
45+
assertThat(fromJson.cal().toInstant()).isEqualTo(data.cal().toInstant());
46+
assertThat(fromJson.cal().getTimeZone()).isEqualTo(data.cal().getTimeZone());
47+
assertThat(fromJson.cal().getFirstDayOfWeek()).isEqualTo(data.cal().getFirstDayOfWeek());
48+
assertThat(fromJson.cal().getMinimalDaysInFirstWeek()).isEqualTo(data.cal().getMinimalDaysInFirstWeek());
49+
assertThat(fromJson).isEqualTo(data);
50+
}
51+
}

jsonb/src/main/java/io/avaje/jsonb/Jsonb.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -386,6 +386,12 @@ interface Builder {
386386
*/
387387
Builder mathTypesAsString(boolean mathTypesAsString);
388388

389+
/**
390+
* Set to true for Calendar to serialise as String with timezone and false to serialise
391+
* as UTC epoch millis. Defaults to false.
392+
*/
393+
Builder calendarAsString(boolean calendarAsString);
394+
389395
/**
390396
* Determines how byte buffers are recycled
391397
*/

jsonb/src/main/java/io/avaje/jsonb/core/CoreAdapterBuilder.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,13 +36,14 @@ final class CoreAdapterBuilder {
3636
private final Map<Object, JsonAdapter<?>> adapterCache = new ConcurrentHashMap<>();
3737
private final ReentrantLock lock = new ReentrantLock();
3838

39-
CoreAdapterBuilder(DJsonb context, List<AdapterFactory> userFactories, boolean mathAsString) {
39+
CoreAdapterBuilder(DJsonb context, List<AdapterFactory> userFactories, boolean mathAsString, boolean calendarAsString) {
4040
this.context = context;
4141
this.factories = new ArrayList<>();
4242
this.factories.addAll(userFactories);
4343
this.factories.add(CoreAdapters.FACTORY);
4444
this.factories.add(BasicTypeAdapters.FACTORY);
4545
this.factories.add(JavaTimeAdapters.FACTORY);
46+
this.factories.add(new JavaTimeAdapters.CalendarFactory(calendarAsString));
4647
this.factories.add(new MathAdapters(mathAsString));
4748
this.factories.add(CoreAdapters.COLLECTION_FACTORY);
4849
this.factories.add(CoreAdapters.MAP_FACTORY);

jsonb/src/main/java/io/avaje/jsonb/core/DJsonb.java

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,10 @@ final class DJsonb implements Jsonb {
3939
boolean serializeEmpty,
4040
boolean failOnUnknown,
4141
boolean mathAsString,
42+
boolean calendarAsString,
4243
BufferRecycleStrategy strategy) {
4344

44-
this.builder = new CoreAdapterBuilder(this, factories, mathAsString);
45+
this.builder = new CoreAdapterBuilder(this, factories, mathAsString, calendarAsString);
4546
if (adapter != null) {
4647
this.io = adapter;
4748
} else {
@@ -245,6 +246,7 @@ static final class DBuilder implements Jsonb.Builder {
245246
private final List<AdapterFactory> factories = new ArrayList<>();
246247
private boolean failOnUnknown;
247248
private boolean mathTypesAsString;
249+
private boolean calendarAsString;
248250
private boolean serializeNulls;
249251
private boolean serializeEmpty = true;
250252
private JsonStream adapter;
@@ -274,6 +276,12 @@ public Builder mathTypesAsString(boolean mathTypesAsString) {
274276
return this;
275277
}
276278

279+
@Override
280+
public Builder calendarAsString(boolean calendarAsString) {
281+
this.calendarAsString = calendarAsString;
282+
return this;
283+
}
284+
277285
@Override
278286
public Builder bufferRecycling(BufferRecycleStrategy strategy) {
279287
this.strategy = strategy;
@@ -326,7 +334,7 @@ private void registerComponents() {
326334
@Override
327335
public DJsonb build() {
328336
registerComponents();
329-
return new DJsonb(adapter, factories, serializeNulls, serializeEmpty, failOnUnknown, mathTypesAsString, strategy);
337+
return new DJsonb(adapter, factories, serializeNulls, serializeEmpty, failOnUnknown, mathTypesAsString, calendarAsString, strategy);
330338
}
331339

332340
static <T> AdapterFactory newAdapterFactory(Type type, JsonAdapter<T> jsonAdapter) {

jsonb/src/main/java/io/avaje/jsonb/core/JavaTimeAdapters.java

Lines changed: 81 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,18 @@
11
package io.avaje.jsonb.core;
22

3+
import java.lang.reflect.Type;
4+
import java.util.Calendar;
35
import java.util.Date;
46

57
import io.avaje.json.JsonAdapter;
68
import io.avaje.jsonb.AdapterFactory;
79
import io.avaje.json.JsonReader;
810
import io.avaje.json.JsonWriter;
11+
import io.avaje.jsonb.Jsonb;
912

1013
import java.time.*;
14+
import java.util.GregorianCalendar;
15+
import java.util.TimeZone;
1116

1217
/**
1318
* Adds support for java time types.
@@ -34,26 +39,44 @@ final class JavaTimeAdapters {
3439
return null;
3540
};
3641

42+
static final class CalendarFactory implements AdapterFactory {
43+
44+
private final boolean calendarAsString;
45+
46+
CalendarFactory(boolean calendarAsString) {
47+
this.calendarAsString = calendarAsString;
48+
}
49+
50+
@Override
51+
public JsonAdapter<?> create(Type type, Jsonb jsonb) {
52+
if (type == Calendar.class) {
53+
return calendarAsString
54+
? JavaTimeAdapters.CALENDAR_ZONED.nullSafe()
55+
: JavaTimeAdapters.CALENDAR_EPOCH_MILLIS.nullSafe() ;
56+
}
57+
return null;
58+
}
59+
}
60+
3761
/**
3862
* Using ISO-8601
3963
*/
40-
4164
private static final JsonAdapter<Date> UTIL_DATE = new JsonAdapter<>() {
42-
@Override
43-
public Date fromJson(JsonReader reader) {
44-
return Date.from(Instant.parse(reader.readString()));
45-
}
65+
@Override
66+
public Date fromJson(JsonReader reader) {
67+
return Date.from(Instant.parse(reader.readString()));
68+
}
4669

47-
@Override
48-
public void toJson(JsonWriter writer, Date value) {
49-
writer.value(value.toInstant().toString());
50-
}
70+
@Override
71+
public void toJson(JsonWriter writer, Date value) {
72+
writer.value(value.toInstant().toString());
73+
}
5174

52-
@Override
53-
public String toString() {
54-
return "JsonAdapter(Date)";
55-
}
56-
};
75+
@Override
76+
public String toString() {
77+
return "JsonAdapter(Date)";
78+
}
79+
};
5780

5881
private static final JsonAdapter<Duration> DURATION_ADAPTER = new JsonAdapter<>() {
5982
@Override
@@ -140,6 +163,49 @@ public String toString() {
140163
}
141164
};
142165

166+
private static final JsonAdapter<Calendar> CALENDAR_ZONED = new JsonAdapter<>() {
167+
@Override
168+
public Calendar fromJson(JsonReader reader) {
169+
final ZonedDateTime zdt = ZonedDateTime.parse(reader.readString());
170+
return GregorianCalendar.from(zdt);
171+
}
172+
173+
@Override
174+
public void toJson(JsonWriter writer, Calendar value) {
175+
TimeZone timeZone = value.getTimeZone();
176+
ZonedDateTime zonedDateTime = ZonedDateTime.ofInstant(value.toInstant(), timeZone.toZoneId());
177+
writer.value(zonedDateTime.toString());
178+
}
179+
180+
@Override
181+
public String toString() {
182+
return "JsonAdapter(Calendar)";
183+
}
184+
};
185+
186+
private static final JsonAdapter<Calendar> CALENDAR_EPOCH_MILLIS = new JsonAdapter<>() {
187+
private final TimeZone UTC = TimeZone.getTimeZone(ZoneId.of("UTC"));
188+
189+
@Override
190+
public Calendar fromJson(JsonReader reader) {
191+
long epochMillis = reader.readLong();
192+
Calendar instance = Calendar.getInstance();
193+
instance.setTimeInMillis(epochMillis);
194+
instance.setTimeZone(UTC);
195+
return instance;
196+
}
197+
198+
@Override
199+
public void toJson(JsonWriter writer, Calendar value) {
200+
writer.value(value.getTimeInMillis());
201+
}
202+
203+
@Override
204+
public String toString() {
205+
return "JsonAdapter(Calendar)";
206+
}
207+
};
208+
143209
private static final JsonAdapter<ZoneOffset> ZONE_OFFSET_ADAPTER = new JsonAdapter<>() {
144210
@Override
145211
public ZoneOffset fromJson(JsonReader reader) {
@@ -170,7 +236,7 @@ public void toJson(JsonWriter writer, ZoneId value) {
170236

171237
@Override
172238
public String toString() {
173-
return "JsonAdapter(ZoneOffset)";
239+
return "JsonAdapter(ZoneId)";
174240
}
175241
};
176242

0 commit comments

Comments
 (0)