Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement java.io.Serializable for some of the classes #373

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion core/jvm/src/Instant.kt
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@ import java.time.Instant as jtInstant
import java.time.Clock as jtClock

@Serializable(with = InstantIso8601Serializer::class)
public actual class Instant internal constructor(internal val value: jtInstant) : Comparable<Instant> {
public actual class Instant internal constructor(
internal val value: jtInstant
) : Comparable<Instant>, java.io.Serializable {

public actual val epochSeconds: Long
get() = value.epochSecond
Expand Down Expand Up @@ -98,6 +100,8 @@ public actual class Instant internal constructor(internal val value: jtInstant)
internal actual val MIN: Instant = Instant(jtInstant.MIN)
internal actual val MAX: Instant = Instant(jtInstant.MAX)
}

private fun writeReplace(): Any = Ser(Ser.INSTANT_TAG, this)
}

private fun Instant.atZone(zone: TimeZone): java.time.ZonedDateTime = try {
Expand Down
6 changes: 5 additions & 1 deletion core/jvm/src/LocalDate.kt
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@ import java.time.temporal.ChronoUnit
import java.time.LocalDate as jtLocalDate

@Serializable(with = LocalDateIso8601Serializer::class)
public actual class LocalDate internal constructor(internal val value: jtLocalDate) : Comparable<LocalDate> {
public actual class LocalDate internal constructor(
internal val value: jtLocalDate
) : Comparable<LocalDate>, java.io.Serializable {
public actual companion object {
public actual fun parse(input: CharSequence, format: DateTimeFormat<LocalDate>): LocalDate =
if (format === Formats.ISO) {
Expand Down Expand Up @@ -76,6 +78,8 @@ public actual class LocalDate internal constructor(internal val value: jtLocalDa
actual override fun compareTo(other: LocalDate): Int = this.value.compareTo(other.value)

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

private fun writeReplace(): Any = Ser(Ser.DATE_TAG, this)
}

@Deprecated("Use the plus overload with an explicit number of units", ReplaceWith("this.plus(1, unit)"))
Expand Down
6 changes: 4 additions & 2 deletions core/jvm/src/LocalDateTime.kt
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@ public actual typealias Month = java.time.Month
public actual typealias DayOfWeek = java.time.DayOfWeek

@Serializable(with = LocalDateTimeIso8601Serializer::class)
public actual class LocalDateTime internal constructor(internal val value: jtLocalDateTime) : Comparable<LocalDateTime> {
public actual class LocalDateTime internal constructor(
internal val value: jtLocalDateTime
) : Comparable<LocalDateTime>, java.io.Serializable {

public actual constructor(year: Int, monthNumber: Int, dayOfMonth: Int, hour: Int, minute: Int, second: Int, nanosecond: Int) :
this(try {
Expand Down Expand Up @@ -83,5 +85,5 @@ public actual class LocalDateTime internal constructor(internal val value: jtLoc
public actual val ISO: DateTimeFormat<LocalDateTime> = ISO_DATETIME
}

private fun writeReplace(): Any = Ser(Ser.DATE_TIME_TAG, this)
}

7 changes: 5 additions & 2 deletions core/jvm/src/LocalTime.kt
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,9 @@ import java.time.format.DateTimeParseException
import java.time.LocalTime as jtLocalTime

@Serializable(with = LocalTimeIso8601Serializer::class)
public actual class LocalTime internal constructor(internal val value: jtLocalTime) :
Comparable<LocalTime> {
public actual class LocalTime internal constructor(
internal val value: jtLocalTime
) : Comparable<LocalTime>, java.io.Serializable {

public actual constructor(hour: Int, minute: Int, second: Int, nanosecond: Int) :
this(
Expand Down Expand Up @@ -89,4 +90,6 @@ public actual class LocalTime internal constructor(internal val value: jtLocalTi
public actual val ISO: DateTimeFormat<LocalTime> get() = ISO_TIME

}

private fun writeReplace(): Any = Ser(Ser.TIME_TAG, this)
}
6 changes: 5 additions & 1 deletion core/jvm/src/UtcOffsetJvm.kt
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@ import java.time.format.DateTimeFormatterBuilder
import java.time.format.*

@Serializable(with = UtcOffsetSerializer::class)
public actual class UtcOffset(internal val zoneOffset: ZoneOffset) {
public actual class UtcOffset(
internal val zoneOffset: ZoneOffset
): java.io.Serializable {
public actual val totalSeconds: Int get() = zoneOffset.totalSeconds

override fun hashCode(): Int = zoneOffset.hashCode()
Expand Down Expand Up @@ -44,6 +46,8 @@ public actual class UtcOffset(internal val zoneOffset: ZoneOffset) {
public actual val ISO_BASIC: DateTimeFormat<UtcOffset> get() = ISO_OFFSET_BASIC
public actual val FOUR_DIGITS: DateTimeFormat<UtcOffset> get() = FOUR_DIGIT_OFFSET
}

private fun writeReplace(): Any = Ser(Ser.UTC_OFFSET_TAG, this)
}

@Suppress("ACTUAL_FUNCTION_WITH_DEFAULT_ARGUMENTS")
Expand Down
75 changes: 75 additions & 0 deletions core/jvm/src/internal/Ser.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
/*
* 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.
*/

@file:Suppress("PackageDirectoryMismatch")
package kotlinx.datetime

import java.io.*

@PublishedApi // changing the class name would result in serialization incompatibility
internal class Ser(private var typeTag: Int, private 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
}
}
95 changes: 95 additions & 0 deletions core/jvm/test/JvmSerializationTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
/*
* 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

import java.io.*
import kotlin.test.*

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
fun serializeTimeZone() {
assertFailsWith<NotSerializableException> {
roundTripSerialization(TimeZone.of("Europe/Moscow"))
}
}

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

private fun deserialize(serialized: ByteArray): Any? {
val bis = ByteArrayInputStream(serialized)
ObjectInputStream(bis).use { ois ->
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 = "aced0005737200146b6f746c696e782e6461746574696d652e53657200000000000000000c0000787077${blockData}78"
val hexFormat = HexFormat { bytes.byteSeparator = "" }

try {
val deserialized = deserialize(serialized.hexToByteArray(hexFormat))
if (expected != deserialized) {
assertEquals(expected, deserialized, "Golden serial form: $serialized\nActual serial form: ${serialize(expected).toHexString(hexFormat)}")
}
} catch (e: Throwable) {
fail("Failed to deserialize $serialized\nActual serial form: ${serialize(expected).toHexString(hexFormat)}", e)
}
}

}