Skip to content

Commit 98c3e53

Browse files
ilya-gdkhalanskyjb
authored andcommitted
Implement java.io.Serializable for some of the classes (#373)
Implement java.io.Serializable for * LocalDate * LocalTime * LocalDateTime * UtcOffset TimeZone is not `Serializable` because its behavior is system-dependent. We can make it `java.io.Serializable` later if there is demand. Instant is not `Serializable` because it is about to be removed. We are using string representations instead of relying on Java's entities being `java.io.Serializable` so that we have more freedom to change our implementation later. Fixes #143
1 parent b37b329 commit 98c3e53

File tree

8 files changed

+195
-10
lines changed

8 files changed

+195
-10
lines changed

core/api/kotlinx-datetime.api

+19-4
Original file line numberDiff line numberDiff line change
@@ -272,7 +272,7 @@ public final class kotlinx/datetime/InstantKt {
272272
public static final fun yearsUntil (Lkotlinx/datetime/Instant;Lkotlinx/datetime/Instant;Lkotlinx/datetime/TimeZone;)I
273273
}
274274

275-
public final class kotlinx/datetime/LocalDate : java/lang/Comparable {
275+
public final class kotlinx/datetime/LocalDate : java/io/Serializable, java/lang/Comparable {
276276
public static final field Companion Lkotlinx/datetime/LocalDate$Companion;
277277
public fun <init> (III)V
278278
public fun <init> (ILjava/time/Month;I)V
@@ -342,7 +342,7 @@ public final class kotlinx/datetime/LocalDateKt {
342342
public static final fun toLocalDate (Ljava/lang/String;)Lkotlinx/datetime/LocalDate;
343343
}
344344

345-
public final class kotlinx/datetime/LocalDateTime : java/lang/Comparable {
345+
public final class kotlinx/datetime/LocalDateTime : java/io/Serializable, java/lang/Comparable {
346346
public static final field Companion Lkotlinx/datetime/LocalDateTime$Companion;
347347
public fun <init> (IIIIIII)V
348348
public synthetic fun <init> (IIIIIIIILkotlin/jvm/internal/DefaultConstructorMarker;)V
@@ -397,7 +397,7 @@ public final class kotlinx/datetime/LocalDateTimeKt {
397397
public static final fun toLocalDateTime (Ljava/lang/String;)Lkotlinx/datetime/LocalDateTime;
398398
}
399399

400-
public final class kotlinx/datetime/LocalTime : java/lang/Comparable {
400+
public final class kotlinx/datetime/LocalTime : java/io/Serializable, java/lang/Comparable {
401401
public static final field Companion Lkotlinx/datetime/LocalTime$Companion;
402402
public fun <init> (IIII)V
403403
public synthetic fun <init> (IIIIILkotlin/jvm/internal/DefaultConstructorMarker;)V
@@ -474,6 +474,21 @@ public final class kotlinx/datetime/MonthKt {
474474
public static final fun getNumber (Lkotlinx/datetime/Month;)I
475475
}
476476

477+
public final class kotlinx/datetime/Ser : java/io/Externalizable {
478+
public static final field Companion Lkotlinx/datetime/Ser$Companion;
479+
public static final field DATE_TAG I
480+
public static final field DATE_TIME_TAG I
481+
public static final field TIME_TAG I
482+
public static final field UTC_OFFSET_TAG I
483+
public fun <init> ()V
484+
public fun <init> (ILjava/lang/Object;)V
485+
public fun readExternal (Ljava/io/ObjectInput;)V
486+
public fun writeExternal (Ljava/io/ObjectOutput;)V
487+
}
488+
489+
public final class kotlinx/datetime/Ser$Companion {
490+
}
491+
477492
public class kotlinx/datetime/TimeZone {
478493
public static final field Companion Lkotlinx/datetime/TimeZone$Companion;
479494
public fun equals (Ljava/lang/Object;)Z
@@ -501,7 +516,7 @@ public final class kotlinx/datetime/TimeZoneKt {
501516
public static final fun toLocalDateTime (Lkotlinx/datetime/Instant;Lkotlinx/datetime/TimeZone;)Lkotlinx/datetime/LocalDateTime;
502517
}
503518

504-
public final class kotlinx/datetime/UtcOffset {
519+
public final class kotlinx/datetime/UtcOffset : java/io/Serializable {
505520
public static final field Companion Lkotlinx/datetime/UtcOffset$Companion;
506521
public fun <init> (Ljava/time/ZoneOffset;)V
507522
public fun equals (Ljava/lang/Object;)Z

core/jvm/src/Instant.kt

+3-1
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,9 @@ import java.time.Instant as jtInstant
2020
import java.time.Clock as jtClock
2121

2222
@Serializable(with = InstantIso8601Serializer::class)
23-
public actual class Instant internal constructor(internal val value: jtInstant) : Comparable<Instant> {
23+
public actual class Instant internal constructor(
24+
internal val value: jtInstant
25+
) : Comparable<Instant> {
2426

2527
public actual val epochSeconds: Long
2628
get() = value.epochSecond

core/jvm/src/LocalDate.kt

+5-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,9 @@ import java.time.LocalDate as jtLocalDate
1818
import kotlin.internal.*
1919

2020
@Serializable(with = LocalDateIso8601Serializer::class)
21-
public actual class LocalDate internal constructor(internal val value: jtLocalDate) : Comparable<LocalDate> {
21+
public actual class LocalDate internal constructor(
22+
internal val value: jtLocalDate
23+
) : Comparable<LocalDate>, java.io.Serializable {
2224
public actual companion object {
2325
public actual fun parse(input: CharSequence, format: DateTimeFormat<LocalDate>): LocalDate =
2426
if (format === Formats.ISO) {
@@ -100,6 +102,8 @@ public actual class LocalDate internal constructor(internal val value: jtLocalDa
100102
@PublishedApi
101103
@JvmName("toEpochDays")
102104
internal fun toEpochDaysJvm(): Int = value.toEpochDay().clampToInt()
105+
106+
private fun writeReplace(): Any = Ser(Ser.DATE_TAG, this)
103107
}
104108

105109
/**

core/jvm/src/LocalDateTimeJvm.kt

+4-1
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,9 @@ import java.time.format.DateTimeParseException
1515
import java.time.LocalDateTime as jtLocalDateTime
1616

1717
@Serializable(with = LocalDateTimeIso8601Serializer::class)
18-
public actual class LocalDateTime internal constructor(internal val value: jtLocalDateTime) : Comparable<LocalDateTime> {
18+
public actual class LocalDateTime internal constructor(
19+
internal val value: jtLocalDateTime
20+
) : Comparable<LocalDateTime>, java.io.Serializable {
1921

2022
public actual constructor(year: Int, month: Int, day: Int, hour: Int, minute: Int, second: Int, nanosecond: Int) :
2123
this(try {
@@ -110,6 +112,7 @@ public actual class LocalDateTime internal constructor(internal val value: jtLoc
110112
public actual val ISO: DateTimeFormat<LocalDateTime> = ISO_DATETIME
111113
}
112114

115+
private fun writeReplace(): Any = Ser(Ser.DATE_TIME_TAG, this)
113116
}
114117

115118
/**

core/jvm/src/LocalTimeJvm.kt

+5-2
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,9 @@ import java.time.format.DateTimeParseException
1616
import java.time.LocalTime as jtLocalTime
1717

1818
@Serializable(with = LocalTimeIso8601Serializer::class)
19-
public actual class LocalTime internal constructor(internal val value: jtLocalTime) :
20-
Comparable<LocalTime> {
19+
public actual class LocalTime internal constructor(
20+
internal val value: jtLocalTime
21+
) : Comparable<LocalTime>, java.io.Serializable {
2122

2223
public actual constructor(hour: Int, minute: Int, second: Int, nanosecond: Int) :
2324
this(
@@ -90,6 +91,8 @@ public actual class LocalTime internal constructor(internal val value: jtLocalTi
9091
public actual val ISO: DateTimeFormat<LocalTime> get() = ISO_TIME
9192

9293
}
94+
95+
private fun writeReplace(): Any = Ser(Ser.TIME_TAG, this)
9396
}
9497

9598
@Deprecated(

core/jvm/src/UtcOffsetJvm.kt

+5-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@ import java.time.format.DateTimeFormatterBuilder
1414
import java.time.format.*
1515

1616
@Serializable(with = UtcOffsetSerializer::class)
17-
public actual class UtcOffset(internal val zoneOffset: ZoneOffset) {
17+
public actual class UtcOffset(
18+
internal val zoneOffset: ZoneOffset
19+
): java.io.Serializable {
1820
public actual val totalSeconds: Int get() = zoneOffset.totalSeconds
1921

2022
override fun hashCode(): Int = zoneOffset.hashCode()
@@ -44,6 +46,8 @@ public actual class UtcOffset(internal val zoneOffset: ZoneOffset) {
4446
public actual val ISO_BASIC: DateTimeFormat<UtcOffset> get() = ISO_OFFSET_BASIC
4547
public actual val FOUR_DIGITS: DateTimeFormat<UtcOffset> get() = FOUR_DIGIT_OFFSET
4648
}
49+
50+
private fun writeReplace(): Any = Ser(Ser.UTC_OFFSET_TAG, this)
4751
}
4852

4953
@Suppress("ACTUAL_FUNCTION_WITH_DEFAULT_ARGUMENTS")

core/jvm/src/internal/Ser.kt

+67
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
/*
2+
* Copyright 2019-2024 JetBrains s.r.o. and contributors.
3+
* Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file.
4+
*/
5+
6+
@file:Suppress("PackageDirectoryMismatch")
7+
package kotlinx.datetime
8+
9+
import java.io.*
10+
11+
@PublishedApi // changing the class name would result in serialization incompatibility
12+
internal class Ser(private var typeTag: Int, private var value: Any?) : Externalizable {
13+
constructor() : this(0, null)
14+
15+
override fun writeExternal(out: ObjectOutput) {
16+
out.writeByte(typeTag)
17+
val value = this.value
18+
when (typeTag) {
19+
DATE_TAG -> {
20+
value as LocalDate
21+
out.writeLong(value.value.toEpochDay())
22+
}
23+
TIME_TAG -> {
24+
value as LocalTime
25+
out.writeLong(value.toNanosecondOfDay())
26+
}
27+
DATE_TIME_TAG -> {
28+
value as LocalDateTime
29+
out.writeLong(value.date.value.toEpochDay())
30+
out.writeLong(value.time.toNanosecondOfDay())
31+
}
32+
UTC_OFFSET_TAG -> {
33+
value as UtcOffset
34+
out.writeInt(value.totalSeconds)
35+
}
36+
else -> throw IllegalStateException("Unknown type tag: $typeTag for value: $value")
37+
}
38+
}
39+
40+
override fun readExternal(`in`: ObjectInput) {
41+
typeTag = `in`.readByte().toInt()
42+
value = when (typeTag) {
43+
DATE_TAG ->
44+
LocalDate(java.time.LocalDate.ofEpochDay(`in`.readLong()))
45+
TIME_TAG ->
46+
LocalTime.fromNanosecondOfDay(`in`.readLong())
47+
DATE_TIME_TAG ->
48+
LocalDateTime(
49+
LocalDate(java.time.LocalDate.ofEpochDay(`in`.readLong())),
50+
LocalTime.fromNanosecondOfDay(`in`.readLong())
51+
)
52+
UTC_OFFSET_TAG ->
53+
UtcOffset(seconds = `in`.readInt())
54+
else -> throw IOException("Unknown type tag: $typeTag")
55+
}
56+
}
57+
58+
private fun readResolve(): Any = value!!
59+
60+
companion object {
61+
private const val serialVersionUID: Long = 0L
62+
const val DATE_TAG = 2
63+
const val TIME_TAG = 3
64+
const val DATE_TIME_TAG = 4
65+
const val UTC_OFFSET_TAG = 10
66+
}
67+
}

core/jvm/test/JvmSerializationTest.kt

+87
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
/*
2+
* Copyright 2019-2024 JetBrains s.r.o. and contributors.
3+
* Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file.
4+
*/
5+
6+
package kotlinx.datetime
7+
8+
import java.io.*
9+
import kotlin.test.*
10+
11+
class JvmSerializationTest {
12+
13+
@Test
14+
fun serializeLocalTime() {
15+
roundTripSerialization(LocalTime(12, 34, 56, 789))
16+
roundTripSerialization(LocalTime.MIN)
17+
roundTripSerialization(LocalTime.MAX)
18+
expectedDeserialization(LocalTime(23, 59, 15, 995_003_220), "090300004e8a52680954")
19+
}
20+
21+
@Test
22+
fun serializeLocalDate() {
23+
roundTripSerialization(LocalDate(2022, 1, 23))
24+
roundTripSerialization(LocalDate.MIN)
25+
roundTripSerialization(LocalDate.MAX)
26+
expectedDeserialization(LocalDate(2024, 8, 12), "09020000000000004deb")
27+
}
28+
29+
@Test
30+
fun serializeLocalDateTime() {
31+
roundTripSerialization(LocalDateTime(2022, 1, 23, 21, 35, 53, 125_123_612))
32+
roundTripSerialization(LocalDateTime.MIN)
33+
roundTripSerialization(LocalDateTime.MAX)
34+
expectedDeserialization(LocalDateTime(2024, 8, 12, 10, 15, 0, 997_665_331), "11040000000000004deb0000218faedb9233")
35+
}
36+
37+
@Test
38+
fun serializeUtcOffset() {
39+
roundTripSerialization(UtcOffset(hours = 3, minutes = 30, seconds = 15))
40+
roundTripSerialization(UtcOffset(java.time.ZoneOffset.MIN))
41+
roundTripSerialization(UtcOffset(java.time.ZoneOffset.MAX))
42+
expectedDeserialization(UtcOffset.parse("-04:15:30"), "050affffc41e")
43+
}
44+
45+
@Test
46+
fun serializeTimeZone() {
47+
assertFailsWith<NotSerializableException> {
48+
roundTripSerialization(TimeZone.of("Europe/Moscow"))
49+
}
50+
}
51+
52+
private fun serialize(value: Any?): ByteArray {
53+
val bos = ByteArrayOutputStream()
54+
val oos = ObjectOutputStream(bos)
55+
oos.writeObject(value)
56+
return bos.toByteArray()
57+
}
58+
59+
private fun deserialize(serialized: ByteArray): Any? {
60+
val bis = ByteArrayInputStream(serialized)
61+
ObjectInputStream(bis).use { ois ->
62+
return ois.readObject()
63+
}
64+
}
65+
66+
private fun <T> roundTripSerialization(value: T) {
67+
val serialized = serialize(value)
68+
val deserialized = deserialize(serialized)
69+
assertEquals(value, deserialized)
70+
}
71+
72+
@OptIn(ExperimentalStdlibApi::class)
73+
private fun expectedDeserialization(expected: Any, blockData: String) {
74+
val serialized = "aced0005737200146b6f746c696e782e6461746574696d652e53657200000000000000000c0000787077${blockData}78"
75+
val hexFormat = HexFormat { bytes.byteSeparator = "" }
76+
77+
try {
78+
val deserialized = deserialize(serialized.hexToByteArray(hexFormat))
79+
if (expected != deserialized) {
80+
assertEquals(expected, deserialized, "Golden serial form: $serialized\nActual serial form: ${serialize(expected).toHexString(hexFormat)}")
81+
}
82+
} catch (e: Throwable) {
83+
fail("Failed to deserialize $serialized\nActual serial form: ${serialize(expected).toHexString(hexFormat)}", e)
84+
}
85+
}
86+
87+
}

0 commit comments

Comments
 (0)