Skip to content

Commit

Permalink
Use externalizable replacement for serializable entities
Browse files Browse the repository at this point in the history
  • Loading branch information
ilya-g committed Aug 15, 2024
1 parent 32c9cc0 commit 42fa2a5
Show file tree
Hide file tree
Showing 7 changed files with 125 additions and 86 deletions.
18 changes: 1 addition & 17 deletions core/jvm/src/Instant.kt
Original file line number Diff line number Diff line change
Expand Up @@ -99,25 +99,9 @@ public actual class Instant internal constructor(

internal actual val MIN: Instant = Instant(jtInstant.MIN)
internal actual val MAX: Instant = Instant(jtInstant.MAX)

private const val serialVersionUID: Long = 1L
}

private fun writeObject(oStream: java.io.ObjectOutputStream) {
oStream.defaultWriteObject()
oStream.writeObject(value.toString())
}

private fun readObject(iStream: java.io.ObjectInputStream) {
iStream.defaultReadObject()
val field = this::class.java.getDeclaredField(::value.name)
field.isAccessible = true
field.set(this, parse(iStream.readObject() as String).value)
}

private fun readObjectNoData() {
throw java.io.InvalidObjectException("Stream data required")
}
private fun writeReplace(): Any = SerializedValue(SerializedValue.INSTANT_TAG, this)
}

private fun Instant.atZone(zone: TimeZone): java.time.ZonedDateTime = try {
Expand Down
18 changes: 1 addition & 17 deletions core/jvm/src/LocalDate.kt
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,6 @@ public actual class LocalDate internal constructor(
@Suppress("FunctionName")
public actual fun Format(block: DateTimeFormatBuilder.WithDate.() -> Unit): DateTimeFormat<LocalDate> =
LocalDateFormat.build(block)

private const val serialVersionUID: Long = 1L
}

public actual object Formats {
Expand Down Expand Up @@ -81,21 +79,7 @@ public actual class LocalDate internal constructor(

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

private fun writeObject(oStream: java.io.ObjectOutputStream) {
oStream.defaultWriteObject()
oStream.writeObject(value.toString())
}

private fun readObject(iStream: java.io.ObjectInputStream) {
iStream.defaultReadObject()
val field = this::class.java.getDeclaredField(::value.name)
field.isAccessible = true
field.set(this, jtLocalDate.parse(iStream.readObject() as String))
}

private fun readObjectNoData() {
throw java.io.InvalidObjectException("Stream data required")
}
private fun writeReplace(): Any = SerializedValue(SerializedValue.DATE_TAG, this)
}

@Deprecated("Use the plus overload with an explicit number of units", ReplaceWith("this.plus(1, unit)"))
Expand Down
19 changes: 2 additions & 17 deletions core/jvm/src/LocalDateTime.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
package kotlinx.datetime

import kotlinx.datetime.format.*
import kotlinx.datetime.internal.SerializedValue
import kotlinx.datetime.serializers.LocalDateTimeIso8601Serializer
import kotlinx.serialization.Serializable
import java.time.DateTimeException
Expand Down Expand Up @@ -80,28 +81,12 @@ public actual class LocalDateTime internal constructor(
@Suppress("FunctionName")
public actual fun Format(builder: DateTimeFormatBuilder.WithDateTime.() -> Unit): DateTimeFormat<LocalDateTime> =
LocalDateTimeFormat.build(builder)

private const val serialVersionUID: Long = 1L
}

public actual object Formats {
public actual val ISO: DateTimeFormat<LocalDateTime> = ISO_DATETIME
}

private fun writeObject(oStream: java.io.ObjectOutputStream) {
oStream.defaultWriteObject()
oStream.writeObject(value.toString())
}

private fun readObject(iStream: java.io.ObjectInputStream) {
iStream.defaultReadObject()
val field = this::class.java.getDeclaredField(::value.name)
field.isAccessible = true
field.set(this, jtLocalDateTime.parse(iStream.readObject() as String))
}

private fun readObjectNoData() {
throw java.io.InvalidObjectException("Stream data required")
}
private fun writeReplace(): Any = SerializedValue(SerializedValue.DATE_TIME_TAG, this)
}

18 changes: 1 addition & 17 deletions core/jvm/src/LocalTime.kt
Original file line number Diff line number Diff line change
Expand Up @@ -85,28 +85,12 @@ public actual class LocalTime internal constructor(
@Suppress("FunctionName")
public actual fun Format(builder: DateTimeFormatBuilder.WithTime.() -> Unit): DateTimeFormat<LocalTime> =
LocalTimeFormat.build(builder)

private const val serialVersionUID: Long = 1L
}

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

}

private fun writeObject(oStream: java.io.ObjectOutputStream) {
oStream.defaultWriteObject()
oStream.writeObject(value.toString())
}

private fun readObject(iStream: java.io.ObjectInputStream) {
iStream.defaultReadObject()
val field = this::class.java.getDeclaredField(::value.name)
field.isAccessible = true
field.set(this, jtLocalTime.parse(iStream.readObject() as String))
}

private fun readObjectNoData() {
throw java.io.InvalidObjectException("Stream data required")
}
private fun writeReplace(): Any = SerializedValue(SerializedValue.TIME_TAG, this)
}
17 changes: 2 additions & 15 deletions core/jvm/src/UtcOffsetJvm.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
package kotlinx.datetime

import kotlinx.datetime.format.*
import kotlinx.datetime.internal.SerializedValue
import kotlinx.datetime.serializers.UtcOffsetSerializer
import kotlinx.serialization.Serializable
import java.time.DateTimeException
Expand Down Expand Up @@ -47,21 +48,7 @@ public actual class UtcOffset(
public actual val FOUR_DIGITS: DateTimeFormat<UtcOffset> get() = FOUR_DIGIT_OFFSET
}

private fun writeObject(oStream: java.io.ObjectOutputStream) {
oStream.defaultWriteObject()
oStream.writeObject(zoneOffset.toString())
}

private fun readObject(iStream: java.io.ObjectInputStream) {
iStream.defaultReadObject()
val field = this::class.java.getDeclaredField(::zoneOffset.name)
field.isAccessible = true
field.set(this, ZoneOffset.of(iStream.readObject() as String))
}

private fun readObjectNoData() {
throw java.io.InvalidObjectException("Stream data required")
}
private fun writeReplace(): Any = SerializedValue(SerializedValue.UTC_OFFSET_TAG, this)
}

@Suppress("ACTUAL_FUNCTION_WITH_DEFAULT_ARGUMENTS")
Expand Down
74 changes: 74 additions & 0 deletions core/jvm/src/internal/SerializedValue.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/*
* Copyright 2019-2024 JetBrains s.r.o. and contributors.
* Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file.
*/

package kotlinx.datetime.internal

import kotlinx.datetime.*
import java.io.*

internal class SerializedValue(var typeTag: Int, var value: Any?) : Externalizable {
constructor() : this(0, null)

override fun writeExternal(out: ObjectOutput) {
out.writeByte(typeTag)
val value = this.value
when (typeTag) {
INSTANT_TAG -> {
value as Instant
out.writeLong(value.epochSeconds)
out.writeInt(value.nanosecondsOfSecond)
}
DATE_TAG -> {
value as LocalDate
out.writeLong(value.value.toEpochDay())
}
TIME_TAG -> {
value as LocalTime
out.writeLong(value.toNanosecondOfDay())
}
DATE_TIME_TAG -> {
value as LocalDateTime
out.writeLong(value.date.value.toEpochDay())
out.writeLong(value.time.toNanosecondOfDay())
}
UTC_OFFSET_TAG -> {
value as UtcOffset
out.writeInt(value.totalSeconds)
}
else -> throw IllegalStateException("Unknown type tag: $typeTag for value: $value")
}
}

override fun readExternal(`in`: ObjectInput) {
typeTag = `in`.readByte().toInt()
value = when (typeTag) {
INSTANT_TAG ->
Instant.fromEpochSeconds(`in`.readLong(), `in`.readInt())
DATE_TAG ->
LocalDate(java.time.LocalDate.ofEpochDay(`in`.readLong()))
TIME_TAG ->
LocalTime.fromNanosecondOfDay(`in`.readLong())
DATE_TIME_TAG ->
LocalDateTime(
LocalDate(java.time.LocalDate.ofEpochDay(`in`.readLong())),
LocalTime.fromNanosecondOfDay(`in`.readLong())
)
UTC_OFFSET_TAG ->
UtcOffset(seconds = `in`.readInt())
else -> throw IOException("Unknown type tag: $typeTag")
}
}

private fun readResolve(): Any = value!!

companion object {
private const val serialVersionUID: Long = 0L
const val INSTANT_TAG = 1
const val DATE_TAG = 2
const val TIME_TAG = 3
const val DATE_TIME_TAG = 4
const val UTC_OFFSET_TAG = 10
}
}
47 changes: 44 additions & 3 deletions core/jvm/test/JvmSerializationTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -13,21 +13,41 @@ class JvmSerializationTest {
@Test
fun serializeInstant() {
roundTripSerialization(Instant.fromEpochSeconds(1234567890, 123456789))
roundTripSerialization(Instant.MIN)
roundTripSerialization(Instant.MAX)
expectedDeserialization(Instant.parse("+150000-04-30T12:30:25.555998Z"), "0d010000043fa44d82612123db30")
}

@Test
fun serializeLocalTime() {
roundTripSerialization(LocalTime(12, 34, 56, 789))
roundTripSerialization(LocalTime.MIN)
roundTripSerialization(LocalTime.MAX)
expectedDeserialization(LocalTime(23, 59, 15, 995_003_220), "090300004e8a52680954")
}

@Test
fun serializeLocalDate() {
roundTripSerialization(LocalDate(2022, 1, 23))
roundTripSerialization(LocalDate.MIN)
roundTripSerialization(LocalDate.MAX)
expectedDeserialization(LocalDate(2024, 8, 12), "09020000000000004deb")
}

@Test
fun serializeLocalDateTime() {
roundTripSerialization(LocalDateTime(2022, 1, 23, 21, 35, 53, 125_123_612))
roundTripSerialization(LocalDateTime.MIN)
roundTripSerialization(LocalDateTime.MAX)
expectedDeserialization(LocalDateTime(2024, 8, 12, 10, 15, 0, 997_665_331), "11040000000000004deb0000218faedb9233")
}

@Test
fun serializeUtcOffset() {
roundTripSerialization(UtcOffset(hours = 3, minutes = 30, seconds = 15))
roundTripSerialization(UtcOffset(java.time.ZoneOffset.MIN))
roundTripSerialization(UtcOffset(java.time.ZoneOffset.MAX))
expectedDeserialization(UtcOffset.parse("-04:15:30"), "050affffc41e")
}

@Test
Expand All @@ -37,14 +57,35 @@ class JvmSerializationTest {
}
}

private fun <T> roundTripSerialization(value: T) {
private fun serialize(value: Any?): ByteArray {
val bos = ByteArrayOutputStream()
val oos = ObjectOutputStream(bos)
oos.writeObject(value)
val serialized = bos.toByteArray()
return bos.toByteArray()
}

private fun deserialize(serialized: ByteArray): Any? {
val bis = ByteArrayInputStream(serialized)
ObjectInputStream(bis).use { ois ->
assertEquals(value, ois.readObject())
return ois.readObject()
}
}

private fun <T> roundTripSerialization(value: T) {
val serialized = serialize(value)
val deserialized = deserialize(serialized)
assertEquals(value, deserialized)
}

@OptIn(ExperimentalStdlibApi::class)
private fun expectedDeserialization(expected: Any, blockData: String) {
val serialized = "aced0005737200296b6f746c696e782e6461746574696d652e696e7465726e616c2e53657269616c697a656456616c756500000000000000000c0000787077${blockData}78"
val hexFormat = HexFormat { bytes.byteSeparator = "" }

val deserialized = deserialize(serialized.hexToByteArray(hexFormat))
if (expected != deserialized) {
assertEquals(expected, deserialized, "Golden serial form: $serialized\nActual serial form: ${serialize(expected).toHexString(hexFormat)}")
}
}

}

0 comments on commit 42fa2a5

Please sign in to comment.