-
Notifications
You must be signed in to change notification settings - Fork 101
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
base: master
Are you sure you want to change the base?
Changes from 4 commits
94c5957
f44c9a4
32c9cc0
42fa2a5
ec0662c
d6238ee
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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 { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can these values be There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
} | ||
} |
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)}") | ||
} | ||
} | ||
|
||
} |
There was a problem hiding this comment.
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.There was a problem hiding this comment.
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?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, I can do that