Skip to content

Commit

Permalink
Implement java.io.Serializable for some of the classes
Browse files Browse the repository at this point in the history
Implement java.io.Serializable for
* Instant
* 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.

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
  • Loading branch information
dkhalanskyjb committed May 23, 2024
1 parent bc8adee commit 94c5957
Show file tree
Hide file tree
Showing 6 changed files with 158 additions and 6 deletions.
23 changes: 22 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 @@ -97,6 +99,25 @@ 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)

@JvmStatic
private 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, jtOffsetDateTime.parse(fixOffsetRepresentation(iStream.readObject() as String)).toInstant())
}

private fun readObjectNoData() {
throw java.io.InvalidObjectException("Stream data required")
}
}

Expand Down
23 changes: 22 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 All @@ -42,6 +44,9 @@ public actual class LocalDate internal constructor(internal val value: jtLocalDa
@Suppress("FunctionName")
public actual fun Format(block: DateTimeFormatBuilder.WithDate.() -> Unit): DateTimeFormat<LocalDate> =
LocalDateFormat.build(block)

@JvmStatic
private val serialVersionUID: Long = 1L
}

public actual object Formats {
Expand Down Expand Up @@ -76,6 +81,22 @@ 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 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")
}
}

@Deprecated("Use the plus overload with an explicit number of units", ReplaceWith("this.plus(1, unit)"))
Expand Down
23 changes: 22 additions & 1 deletion core/jvm/src/LocalDateTime.kt
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,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 @@ -77,11 +80,29 @@ public actual class LocalDateTime internal constructor(internal val value: jtLoc
@Suppress("FunctionName")
public actual fun Format(builder: DateTimeFormatBuilder.WithDateTime.() -> Unit): DateTimeFormat<LocalDateTime> =
LocalDateTimeFormat.build(builder)

@JvmStatic
private 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")
}
}

25 changes: 23 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 @@ -83,10 +85,29 @@ public actual class LocalTime internal constructor(internal val value: jtLocalTi
@Suppress("FunctionName")
public actual fun Format(builder: DateTimeFormatBuilder.WithTime.() -> Unit): DateTimeFormat<LocalTime> =
LocalTimeFormat.build(builder)

@JvmStatic
private 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")
}
}
20 changes: 19 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,22 @@ 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 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")
}
}

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

@Test
fun serializeLocalTime() {
roundTripSerialization(LocalTime(12, 34, 56, 789))
}

@Test
fun serializeLocalDateTime() {
roundTripSerialization(LocalDateTime(2022, 1, 23, 21, 35, 53, 125_123_612))
}

@Test
fun serializeUtcOffset() {
roundTripSerialization(UtcOffset(hours = 3, minutes = 30, seconds = 15))
}

@Test
fun serializeTimeZone() {
assertFailsWith<NotSerializableException> {
roundTripSerialization(TimeZone.of("Europe/Moscow"))
}
}

private fun <T> roundTripSerialization(value: T) {
val bos = ByteArrayOutputStream()
val oos = ObjectOutputStream(bos)
oos.writeObject(value)
val serialized = bos.toByteArray()
val bis = ByteArrayInputStream(serialized)
ObjectInputStream(bis).use { ois ->
assertEquals(value, ois.readObject())
}
}
}

0 comments on commit 94c5957

Please sign in to comment.