Skip to content

Commit ce1dc4a

Browse files
committed
Use externalizable replacement for serializable entities
1 parent 32c9cc0 commit ce1dc4a

File tree

7 files changed

+117
-86
lines changed

7 files changed

+117
-86
lines changed

core/jvm/src/Instant.kt

+1-17
Original file line numberDiff line numberDiff line change
@@ -99,25 +99,9 @@ public actual class Instant internal constructor(
9999

100100
internal actual val MIN: Instant = Instant(jtInstant.MIN)
101101
internal actual val MAX: Instant = Instant(jtInstant.MAX)
102-
103-
private const val serialVersionUID: Long = 1L
104-
}
105-
106-
private fun writeObject(oStream: java.io.ObjectOutputStream) {
107-
oStream.defaultWriteObject()
108-
oStream.writeObject(value.toString())
109102
}
110103

111-
private fun readObject(iStream: java.io.ObjectInputStream) {
112-
iStream.defaultReadObject()
113-
val field = this::class.java.getDeclaredField(::value.name)
114-
field.isAccessible = true
115-
field.set(this, parse(iStream.readObject() as String).value)
116-
}
117-
118-
private fun readObjectNoData() {
119-
throw java.io.InvalidObjectException("Stream data required")
120-
}
104+
private fun writeReplace(): Any = SerializedValue(SerializedValue.INSTANT_TAG, this)
121105
}
122106

123107
private fun Instant.atZone(zone: TimeZone): java.time.ZonedDateTime = try {

core/jvm/src/LocalDate.kt

+1-17
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,6 @@ public actual class LocalDate internal constructor(
4444
@Suppress("FunctionName")
4545
public actual fun Format(block: DateTimeFormatBuilder.WithDate.() -> Unit): DateTimeFormat<LocalDate> =
4646
LocalDateFormat.build(block)
47-
48-
private const val serialVersionUID: Long = 1L
4947
}
5048

5149
public actual object Formats {
@@ -81,21 +79,7 @@ public actual class LocalDate internal constructor(
8179

8280
public actual fun toEpochDays(): Int = value.toEpochDay().clampToInt()
8381

84-
private fun writeObject(oStream: java.io.ObjectOutputStream) {
85-
oStream.defaultWriteObject()
86-
oStream.writeObject(value.toString())
87-
}
88-
89-
private fun readObject(iStream: java.io.ObjectInputStream) {
90-
iStream.defaultReadObject()
91-
val field = this::class.java.getDeclaredField(::value.name)
92-
field.isAccessible = true
93-
field.set(this, jtLocalDate.parse(iStream.readObject() as String))
94-
}
95-
96-
private fun readObjectNoData() {
97-
throw java.io.InvalidObjectException("Stream data required")
98-
}
82+
private fun writeReplace(): Any = SerializedValue(SerializedValue.DATE_TAG, this)
9983
}
10084

10185
@Deprecated("Use the plus overload with an explicit number of units", ReplaceWith("this.plus(1, unit)"))

core/jvm/src/LocalDateTime.kt

+2-17
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
package kotlinx.datetime
77

88
import kotlinx.datetime.format.*
9+
import kotlinx.datetime.internal.SerializedValue
910
import kotlinx.datetime.serializers.LocalDateTimeIso8601Serializer
1011
import kotlinx.serialization.Serializable
1112
import java.time.DateTimeException
@@ -80,28 +81,12 @@ public actual class LocalDateTime internal constructor(
8081
@Suppress("FunctionName")
8182
public actual fun Format(builder: DateTimeFormatBuilder.WithDateTime.() -> Unit): DateTimeFormat<LocalDateTime> =
8283
LocalDateTimeFormat.build(builder)
83-
84-
private const val serialVersionUID: Long = 1L
8584
}
8685

8786
public actual object Formats {
8887
public actual val ISO: DateTimeFormat<LocalDateTime> = ISO_DATETIME
8988
}
9089

91-
private fun writeObject(oStream: java.io.ObjectOutputStream) {
92-
oStream.defaultWriteObject()
93-
oStream.writeObject(value.toString())
94-
}
95-
96-
private fun readObject(iStream: java.io.ObjectInputStream) {
97-
iStream.defaultReadObject()
98-
val field = this::class.java.getDeclaredField(::value.name)
99-
field.isAccessible = true
100-
field.set(this, jtLocalDateTime.parse(iStream.readObject() as String))
101-
}
102-
103-
private fun readObjectNoData() {
104-
throw java.io.InvalidObjectException("Stream data required")
105-
}
90+
private fun writeReplace(): Any = SerializedValue(SerializedValue.DATE_TIME_TAG, this)
10691
}
10792

core/jvm/src/LocalTime.kt

+1-17
Original file line numberDiff line numberDiff line change
@@ -85,28 +85,12 @@ public actual class LocalTime internal constructor(
8585
@Suppress("FunctionName")
8686
public actual fun Format(builder: DateTimeFormatBuilder.WithTime.() -> Unit): DateTimeFormat<LocalTime> =
8787
LocalTimeFormat.build(builder)
88-
89-
private const val serialVersionUID: Long = 1L
9088
}
9189

9290
public actual object Formats {
9391
public actual val ISO: DateTimeFormat<LocalTime> get() = ISO_TIME
9492

9593
}
9694

97-
private fun writeObject(oStream: java.io.ObjectOutputStream) {
98-
oStream.defaultWriteObject()
99-
oStream.writeObject(value.toString())
100-
}
101-
102-
private fun readObject(iStream: java.io.ObjectInputStream) {
103-
iStream.defaultReadObject()
104-
val field = this::class.java.getDeclaredField(::value.name)
105-
field.isAccessible = true
106-
field.set(this, jtLocalTime.parse(iStream.readObject() as String))
107-
}
108-
109-
private fun readObjectNoData() {
110-
throw java.io.InvalidObjectException("Stream data required")
111-
}
95+
private fun writeReplace(): Any = SerializedValue(SerializedValue.TIME_TAG, this)
11296
}

core/jvm/src/UtcOffsetJvm.kt

+2-15
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
package kotlinx.datetime
77

88
import kotlinx.datetime.format.*
9+
import kotlinx.datetime.internal.SerializedValue
910
import kotlinx.datetime.serializers.UtcOffsetSerializer
1011
import kotlinx.serialization.Serializable
1112
import java.time.DateTimeException
@@ -47,21 +48,7 @@ public actual class UtcOffset(
4748
public actual val FOUR_DIGITS: DateTimeFormat<UtcOffset> get() = FOUR_DIGIT_OFFSET
4849
}
4950

50-
private fun writeObject(oStream: java.io.ObjectOutputStream) {
51-
oStream.defaultWriteObject()
52-
oStream.writeObject(zoneOffset.toString())
53-
}
54-
55-
private fun readObject(iStream: java.io.ObjectInputStream) {
56-
iStream.defaultReadObject()
57-
val field = this::class.java.getDeclaredField(::zoneOffset.name)
58-
field.isAccessible = true
59-
field.set(this, ZoneOffset.of(iStream.readObject() as String))
60-
}
61-
62-
private fun readObjectNoData() {
63-
throw java.io.InvalidObjectException("Stream data required")
64-
}
51+
private fun writeReplace(): Any = SerializedValue(SerializedValue.UTC_OFFSET_TAG, this)
6552
}
6653

6754
@Suppress("ACTUAL_FUNCTION_WITH_DEFAULT_ARGUMENTS")
+74
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
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.internal
7+
8+
import kotlinx.datetime.*
9+
import java.io.*
10+
11+
internal class SerializedValue(var typeTag: Int, var value: Any?) : Externalizable {
12+
constructor() : this(0, null)
13+
14+
override fun writeExternal(out: ObjectOutput) {
15+
out.writeByte(typeTag)
16+
val value = this.value
17+
when (typeTag) {
18+
INSTANT_TAG -> {
19+
value as Instant
20+
out.writeLong(value.epochSeconds)
21+
out.writeInt(value.nanosecondsOfSecond)
22+
}
23+
DATE_TAG -> {
24+
value as LocalDate
25+
out.writeLong(value.value.toEpochDay())
26+
}
27+
TIME_TAG -> {
28+
value as LocalTime
29+
out.writeLong(value.toNanosecondOfDay())
30+
}
31+
DATE_TIME_TAG -> {
32+
value as LocalDateTime
33+
out.writeLong(value.date.value.toEpochDay())
34+
out.writeLong(value.time.toNanosecondOfDay())
35+
}
36+
UTC_OFFSET_TAG -> {
37+
value as UtcOffset
38+
out.writeInt(value.totalSeconds)
39+
}
40+
else -> throw IllegalStateException("Unknown type tag: $typeTag for value: $value")
41+
}
42+
}
43+
44+
override fun readExternal(`in`: ObjectInput) {
45+
typeTag = `in`.readByte().toInt()
46+
value = when (typeTag) {
47+
INSTANT_TAG ->
48+
Instant.fromEpochSeconds(`in`.readLong(), `in`.readInt())
49+
DATE_TAG ->
50+
LocalDate(java.time.LocalDate.ofEpochDay(`in`.readLong()))
51+
TIME_TAG ->
52+
LocalTime.fromNanosecondOfDay(`in`.readLong())
53+
DATE_TIME_TAG ->
54+
LocalDateTime(
55+
LocalDate(java.time.LocalDate.ofEpochDay(`in`.readLong())),
56+
LocalTime.fromNanosecondOfDay(`in`.readLong())
57+
)
58+
UTC_OFFSET_TAG ->
59+
UtcOffset(seconds = `in`.readInt())
60+
else -> throw IOException("Unknown type tag: $typeTag")
61+
}
62+
}
63+
64+
private fun readResolve(): Any = value!!
65+
66+
companion object {
67+
private const val serialVersionUID: Long = 0L
68+
const val INSTANT_TAG = 1
69+
const val DATE_TAG = 2
70+
const val TIME_TAG = 3
71+
const val DATE_TIME_TAG = 4
72+
const val UTC_OFFSET_TAG = 10
73+
}
74+
}

core/jvm/test/JvmSerializationTest.kt

+36-3
Original file line numberDiff line numberDiff line change
@@ -13,21 +13,33 @@ class JvmSerializationTest {
1313
@Test
1414
fun serializeInstant() {
1515
roundTripSerialization(Instant.fromEpochSeconds(1234567890, 123456789))
16+
roundTripSerialization(Instant.MIN)
17+
roundTripSerialization(Instant.MAX)
18+
expectedDeserialization(Instant.parse("+150000-04-30T12:30:25.555998Z"), "0d010000043fa44d82612123db30")
1619
}
1720

1821
@Test
1922
fun serializeLocalTime() {
2023
roundTripSerialization(LocalTime(12, 34, 56, 789))
24+
roundTripSerialization(LocalTime.MIN)
25+
roundTripSerialization(LocalTime.MAX)
26+
expectedDeserialization(LocalTime(23, 59, 15, 995_003_220), "090300004e8a52680954")
2127
}
2228

2329
@Test
2430
fun serializeLocalDateTime() {
2531
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")
2635
}
2736

2837
@Test
2938
fun serializeUtcOffset() {
3039
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")
3143
}
3244

3345
@Test
@@ -37,14 +49,35 @@ class JvmSerializationTest {
3749
}
3850
}
3951

40-
private fun <T> roundTripSerialization(value: T) {
52+
private fun serialize(value: Any?): ByteArray {
4153
val bos = ByteArrayOutputStream()
4254
val oos = ObjectOutputStream(bos)
4355
oos.writeObject(value)
44-
val serialized = bos.toByteArray()
56+
return bos.toByteArray()
57+
}
58+
59+
private fun deserialize(serialized: ByteArray): Any? {
4560
val bis = ByteArrayInputStream(serialized)
4661
ObjectInputStream(bis).use { ois ->
47-
assertEquals(value, ois.readObject())
62+
return ois.readObject()
4863
}
4964
}
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 = "aced0005737200296b6f746c696e782e6461746574696d652e696e7465726e616c2e53657269616c697a656456616c756500000000000000000c0000787077${blockData}78"
75+
val hexFormat = HexFormat { bytes.byteSeparator = "" }
76+
77+
val deserialized = deserialize(serialized.hexToByteArray(hexFormat))
78+
if (expected != deserialized) {
79+
assertEquals(expected, deserialized, "Golden serial form: $serialized\nActual serial form: ${serialize(expected).toHexString(hexFormat)}")
80+
}
81+
}
82+
5083
}

0 commit comments

Comments
 (0)