diff --git a/plusone-commons/pom.xml b/plusone-commons/pom.xml index 223023b..c238688 100644 --- a/plusone-commons/pom.xml +++ b/plusone-commons/pom.xml @@ -55,6 +55,13 @@ true + + + com.google.code.gson + gson + true + + @@ -111,12 +118,6 @@ jackson-datatype-jsr310 test - - - com.google.code.gson - gson - test - diff --git a/plusone-commons/src/main/java/xyz/zhouxy/plusone/commons/gson/LocalDateTimeTypeAdapter.java b/plusone-commons/src/main/java/xyz/zhouxy/plusone/commons/gson/LocalDateTimeTypeAdapter.java new file mode 100644 index 0000000..317f404 --- /dev/null +++ b/plusone-commons/src/main/java/xyz/zhouxy/plusone/commons/gson/LocalDateTimeTypeAdapter.java @@ -0,0 +1,71 @@ +/* + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package xyz.zhouxy.plusone.commons.gson; + +import java.io.IOException; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; + +import com.google.gson.TypeAdapter; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonWriter; + +import xyz.zhouxy.plusone.commons.util.AssertTools; + +/** + * {@code LocalDateTime} 的 {@code TypeAdapter}, + * 用于 Gson 对 {@code LocalDateTime} 进行相互转换。 + * + * @author ZhouXY + * @since 1.1.0 + * @see TypeAdapter + * @see com.google.gson.GsonBuilder + */ +public class LocalDateTimeTypeAdapter extends TypeAdapter { + + private final DateTimeFormatter dateTimeFormatter; + + /** + * 默认构造函数, + * 使用 {@link DateTimeFormatter#ISO_LOCAL_DATE_TIME} 进行 {@link LocalDateTime} 的序列化与反序列化。 + */ + public LocalDateTimeTypeAdapter() { + this.dateTimeFormatter = DateTimeFormatter.ISO_LOCAL_DATE_TIME; + } + + /** + * 构造函数, + * 使用传入的 {@link DateTimeFormatter} 进行 {@link LocalDateTime} 的序列化与反序列化。 + * + * @param formatter 用于序列化 {@link LocalDateTime} 的格式化器,不可为 {@code null}。 + */ + public LocalDateTimeTypeAdapter(DateTimeFormatter formatter) { + AssertTools.checkArgumentNotNull(formatter, "formatter can not be null."); + this.dateTimeFormatter = formatter; + } + + /** {@inheritDoc} */ + @Override + public void write(JsonWriter out, LocalDateTime value) throws IOException { + out.value(dateTimeFormatter.format(value)); + } + + /** {@inheritDoc} */ + @Override + public LocalDateTime read(JsonReader in) throws IOException { + return LocalDateTime.parse(in.nextString(), dateTimeFormatter); + } +} diff --git a/plusone-commons/src/main/java/xyz/zhouxy/plusone/commons/gson/LocalDateTypeAdapter.java b/plusone-commons/src/main/java/xyz/zhouxy/plusone/commons/gson/LocalDateTypeAdapter.java new file mode 100644 index 0000000..5a42ad3 --- /dev/null +++ b/plusone-commons/src/main/java/xyz/zhouxy/plusone/commons/gson/LocalDateTypeAdapter.java @@ -0,0 +1,71 @@ +/* + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package xyz.zhouxy.plusone.commons.gson; + +import java.io.IOException; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; + +import com.google.gson.TypeAdapter; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonWriter; + +import xyz.zhouxy.plusone.commons.util.AssertTools; + +/** + * {@code LocalDate} 的 {@code TypeAdapter}, + * 用于 Gson 对 {@code LocalDate} 进行相互转换。 + * + * @author ZhouXY + * @since 1.1.0 + * @see TypeAdapter + * @see com.google.gson.GsonBuilder + */ +public class LocalDateTypeAdapter extends TypeAdapter { + + private final DateTimeFormatter dateTimeFormatter; + + /** + * 默认构造函数, + * 使用 {@link DateTimeFormatter#ISO_LOCAL_DATE} 进行 {@link LocalDate} 的序列化与反序列化。 + */ + public LocalDateTypeAdapter() { + this.dateTimeFormatter = DateTimeFormatter.ISO_LOCAL_DATE; + } + + /** + * 构造函数, + * 使用传入的 {@link DateTimeFormatter} 进行 {@link LocalDate} 的序列化与反序列化。 + * + * @param formatter 用于序列化 {@link LocalDate} 的格式化器,不可为 {@code null}。 + */ + public LocalDateTypeAdapter(DateTimeFormatter formatter) { + AssertTools.checkArgumentNotNull(formatter, "formatter can not be null."); + this.dateTimeFormatter = formatter; + } + + /** {@inheritDoc} */ + @Override + public void write(JsonWriter out, LocalDate value) throws IOException { + out.value(dateTimeFormatter.format(value)); + } + + /** {@inheritDoc} */ + @Override + public LocalDate read(JsonReader in) throws IOException { + return LocalDate.parse(in.nextString(), dateTimeFormatter); + } +} diff --git a/plusone-commons/src/main/java/xyz/zhouxy/plusone/commons/gson/ZonedDateTimeTypeAdapter.java b/plusone-commons/src/main/java/xyz/zhouxy/plusone/commons/gson/ZonedDateTimeTypeAdapter.java new file mode 100644 index 0000000..7acd75e --- /dev/null +++ b/plusone-commons/src/main/java/xyz/zhouxy/plusone/commons/gson/ZonedDateTimeTypeAdapter.java @@ -0,0 +1,71 @@ +/* + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package xyz.zhouxy.plusone.commons.gson; + +import java.io.IOException; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; + +import com.google.gson.TypeAdapter; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonWriter; + +import xyz.zhouxy.plusone.commons.util.AssertTools; + +/** + * {@code ZonedDateTime} 的 {@code TypeAdapter}, + * 用于 Gson 对 {@code ZonedDateTime} 进行相互转换。 + * + * @author ZhouXY + * @since 1.1.0 + * @see TypeAdapter + * @see com.google.gson.GsonBuilder + */ +public class ZonedDateTimeTypeAdapter extends TypeAdapter { + + private final DateTimeFormatter dateTimeFormatter; + + /** + * 默认构造函数, + * 使用 {@link DateTimeFormatter#ISO_ZONED_DATE_TIME} 进行 {@link ZonedDateTime} 的序列化与反序列化。 + */ + public ZonedDateTimeTypeAdapter() { + this.dateTimeFormatter = DateTimeFormatter.ISO_ZONED_DATE_TIME; + } + + /** + * 构造函数, + * 使用传入的 {@link DateTimeFormatter} 进行 {@link ZonedDateTime} 的序列化与反序列化。 + * + * @param formatter 用于序列化 {@link ZonedDateTime} 的格式化器,不可为 {@code null}。 + */ + public ZonedDateTimeTypeAdapter(DateTimeFormatter formatter) { + AssertTools.checkArgumentNotNull(formatter, "formatter can not be null."); + this.dateTimeFormatter = formatter; + } + + /** {@inheritDoc} */ + @Override + public void write(JsonWriter out, ZonedDateTime value) throws IOException { + out.value(dateTimeFormatter.format(value)); + } + + /** {@inheritDoc} */ + @Override + public ZonedDateTime read(JsonReader in) throws IOException { + return ZonedDateTime.parse(in.nextString(), dateTimeFormatter); + } +} diff --git a/plusone-commons/src/test/java/xyz/zhouxy/plusone/commons/gson/GsonTypeAdapterTests.java b/plusone-commons/src/test/java/xyz/zhouxy/plusone/commons/gson/GsonTypeAdapterTests.java new file mode 100644 index 0000000..b1d2022 --- /dev/null +++ b/plusone-commons/src/test/java/xyz/zhouxy/plusone/commons/gson/GsonTypeAdapterTests.java @@ -0,0 +1,131 @@ +/* + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package xyz.zhouxy.plusone.commons.gson; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeFormatterBuilder; +import java.time.temporal.ChronoField; + +import org.junit.jupiter.api.Test; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class GsonTypeAdapterTests { + + final Gson gsonWithDefaultFormatter = new GsonBuilder() + .registerTypeAdapter(LocalDate.class, new LocalDateTypeAdapter().nullSafe()) + .registerTypeAdapter(LocalDateTime.class, new LocalDateTimeTypeAdapter().nullSafe()) + .registerTypeAdapter(ZonedDateTime.class, new ZonedDateTimeTypeAdapter().nullSafe()) + .create(); + + final DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern("yyyy/MM/dd"); + final DateTimeFormatter localDateTimeFormatter = new DateTimeFormatterBuilder() + .appendPattern("yyyy/MM/dd HH:mm:ss") + .appendValue(ChronoField.MILLI_OF_SECOND, 3) + .toFormatter(); + final DateTimeFormatter zonedDateTimeFormatter = new DateTimeFormatterBuilder() + .appendPattern("yyyy/MM/dd HH:mm:ss") + .appendValue(ChronoField.MILLI_OF_SECOND, 3) + .appendZoneId() + .toFormatter(); + final Gson gsonWithSpecifiedFormatter = new GsonBuilder() + .registerTypeAdapter(LocalDate.class, new LocalDateTypeAdapter(dateFormatter).nullSafe()) + .registerTypeAdapter(LocalDateTime.class, new LocalDateTimeTypeAdapter(localDateTimeFormatter).nullSafe()) + .registerTypeAdapter(ZonedDateTime.class, new ZonedDateTimeTypeAdapter(zonedDateTimeFormatter).nullSafe()) + .create(); + + final LocalDate date = LocalDate.of(2025, 6, 6); + final LocalDateTime localDateTime = date.atTime(6, 6, 6, 666000000); + final ZonedDateTime zonedDateTime = localDateTime.atZone(ZoneId.of("+08:00")); + + @Test + void test_serialize_defaultFormatter() { + Foo foo = new Foo(); + foo.localDate = date; + foo.localDateTime = localDateTime; + foo.zonedDateTime = zonedDateTime; + + String json = String.format( + "{\"localDate\":\"%s\",\"localDateTime\":\"%s\",\"zonedDateTime\":\"%s\"}", + DateTimeFormatter.ISO_LOCAL_DATE.format(date), + DateTimeFormatter.ISO_LOCAL_DATE_TIME.format(localDateTime), + DateTimeFormatter.ISO_ZONED_DATE_TIME.format(zonedDateTime) + ); + + assertEquals(json, gsonWithDefaultFormatter.toJson(foo)); + } + + @Test + void test_serialize_specifiedFormatter() { + Foo foo = new Foo(); + foo.localDate = date; + foo.localDateTime = localDateTime; + foo.zonedDateTime = zonedDateTime; + + String json = String.format( + "{\"localDate\":\"%s\",\"localDateTime\":\"%s\",\"zonedDateTime\":\"%s\"}", + dateFormatter.format(date), + localDateTimeFormatter.format(localDateTime), + zonedDateTimeFormatter.format(zonedDateTime) + ); + + assertEquals(json, gsonWithSpecifiedFormatter.toJson(foo)); + } + + @Test + void test_deserialize_defaultFormatter() { + String json = String.format( + "{\"localDate\":\"%s\",\"localDateTime\":\"%s\",\"zonedDateTime\":\"%s\"}", + DateTimeFormatter.ISO_LOCAL_DATE.format(date), + DateTimeFormatter.ISO_LOCAL_DATE_TIME.format(localDateTime), + DateTimeFormatter.ISO_ZONED_DATE_TIME.format(zonedDateTime) + ); + Foo foo = gsonWithDefaultFormatter.fromJson(json, Foo.class); + assertEquals(date, foo.localDate); + assertEquals(localDateTime, foo.localDateTime); + assertEquals(zonedDateTime, foo.zonedDateTime); + } + + @Test + void test_deserialize_specifiedFormatter() { + String json = String.format( + "{\"localDate\":\"%s\",\"localDateTime\":\"%s\",\"zonedDateTime\":\"%s\"}", + dateFormatter.format(date), + localDateTimeFormatter.format(localDateTime), + zonedDateTimeFormatter.format(zonedDateTime) + ); + Foo foo = gsonWithSpecifiedFormatter.fromJson(json, Foo.class); + assertEquals(date, foo.localDate); + assertEquals(localDateTime, foo.localDateTime); + assertEquals(zonedDateTime, foo.zonedDateTime); + } + + static class Foo { + LocalDate localDate; + LocalDateTime localDateTime; + ZonedDateTime zonedDateTime; + } +}