Skip to content

Commit

Permalink
KTOR-7620 Make Url class @serializable and JVM Serializable
Browse files Browse the repository at this point in the history
In our project we had to define our own UrlSerializer. It would be much nicer to have this in the Ktor library itself, so it works out of the box (similar to how Cookie was recently extended).

Also, types like Url and Cookie should be java.io.Serializable. Otherwise Android crashes when using those types as e.g. screen arguments. This happens very quickly when Url is used indirectly as part of a data class where we wanted type safety.
  • Loading branch information
wkornewald committed Oct 31, 2024
1 parent 5b086cc commit 9057d9f
Show file tree
Hide file tree
Showing 14 changed files with 216 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 fun <init> ()V
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
20 changes: 16 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,19 @@ 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]
}
}

final class io.ktor.http/UrlSerializer : kotlinx.serialization/KSerializer<io.ktor.http/Url> { // io.ktor.http/UrlSerializer|null[0]
constructor <init>() // io.ktor.http/UrlSerializer.<init>|<init>(){}[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]
}

sealed class io.ktor.http.auth/HttpAuthHeader { // io.ktor.http.auth/HttpAuthHeader|null[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"))
}
}
}
}
13 changes: 12 additions & 1 deletion ktor-http/common/src/io/ktor/http/Cookie.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ 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 +38,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
3 changes: 2 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,14 @@
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) {
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
29 changes: 28 additions & 1 deletion ktor-http/common/src/io/ktor/http/Url.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@

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 +23,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 +35,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 +228,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 +262,22 @@ internal val Url.encodedUserAndPassword: String
get() = buildString {
appendUserAndPassword(encodedUser, encodedPassword)
}

public class UrlSerializer : KSerializer<Url> {
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("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())
}
24 changes: 24 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,24 @@
// 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() {
val url = Url("https://localhost/path?key=value#fragment")
assertEquals(url, assertSerializable(url))
}

@Test
fun cookieTest() {
val cookie = Cookie("key", "value")
assertEquals(cookie, assertSerializable(cookie))
}
}
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|[email protected](){}[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|[email protected](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|[email protected](){}[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|[email protected](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|[email protected](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
16 changes: 16 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,16 @@
/*
* 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

public expect interface JvmSerializable

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

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,11 @@
/*
* 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. */
public actual interface JvmSerializable

public actual fun <T : Any> JvmSerializerReplacement(serializer: JvmSerializer<T>, value: T): Any =
DummyJvmSimpleSerializerReplacement
39 changes: 39 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,39 @@
/*
* 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.*

public actual typealias JvmSerializable = Serializable

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

@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
}
}
11 changes: 11 additions & 0 deletions ktor-io/posix/src/io/ktor/utils/io/JvmSerializable.posix.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/*
* 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. */
public actual interface JvmSerializable

public actual fun <T : Any> JvmSerializerReplacement(serializer: JvmSerializer<T>, value: T): Any =
DummyJvmSimpleSerializerReplacement
Loading

0 comments on commit 9057d9f

Please sign in to comment.