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 4 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 = SerializedValue(SerializedValue.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 = SerializedValue(SerializedValue.DATE_TAG, this)
}

@Deprecated("Use the plus overload with an explicit number of units", ReplaceWith("this.plus(1, unit)"))
Expand Down
7 changes: 6 additions & 1 deletion 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 All @@ -16,7 +17,10 @@ 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(
// only a `var` to allow Java deserialization
internal var 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 +87,6 @@ public actual class LocalDateTime internal constructor(internal val value: jtLoc
public actual val ISO: DateTimeFormat<LocalDateTime> = ISO_DATETIME
}

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

8 changes: 6 additions & 2 deletions core/jvm/src/LocalTime.kt
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,10 @@ 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(
// only a `var` to allow Java deserialization
internal var 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 +91,6 @@ public actual class LocalTime internal constructor(internal val value: jtLocalTi
public actual val ISO: DateTimeFormat<LocalTime> get() = ISO_TIME

}

private fun writeReplace(): Any = SerializedValue(SerializedValue.TIME_TAG, this)
}
7 changes: 6 additions & 1 deletion 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 All @@ -14,7 +15,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 +47,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 = 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 {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alternatively, we can place it in kotlinx.datetime package and name it shortly, e.g. Ser, to shave off some bytes.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you mean, shave off some bytes from the serialized representation? I'm okay with that, as long as it still stays in the internal/ directory.

Does it make sense to mark this with @PublishedApi to signal that it's an incompatible change to move/rename this class?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does it make sense to mark this with @PublishedApi to signal that it's an incompatible change to move/rename this class?

Yes, I can do that

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can these values be private?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They can, but does it makes a difference for an internal class? Or you mean in order not to generate getters/setters?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, for the latter.

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
}
}
91 changes: 91 additions & 0 deletions core/jvm/test/JvmSerializationTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
/*
* 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 = "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)}")
}
}

}