Skip to content

Commit

Permalink
KTOR-7620 Make Url class @serializable and JVM Serializable (#4421)
Browse files Browse the repository at this point in the history
  • Loading branch information
wkornewald authored and e5l committed Nov 27, 2024
1 parent d217d36 commit bc54f51
Show file tree
Hide file tree
Showing 14 changed files with 232 additions and 10 deletions.
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> {
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 @@ -210,6 +210,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 @@ -218,6 +229,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

0 comments on commit bc54f51

Please sign in to comment.