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

KTOR-7620 Make Url class @Serializable and JVM Serializable #4421

Merged
merged 2 commits into from
Nov 12, 2024
Merged
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
16 changes: 13 additions & 3 deletions ktor-http/api/ktor-http.api
Original file line number Diff line number Diff line change
Expand Up @@ -262,7 +262,7 @@ public final class io/ktor/http/ContentTypesKt {
public static final fun withCharsetIfNeeded (Lio/ktor/http/ContentType;Ljava/nio/charset/Charset;)Lio/ktor/http/ContentType;
}

public final class io/ktor/http/Cookie {
public final class io/ktor/http/Cookie : java/io/Serializable {
public static final field Companion Lio/ktor/http/Cookie$Companion;
public fun <init> (Ljava/lang/String;Ljava/lang/String;Lio/ktor/http/CookieEncoding;Ljava/lang/Integer;Lio/ktor/util/date/GMTDate;Ljava/lang/String;Ljava/lang/String;ZZLjava/util/Map;)V
public synthetic fun <init> (Ljava/lang/String;Ljava/lang/String;Lio/ktor/http/CookieEncoding;Ljava/lang/Integer;Lio/ktor/util/date/GMTDate;Ljava/lang/String;Ljava/lang/String;ZZLjava/util/Map;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
Expand Down Expand Up @@ -966,7 +966,7 @@ public final class io/ktor/http/URLParserKt {
public static final fun takeFrom (Lio/ktor/http/URLBuilder;Ljava/lang/String;)Lio/ktor/http/URLBuilder;
}

public final class io/ktor/http/URLProtocol {
public final class io/ktor/http/URLProtocol : java/io/Serializable {
public static final field Companion Lio/ktor/http/URLProtocol$Companion;
public fun <init> (Ljava/lang/String;I)V
public final fun component1 ()Ljava/lang/String;
Expand Down Expand Up @@ -1026,7 +1026,7 @@ public final class io/ktor/http/UnsafeHeaderException : java/lang/IllegalArgumen
public fun <init> (Ljava/lang/String;)V
}

public final class io/ktor/http/Url {
public final class io/ktor/http/Url : java/io/Serializable {
public static final field Companion Lio/ktor/http/Url$Companion;
public fun equals (Ljava/lang/Object;)Z
public final fun getEncodedFragment ()Ljava/lang/String;
Expand All @@ -1053,13 +1053,23 @@ public final class io/ktor/http/Url {
}

public final class io/ktor/http/Url$Companion {
public final fun serializer ()Lkotlinx/serialization/KSerializer;
}

public final class io/ktor/http/UrlKt {
public static final fun getAuthority (Lio/ktor/http/Url;)Ljava/lang/String;
public static final fun getProtocolWithAuthority (Lio/ktor/http/Url;)Ljava/lang/String;
}

public final class io/ktor/http/UrlSerializer : kotlinx/serialization/KSerializer {
public static final field INSTANCE Lio/ktor/http/UrlSerializer;
public fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/ktor/http/Url;
public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object;
public fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor;
public fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/ktor/http/Url;)V
public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V
}

public final class io/ktor/http/auth/AuthScheme {
public static final field Basic Ljava/lang/String;
public static final field Bearer Ljava/lang/String;
Expand Down
18 changes: 14 additions & 4 deletions ktor-http/api/ktor-http.klib.api
Original file line number Diff line number Diff line change
Expand Up @@ -573,7 +573,7 @@ final class io.ktor.http/ContentType : io.ktor.http/HeaderValueWithParameters {
}
}

final class io.ktor.http/Cookie { // io.ktor.http/Cookie|null[0]
final class io.ktor.http/Cookie : io.ktor.utils.io/JvmSerializable { // io.ktor.http/Cookie|null[0]
constructor <init>(kotlin/String, kotlin/String, io.ktor.http/CookieEncoding = ..., kotlin/Int? = ..., io.ktor.util.date/GMTDate? = ..., kotlin/String? = ..., kotlin/String? = ..., kotlin/Boolean = ..., kotlin/Boolean = ..., kotlin.collections/Map<kotlin/String, kotlin/String?> = ...) // io.ktor.http/Cookie.<init>|<init>(kotlin.String;kotlin.String;io.ktor.http.CookieEncoding;kotlin.Int?;io.ktor.util.date.GMTDate?;kotlin.String?;kotlin.String?;kotlin.Boolean;kotlin.Boolean;kotlin.collections.Map<kotlin.String,kotlin.String?>){}[0]

final val domain // io.ktor.http/Cookie.domain|{}domain[0]
Expand Down Expand Up @@ -1048,7 +1048,7 @@ final class io.ktor.http/URLParserException : kotlin/IllegalStateException { //
constructor <init>(kotlin/String, kotlin/Throwable) // io.ktor.http/URLParserException.<init>|<init>(kotlin.String;kotlin.Throwable){}[0]
}

final class io.ktor.http/URLProtocol { // io.ktor.http/URLProtocol|null[0]
final class io.ktor.http/URLProtocol : io.ktor.utils.io/JvmSerializable { // io.ktor.http/URLProtocol|null[0]
constructor <init>(kotlin/String, kotlin/Int) // io.ktor.http/URLProtocol.<init>|<init>(kotlin.String;kotlin.Int){}[0]

final val defaultPort // io.ktor.http/URLProtocol.defaultPort|{}defaultPort[0]
Expand Down Expand Up @@ -1085,7 +1085,7 @@ final class io.ktor.http/UnsafeHeaderException : kotlin/IllegalArgumentException
constructor <init>(kotlin/String) // io.ktor.http/UnsafeHeaderException.<init>|<init>(kotlin.String){}[0]
}

final class io.ktor.http/Url { // io.ktor.http/Url|null[0]
final class io.ktor.http/Url : io.ktor.utils.io/JvmSerializable { // io.ktor.http/Url|null[0]
final val encodedFragment // io.ktor.http/Url.encodedFragment|{}encodedFragment[0]
final fun <get-encodedFragment>(): kotlin/String // io.ktor.http/Url.encodedFragment.<get-encodedFragment>|<get-encodedFragment>(){}[0]
final val encodedPassword // io.ktor.http/Url.encodedPassword|{}encodedPassword[0]
Expand Down Expand Up @@ -1129,7 +1129,9 @@ final class io.ktor.http/Url { // io.ktor.http/Url|null[0]
final fun hashCode(): kotlin/Int // io.ktor.http/Url.hashCode|hashCode(){}[0]
final fun toString(): kotlin/String // io.ktor.http/Url.toString|toString(){}[0]

final object Companion // io.ktor.http/Url.Companion|null[0]
final object Companion { // io.ktor.http/Url.Companion|null[0]
final fun serializer(): kotlinx.serialization/KSerializer<io.ktor.http/Url> // io.ktor.http/Url.Companion.serializer|serializer(){}[0]
}
}

sealed class io.ktor.http.auth/HttpAuthHeader { // io.ktor.http.auth/HttpAuthHeader|null[0]
Expand Down Expand Up @@ -1580,6 +1582,14 @@ final object io.ktor.http/HttpHeaders { // io.ktor.http/HttpHeaders|null[0]
final fun isUnsafe(kotlin/String): kotlin/Boolean // io.ktor.http/HttpHeaders.isUnsafe|isUnsafe(kotlin.String){}[0]
}

final object io.ktor.http/UrlSerializer : kotlinx.serialization/KSerializer<io.ktor.http/Url> { // io.ktor.http/UrlSerializer|null[0]
final val descriptor // io.ktor.http/UrlSerializer.descriptor|{}descriptor[0]
final fun <get-descriptor>(): kotlinx.serialization.descriptors/SerialDescriptor // io.ktor.http/UrlSerializer.descriptor.<get-descriptor>|<get-descriptor>(){}[0]

final fun deserialize(kotlinx.serialization.encoding/Decoder): io.ktor.http/Url // io.ktor.http/UrlSerializer.deserialize|deserialize(kotlinx.serialization.encoding.Decoder){}[0]
final fun serialize(kotlinx.serialization.encoding/Encoder, io.ktor.http/Url) // io.ktor.http/UrlSerializer.serialize|serialize(kotlinx.serialization.encoding.Encoder;io.ktor.http.Url){}[0]
}

final const val io.ktor.http/DEFAULT_PORT // io.ktor.http/DEFAULT_PORT|{}DEFAULT_PORT[0]
final fun <get-DEFAULT_PORT>(): kotlin/Int // io.ktor.http/DEFAULT_PORT.<get-DEFAULT_PORT>|<get-DEFAULT_PORT>(){}[0]

Expand Down
7 changes: 7 additions & 0 deletions ktor-http/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,12 @@ kotlin {
api(libs.kotlinx.serialization.core)
}
}
jvmTest {
dependencies {
implementation(project(":ktor-shared:ktor-junit"))
implementation(project(":ktor-shared:ktor-serialization:ktor-serialization-kotlinx"))
implementation(project(":ktor-shared:ktor-serialization:ktor-serialization-kotlinx:ktor-serialization-kotlinx-json"))
}
}
}
}
15 changes: 14 additions & 1 deletion ktor-http/common/src/io/ktor/http/Cookie.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,13 @@
* Copyright 2014-2024 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license.
*/

@file:OptIn(InternalAPI::class)

package io.ktor.http

import io.ktor.util.*
import io.ktor.util.date.*
import io.ktor.utils.io.*
import kotlinx.serialization.*
import kotlin.jvm.*

Expand Down Expand Up @@ -37,7 +40,17 @@ public data class Cookie(
val secure: Boolean = false,
val httpOnly: Boolean = false,
val extensions: Map<String, String?> = emptyMap()
)
) : JvmSerializable {
private fun writeReplace(): Any = JvmSerializerReplacement(CookieJvmSerializer, this)
}

internal object CookieJvmSerializer : JvmSerializer<Cookie> {
override fun jvmSerialize(value: Cookie): ByteArray =
renderSetCookieHeader(value).encodeToByteArray()

override fun jvmDeserialize(value: ByteArray): Cookie =
parseServerSetCookieHeader(value.decodeToString())
}

/**
* Cooke encoding strategy
Expand Down
4 changes: 3 additions & 1 deletion ktor-http/common/src/io/ktor/http/URLProtocol.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,15 @@
package io.ktor.http

import io.ktor.util.*
import io.ktor.utils.io.*

/**
* Represents URL protocol
* @property name of protocol (schema)
* @property defaultPort default port for protocol or `-1` if not known
*/
public data class URLProtocol(val name: String, val defaultPort: Int) {
@OptIn(InternalAPI::class)
public data class URLProtocol(val name: String, val defaultPort: Int) : JvmSerializable {
init {
require(name.all { it.isLowerCase() }) { "All characters should be lower case" }
}
Expand Down
32 changes: 31 additions & 1 deletion ktor-http/common/src/io/ktor/http/Url.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,15 @@
* Copyright 2014-2021 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license.
*/

@file:OptIn(InternalAPI::class)

package io.ktor.http

import io.ktor.utils.io.*
import kotlinx.serialization.*
import kotlinx.serialization.descriptors.*
import kotlinx.serialization.encoding.*

/**
* Represents an immutable URL
*
Expand All @@ -18,6 +25,7 @@ package io.ktor.http
* @property password password part of URL
* @property trailingQuery keep trailing question character even if there are no query parameters
*/
@Serializable(with = UrlSerializer::class)
public class Url internal constructor(
protocol: URLProtocol?,
public val host: String,
Expand All @@ -29,7 +37,7 @@ public class Url internal constructor(
public val password: String?,
public val trailingQuery: Boolean,
private val urlString: String
) {
) : JvmSerializable {
init {
require(specifiedPort in 0..65535) {
"Port must be between 0 and 65535, or $DEFAULT_PORT if not set. Provided: $specifiedPort"
Expand Down Expand Up @@ -222,6 +230,8 @@ public class Url internal constructor(
return urlString.hashCode()
}

private fun writeReplace(): Any = JvmSerializerReplacement(UrlJvmSerializer, this)

public companion object
}

Expand Down Expand Up @@ -254,3 +264,23 @@ internal val Url.encodedUserAndPassword: String
get() = buildString {
appendUserAndPassword(encodedUser, encodedPassword)
}

public object UrlSerializer : KSerializer<Url> {
override val descriptor: SerialDescriptor =
PrimitiveSerialDescriptor("io.ktor.http.Url", PrimitiveKind.STRING)

override fun deserialize(decoder: Decoder): Url =
Url(decoder.decodeString())

override fun serialize(encoder: Encoder, value: Url) {
encoder.encodeString(value.toString())
}
}

internal object UrlJvmSerializer : JvmSerializer<Url> {
Copy link
Contributor Author

@wkornewald wkornewald Oct 27, 2024

Choose a reason for hiding this comment

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

Just an idea: To make this slightly easier to implement one could also add a reusable serializer which utilizes kotlinx-serialization (e.g. using JSON or BSON or CBOR internally).

Copy link
Member

Choose a reason for hiding this comment

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

I like this idea! Would you like to implement it in this PR or in a separate one?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

A separate one. This should rather happen in kotlinx.serialization, so it's generally reusable.

override fun jvmSerialize(value: Url): ByteArray =
value.toString().encodeToByteArray()

override fun jvmDeserialize(value: ByteArray): Url =
Url(value.decodeToString())
}
22 changes: 22 additions & 0 deletions ktor-http/jvm/test/io/ktor/tests/http/SerializableTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// ktlint-disable filename
/*
* Copyright 2014-2019 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license.
*/

package io.ktor.tests.http

import io.ktor.http.*
import io.ktor.junit.*
import kotlin.test.*

class SerializableTest {
@Test
fun urlTest() {
assertSerializable(Url("https://localhost/path?key=value#fragment"))
}

@Test
fun cookieTest() {
assertSerializable(Cookie("key", "value"))
}
}
20 changes: 20 additions & 0 deletions ktor-io/api/ktor-io.api
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,17 @@ public final class io/ktor/utils/io/CountedByteWriteChannelKt {
public static final fun counted (Lio/ktor/utils/io/ByteWriteChannel;)Lio/ktor/utils/io/CountedByteWriteChannel;
}

public final class io/ktor/utils/io/DefaultJvmSerializerReplacement : java/io/Externalizable {
public static final field Companion Lio/ktor/utils/io/DefaultJvmSerializerReplacement$Companion;
public fun <init> ()V
public fun <init> (Lio/ktor/utils/io/JvmSerializer;Ljava/lang/Object;)V
public fun readExternal (Ljava/io/ObjectInput;)V
public fun writeExternal (Ljava/io/ObjectOutput;)V
}

public final class io/ktor/utils/io/DefaultJvmSerializerReplacement$Companion {
}

public final class io/ktor/utils/io/DeprecationKt {
public static final fun readText (Lkotlinx/io/Source;)Ljava/lang/String;
public static final fun release (Lkotlinx/io/Sink;)V
Expand All @@ -217,6 +228,15 @@ public final class io/ktor/utils/io/DeprecationKt {
public abstract interface annotation class io/ktor/utils/io/InternalAPI : java/lang/annotation/Annotation {
}

public final class io/ktor/utils/io/JvmSerializable_jvmKt {
public static final fun JvmSerializerReplacement (Lio/ktor/utils/io/JvmSerializer;Ljava/lang/Object;)Ljava/lang/Object;
}

public abstract interface class io/ktor/utils/io/JvmSerializer : java/io/Serializable {
public abstract fun jvmDeserialize ([B)Ljava/lang/Object;
public abstract fun jvmSerialize (Ljava/lang/Object;)[B
}

public abstract interface annotation class io/ktor/utils/io/KtorDsl : java/lang/annotation/Annotation {
}

Expand Down
8 changes: 8 additions & 0 deletions ktor-io/api/ktor-io.klib.api
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,11 @@ abstract interface <#A: kotlin/Any> io.ktor.utils.io.pool/ObjectPool : kotlin/Au
open fun close() // io.ktor.utils.io.pool/ObjectPool.close|close(){}[0]
}

abstract interface <#A: kotlin/Any?> io.ktor.utils.io/JvmSerializer : io.ktor.utils.io/JvmSerializable { // io.ktor.utils.io/JvmSerializer|null[0]
abstract fun jvmDeserialize(kotlin/ByteArray): #A // io.ktor.utils.io/JvmSerializer.jvmDeserialize|jvmDeserialize(kotlin.ByteArray){}[0]
abstract fun jvmSerialize(#A): kotlin/ByteArray // io.ktor.utils.io/JvmSerializer.jvmSerialize|jvmSerialize(1:0){}[0]
}

abstract interface io.ktor.utils.io.core/Closeable : kotlin/AutoCloseable { // io.ktor.utils.io.core/Closeable|null[0]
abstract fun close() // io.ktor.utils.io.core/Closeable.close|close(){}[0]
}
Expand Down Expand Up @@ -97,6 +102,8 @@ abstract interface io.ktor.utils.io/ChannelJob { // io.ktor.utils.io/ChannelJob|
abstract fun <get-job>(): kotlinx.coroutines/Job // io.ktor.utils.io/ChannelJob.job.<get-job>|<get-job>(){}[0]
}

abstract interface io.ktor.utils.io/JvmSerializable // io.ktor.utils.io/JvmSerializable|null[0]

abstract class <#A: kotlin/Any> io.ktor.utils.io.pool/DefaultPool : io.ktor.utils.io.pool/ObjectPool<#A> { // io.ktor.utils.io.pool/DefaultPool|null[0]
constructor <init>(kotlin/Int) // io.ktor.utils.io.pool/DefaultPool.<init>|<init>(kotlin.Int){}[0]

Expand Down Expand Up @@ -399,6 +406,7 @@ final fun (kotlinx.io/Source).io.ktor.utils.io.core/readTextExactCharacters(kotl
final fun (kotlinx.io/Source).io.ktor.utils.io.core/release() // io.ktor.utils.io.core/release|release@kotlinx.io.Source(){}[0]
final fun (kotlinx.io/Source).io.ktor.utils.io.core/takeWhile(kotlin/Function1<kotlinx.io/Buffer, kotlin/Boolean>) // io.ktor.utils.io.core/takeWhile|takeWhile@kotlinx.io.Source(kotlin.Function1<kotlinx.io.Buffer,kotlin.Boolean>){}[0]
final fun (kotlinx.io/Source).io.ktor.utils.io/readText(): kotlin/String // io.ktor.utils.io/readText|readText@kotlinx.io.Source(){}[0]
final fun <#A: kotlin/Any> io.ktor.utils.io/JvmSerializerReplacement(io.ktor.utils.io/JvmSerializer<#A>, #A): kotlin/Any // io.ktor.utils.io/JvmSerializerReplacement|JvmSerializerReplacement(io.ktor.utils.io.JvmSerializer<0:0>;0:0){0§<kotlin.Any>}[0]
final fun <#A: kotlin/Any?> (kotlinx.io/Sink).io.ktor.utils.io.core/preview(kotlin/Function1<kotlinx.io/Source, #A>): #A // io.ktor.utils.io.core/preview|preview@kotlinx.io.Sink(kotlin.Function1<kotlinx.io.Source,0:0>){0§<kotlin.Any?>}[0]
final fun <#A: kotlin/Any?> (kotlinx.io/Source).io.ktor.utils.io.core/preview(kotlin/Function1<kotlinx.io/Source, #A>): #A // io.ktor.utils.io.core/preview|preview@kotlinx.io.Source(kotlin.Function1<kotlinx.io.Source,0:0>){0§<kotlin.Any?>}[0]
final fun <#A: kotlin/Any?> io.ktor.utils.io.core/withMemory(kotlin/Int, kotlin/Function1<kotlin/ByteArray, #A>): #A // io.ktor.utils.io.core/withMemory|withMemory(kotlin.Int;kotlin.Function1<kotlin.ByteArray,0:0>){0§<kotlin.Any?>}[0]
Expand Down
20 changes: 20 additions & 0 deletions ktor-io/common/src/io/ktor/utils/io/JvmSerializable.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/*
* Copyright 2014-2024 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license.
*/

package io.ktor.utils.io

/** Alias for `java.io.Serializable` on JVM. Empty interface otherwise. */
@InternalAPI
public expect interface JvmSerializable

@InternalAPI
public interface JvmSerializer<T> : JvmSerializable {
public fun jvmSerialize(value: T): ByteArray
public fun jvmDeserialize(value: ByteArray): T
}

@InternalAPI
public expect fun <T : Any> JvmSerializerReplacement(serializer: JvmSerializer<T>, value: T): Any

internal object DummyJvmSimpleSerializerReplacement
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/*
* Copyright 2014-2024 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license.
*/

package io.ktor.utils.io

@InternalAPI
public actual interface JvmSerializable

@InternalAPI
public actual fun <T : Any> JvmSerializerReplacement(serializer: JvmSerializer<T>, value: T): Any =
DummyJvmSimpleSerializerReplacement
42 changes: 42 additions & 0 deletions ktor-io/jvm/src/io/ktor/utils/io/JvmSerializable.jvm.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/*
* Copyright 2014-2024 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license.
*/

package io.ktor.utils.io

import java.io.*

@InternalAPI
public actual typealias JvmSerializable = Serializable

@Suppress("UNCHECKED_CAST")
@InternalAPI
public actual fun <T : Any> JvmSerializerReplacement(serializer: JvmSerializer<T>, value: T): Any =
DefaultJvmSerializerReplacement(serializer, value)

@OptIn(InternalAPI::class)
@PublishedApi // IMPORTANT: changing the class name would result in serialization incompatibility
internal class DefaultJvmSerializerReplacement<T : Any>(
private var serializer: JvmSerializer<T>?,
private var value: T?
) : Externalizable {
constructor() : this(null, null)

override fun writeExternal(out: ObjectOutput) {
out.writeObject(serializer)
out.writeObject(serializer!!.jvmSerialize(value!!))
}

@Suppress("UNCHECKED_CAST")
override fun readExternal(`in`: ObjectInput) {
serializer = `in`.readObject() as JvmSerializer<T>
value = serializer!!.jvmDeserialize(`in`.readObject() as ByteArray)
}

private fun readResolve(): Any =
value!!

companion object {
private const val serialVersionUID: Long = 0L
}
}
Loading