From 22b5d7a47d109fb3713adde0d7cc9a226f8d4a34 Mon Sep 17 00:00:00 2001 From: Leonid Stashevsky Date: Mon, 10 Aug 2020 15:48:42 +0300 Subject: [PATCH] Revert breaking changes for 1.4 release (#2001) * Revert Semaphore breaking change * Revert ContentType breaking changes * Update public API --- .../ktor-client-json/api/ktor-client-json.api | 8 +- .../features/json/JsonContentTypeMatcher.kt | 18 +++ .../ktor/client/features/json/JsonFeature.kt | 128 +++++++++++------- .../common/test/JsonFeatureTest.kt | 32 ++--- .../common/test/KotlinxSerializerTest.kt | 2 +- .../ktor/client/features/json/DefaultJvm.kt | 2 +- ktor-http/api/ktor-http.api | 6 +- .../src/io/ktor/http/ContentTypeMatcher.kt | 2 +- .../common/src/io/ktor/http/ContentTypes.kt | 53 +++++--- .../ktor/tests/http/ContentTypeMatchTest.kt | 50 +++---- .../io/ktor/tests/server/cio/CIOEngineTest.kt | 2 + .../jvm/src/io/ktor/features/Compression.kt | 4 +- .../io/ktor/features/ContentNegotiation.kt | 4 +- .../request/ApplicationRequestProperties.kt | 2 +- .../io/ktor/server/engine/DefaultTransform.kt | 4 +- .../server/testing/suites/ContentTestSuite.kt | 5 +- .../server/features/PartialContentTest.kt | 2 +- ktor-utils/api/ktor-utils.api | 13 +- ktor-utils/common/src/io/ktor/util/LazyVar.kt | 22 --- .../jvm/src/io/ktor/util/cio/Semaphore.kt | 42 ++++++ 20 files changed, 246 insertions(+), 155 deletions(-) create mode 100644 ktor-client/ktor-client-features/ktor-client-json/common/src/io/ktor/client/features/json/JsonContentTypeMatcher.kt delete mode 100644 ktor-utils/common/src/io/ktor/util/LazyVar.kt create mode 100644 ktor-utils/jvm/src/io/ktor/util/cio/Semaphore.kt diff --git a/ktor-client/ktor-client-features/ktor-client-json/api/ktor-client-json.api b/ktor-client/ktor-client-features/ktor-client-json/api/ktor-client-json.api index 6dd5c9781cb..7680158c5ff 100644 --- a/ktor-client/ktor-client-features/ktor-client-json/api/ktor-client-json.api +++ b/ktor-client/ktor-client-features/ktor-client-json/api/ktor-client-json.api @@ -5,15 +5,19 @@ public final class io/ktor/client/features/json/DefaultJvmKt { public final class io/ktor/client/features/json/JsonFeature { public static final field Feature Lio/ktor/client/features/json/JsonFeature$Feature; public fun (Lio/ktor/client/features/json/JsonSerializer;)V - public final fun getConfig ()Lio/ktor/client/features/json/JsonFeature$Config; + public final fun getAcceptContentTypes ()Ljava/util/List; + public final fun getSerializer ()Lio/ktor/client/features/json/JsonSerializer; } public final class io/ktor/client/features/json/JsonFeature$Config { public fun ()V - public final fun accept ([Lio/ktor/http/ContentTypeMatcher;)V + public final fun accept ([Lio/ktor/http/ContentType;)V public final fun getAcceptContentTypes ()Ljava/util/List; + public final fun getReceiveContentTypeMatchers ()Ljava/util/List; public final fun getSerializer ()Lio/ktor/client/features/json/JsonSerializer; + public final fun receive (Lio/ktor/http/ContentTypeMatcher;)V public final fun setAcceptContentTypes (Ljava/util/List;)V + public final fun setReceiveContentTypeMatchers (Ljava/util/List;)V public final fun setSerializer (Lio/ktor/client/features/json/JsonSerializer;)V } diff --git a/ktor-client/ktor-client-features/ktor-client-json/common/src/io/ktor/client/features/json/JsonContentTypeMatcher.kt b/ktor-client/ktor-client-features/ktor-client-json/common/src/io/ktor/client/features/json/JsonContentTypeMatcher.kt new file mode 100644 index 00000000000..956fd9d1b41 --- /dev/null +++ b/ktor-client/ktor-client-features/ktor-client-json/common/src/io/ktor/client/features/json/JsonContentTypeMatcher.kt @@ -0,0 +1,18 @@ +/* + * Copyright 2014-2020 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +package io.ktor.client.features.json + +import io.ktor.http.* + +internal class JsonContentTypeMatcher : ContentTypeMatcher { + override fun contains(contentType: ContentType): Boolean { + if (ContentType.Application.Json.match(contentType)) { + return true + } + + val value = contentType.withoutParameters().toString() + return value.startsWith("application/") && value.endsWith("+json") + } +} diff --git a/ktor-client/ktor-client-features/ktor-client-json/common/src/io/ktor/client/features/json/JsonFeature.kt b/ktor-client/ktor-client-features/ktor-client-json/common/src/io/ktor/client/features/json/JsonFeature.kt index d1bb6bbff4c..e62022237c9 100644 --- a/ktor-client/ktor-client-features/ktor-client-json/common/src/io/ktor/client/features/json/JsonFeature.kt +++ b/ktor-client/ktor-client-features/ktor-client-json/common/src/io/ktor/client/features/json/JsonFeature.kt @@ -6,14 +6,12 @@ package io.ktor.client.features.json import io.ktor.client.* import io.ktor.client.features.* -import io.ktor.client.features.json.JsonFeature.* import io.ktor.client.request.* import io.ktor.client.statement.* import io.ktor.client.utils.* import io.ktor.http.* import io.ktor.util.* import io.ktor.utils.io.* -import io.ktor.utils.io.core.* /** @@ -28,26 +26,30 @@ expect fun defaultSerializer(): JsonSerializer /** * [HttpClient] feature that serializes/de-serializes as JSON custom objects - * to request and from response bodies using a [Config.serializer]. + * to request and from response bodies using a [serializer]. * - * The default [Config.serializer] is [GsonSerializer]. + * The default [serializer] is [GsonSerializer]. * - * The default [Config.acceptContentTypes] is a list with a [ContentTypeMatcher] accepting - * [ContentType.Application.Json] and any `application/...+json` pattern. + * The default [acceptContentTypes] is a list which contains [ContentType.Application.Json] * - * Note: - * The request/response body is only serialized/deserialized if the specified type is a public - * accessible class and the Content-Type is matched by [Config.acceptContentTypes]. + * Note: It will de-serialize the body response if the specified type is a public accessible class + * and the Content-Type is one of [acceptContentTypes] list (`application/json` by default). + * + * @property serializer that is used to serialize and deserialize request/response bodies + * @property acceptContentTypes that are allowed when receiving content */ -class JsonFeature internal constructor(val config: Config) { - @Deprecated( - "Install feature properly instead of direct instantiation.", - level = DeprecationLevel.ERROR - ) - constructor(serializer: JsonSerializer) : this( - Config().apply { - this.serializer = serializer - } +class JsonFeature internal constructor( + val serializer: JsonSerializer, + val acceptContentTypes: List = listOf(ContentType.Application.Json), + private val receiveContentTypeMatchers: List = listOf(JsonContentTypeMatcher()), +) { + @Deprecated("Install feature properly instead of direct instantiation.", level = DeprecationLevel.ERROR) + constructor(serializer: JsonSerializer) : this(serializer, listOf(ContentType.Application.Json)) + + internal constructor(config: Config) : this( + config.serializer ?: defaultSerializer(), + config.acceptContentTypes, + config.receiveContentTypeMatchers ) /** @@ -59,12 +61,13 @@ class JsonFeature internal constructor(val config: Config) { * * Default value for [serializer] is [defaultSerializer]. */ - var serializer: JsonSerializer by lazyVar { defaultSerializer() } + var serializer: JsonSerializer? = null /** * Backing field with mutable list of content types that are handled by this feature. */ - private val _acceptContentTypes: MutableList = + private val _acceptContentTypes: MutableList = mutableListOf(ContentType.Application.Json) + private val _receiveContentTypeMatchers: MutableList = mutableListOf(JsonContentTypeMatcher()) /** @@ -72,23 +75,51 @@ class JsonFeature internal constructor(val config: Config) { * It also affects `Accept` request header value. * Please note that wildcard content types are supported but no quality specification provided. */ - var acceptContentTypes: List - get() = _acceptContentTypes + @KtorExperimentalAPI + var acceptContentTypes: List set(value) { + require(value.isNotEmpty()) { "At least one content type should be provided to acceptContentTypes" } + _acceptContentTypes.clear() - _acceptContentTypes.addAll(value.toSet()) + _acceptContentTypes.addAll(value) + } + get() = _acceptContentTypes + + /** + * List of content type matchers that are handled by this feature. + * Please note that wildcard content types are supported but no quality specification provided. + */ + @KtorExperimentalAPI + var receiveContentTypeMatchers: List + set(value) { + require(value.isNotEmpty()) { "At least one content type should be provided to acceptContentTypes" } + _receiveContentTypeMatchers.clear() + _receiveContentTypeMatchers.addAll(value) } + get() = _receiveContentTypeMatchers + + /** + * Adds accepted content types. Be aware that [ContentType.Application.Json] accepted by default is removed from + * the list if you use this function to provide accepted content types. + * It also affects `Accept` request header value. + */ + fun accept(vararg contentTypes: ContentType) { + _acceptContentTypes += contentTypes + } /** * Adds accepted content types. Existing content types will not be removed. */ - fun accept(vararg contentTypes: ContentTypeMatcher) { - val values = _acceptContentTypes.toSet() + contentTypes - acceptContentTypes = values.toList() + fun receive(matcher: ContentTypeMatcher) { + _receiveContentTypeMatchers += matcher } + } - internal fun matchesContentType(contentType: ContentType?): Boolean = - contentType != null && acceptContentTypes.any { it.match(contentType) } + internal fun canHandle(contentType: ContentType): Boolean { + val accepted = acceptContentTypes.any { contentType.match(it) } + val matchers = receiveContentTypeMatchers + + return accepted || matchers.any { matcher -> matcher.contains(contentType) } } /** @@ -97,37 +128,40 @@ class JsonFeature internal constructor(val config: Config) { companion object Feature : HttpClientFeature { override val key: AttributeKey = AttributeKey("Json") - override fun prepare(block: Config.() -> Unit): JsonFeature = - JsonFeature(Config().apply(block)) + override fun prepare(block: Config.() -> Unit): JsonFeature { + val config = Config().apply(block) + val serializer = config.serializer ?: defaultSerializer() + val allowedContentTypes = config.acceptContentTypes.toList() + val receiveContentTypeMatchers = config.receiveContentTypeMatchers + + return JsonFeature(serializer, allowedContentTypes, receiveContentTypeMatchers) + } override fun install(feature: JsonFeature, scope: HttpClient) { - val config = feature.config scope.requestPipeline.intercept(HttpRequestPipeline.Transform) { payload -> + feature.acceptContentTypes.forEach { context.accept(it) } + val contentType = context.contentType() ?: return@intercept - if (!config.matchesContentType(contentType)) - return@intercept + if (!feature.canHandle(contentType)) return@intercept context.headers.remove(HttpHeaders.ContentType) val serializedContent = when (payload) { Unit -> EmptyContent is EmptyContent -> EmptyContent - else -> config.serializer.write(payload, contentType) + else -> feature.serializer.write(payload, contentType) } proceedWith(serializedContent) } scope.responsePipeline.intercept(HttpResponsePipeline.Transform) { (info, body) -> - if (!config.matchesContentType(context.response.contentType())) - return@intercept + if (body !is ByteReadChannel) return@intercept - val data = when (body) { - is ByteReadChannel -> body.readRemaining() - is String -> ByteReadPacket(body.toByteArray()) - else -> return@intercept - } - val parsedBody = config.serializer.read(info, data) + val contentType = context.response.contentType() ?: return@intercept + if (!feature.canHandle(contentType)) return@intercept + + val parsedBody = feature.serializer.read(info, body.readRemaining()) val response = HttpResponseContainer(info, parsedBody) proceedWith(response) } @@ -135,16 +169,6 @@ class JsonFeature internal constructor(val config: Config) { } } -private class JsonContentTypeMatcher : ContentTypeMatcher { - override fun match(contentType: ContentType): Boolean { - if (ContentType.Application.Json.match(contentType)) { - return true - } - val value = contentType.withoutParameters().toString() - return value.startsWith("application/") && value.endsWith("+json") - } -} - /** * Install [JsonFeature]. */ diff --git a/ktor-client/ktor-client-features/ktor-client-json/common/test/JsonFeatureTest.kt b/ktor-client/ktor-client-features/ktor-client-json/common/test/JsonFeatureTest.kt index 930a892b953..69ad66703b8 100644 --- a/ktor-client/ktor-client-features/ktor-client-json/common/test/JsonFeatureTest.kt +++ b/ktor-client/ktor-client-features/ktor-client-json/common/test/JsonFeatureTest.kt @@ -13,22 +13,22 @@ class JsonFeatureTest { val config = JsonFeature.Config() assertEquals(1, config.acceptContentTypes.size) - assertTrue { config.matchesContentType(ContentType.Application.Json) } + assertTrue { config.acceptContentTypes.contains(ContentType.Application.Json) } - assertTrue { config.matchesContentType(ContentType.parse("application/json")) } - assertTrue { config.matchesContentType(ContentType.parse("application/vnd.foo+json")) } - assertFalse { config.matchesContentType(ContentType.parse("text/json")) } + val feature = JsonFeature(config) + assertTrue { feature.canHandle(ContentType.parse("application/json")) } + assertTrue { feature.canHandle(ContentType.parse("application/vnd.foo+json")) } + assertFalse { feature.canHandle(ContentType.parse("text/json")) } } @Test fun testAcceptCall() { val config = JsonFeature.Config() config.accept(ContentType.Application.Xml) - config.accept(ContentType.Application.Xml) assertEquals(2, config.acceptContentTypes.size) - assertTrue { config.matchesContentType(ContentType.Application.Json) } - assertTrue { config.matchesContentType(ContentType.Application.Xml) } + assertTrue { config.acceptContentTypes.contains(ContentType.Application.Json) } + assertTrue { config.acceptContentTypes.contains(ContentType.Application.Xml) } } @Test @@ -37,22 +37,22 @@ class JsonFeatureTest { config.acceptContentTypes = listOf(ContentType.Application.Pdf, ContentType.Application.Xml) assertEquals(2, config.acceptContentTypes.size) - assertFalse { config.matchesContentType(ContentType.Application.Json) } - assertTrue { config.matchesContentType(ContentType.Application.Xml) } - assertTrue { config.matchesContentType(ContentType.Application.Pdf) } + assertFalse { config.acceptContentTypes.contains(ContentType.Application.Json) } + assertTrue { config.acceptContentTypes.contains(ContentType.Application.Xml) } + assertTrue { config.acceptContentTypes.contains(ContentType.Application.Pdf) } } @Test fun testContentTypesFilter() { val config = JsonFeature.Config().apply { - acceptContentTypes = listOf(object : ContentTypeMatcher { - override fun match(contentType: ContentType): Boolean = - contentType.match("text/json") + receive(object : ContentTypeMatcher { + override fun contains(contentType: ContentType): Boolean { + return contentType.toString() == "text/json" + } }) } - assertFalse { config.matchesContentType(ContentType.parse("application/json")) } - assertFalse { config.matchesContentType(ContentType.parse("application/vnd.foo+json")) } - assertTrue { config.matchesContentType(ContentType.parse("text/json")) } + val feature = JsonFeature(config) + assertTrue { feature.canHandle(ContentType.parse("text/json")) } } } diff --git a/ktor-client/ktor-client-features/ktor-client-json/common/test/KotlinxSerializerTest.kt b/ktor-client/ktor-client-features/ktor-client-json/common/test/KotlinxSerializerTest.kt index b7f9bed4456..7efdeed2fb8 100644 --- a/ktor-client/ktor-client-features/ktor-client-json/common/test/KotlinxSerializerTest.kt +++ b/ktor-client/ktor-client-features/ktor-client-json/common/test/KotlinxSerializerTest.kt @@ -99,7 +99,7 @@ class KotlinxSerializerTest : ClientLoader() { val response = client.post("$TEST_SERVER/echo-with-content-type") { body = "Hello" } - assertEquals("Hello", response) + assertEquals("\"Hello\"", response) val textResponse = client.post("$TEST_SERVER/echo") { body = "Hello" diff --git a/ktor-client/ktor-client-features/ktor-client-json/jvm/src/io/ktor/client/features/json/DefaultJvm.kt b/ktor-client/ktor-client-features/ktor-client-json/jvm/src/io/ktor/client/features/json/DefaultJvm.kt index 2932ed32d2a..513ed09696b 100644 --- a/ktor-client/ktor-client-features/ktor-client-json/jvm/src/io/ktor/client/features/json/DefaultJvm.kt +++ b/ktor-client/ktor-client-features/ktor-client-json/jvm/src/io/ktor/client/features/json/DefaultJvm.kt @@ -17,5 +17,5 @@ actual fun defaultSerializer(): JsonSerializer { " - ktor-client-serialization" ) - return serializers.maxBy { it::javaClass.name }!! + return serializers.maxByOrNull { it::javaClass.name }!! } diff --git a/ktor-http/api/ktor-http.api b/ktor-http/api/ktor-http.api index ed180e9c27b..56b386c35c7 100644 --- a/ktor-http/api/ktor-http.api +++ b/ktor-http/api/ktor-http.api @@ -128,7 +128,7 @@ public final class io/ktor/http/ContentRangeKt { public static synthetic fun contentRangeHeaderValue$default (Lkotlin/ranges/LongRange;Ljava/lang/Long;Ljava/lang/String;ILjava/lang/Object;)Ljava/lang/String; } -public final class io/ktor/http/ContentType : io/ktor/http/HeaderValueWithParameters, io/ktor/http/ContentTypeMatcher { +public final class io/ktor/http/ContentType : io/ktor/http/HeaderValueWithParameters { public static final field Companion Lio/ktor/http/ContentType$Companion; public fun (Ljava/lang/String;Ljava/lang/String;Ljava/util/List;)V public synthetic fun (Ljava/lang/String;Ljava/lang/String;Ljava/util/List;ILkotlin/jvm/internal/DefaultConstructorMarker;)V @@ -136,7 +136,7 @@ public final class io/ktor/http/ContentType : io/ktor/http/HeaderValueWithParame public final fun getContentSubtype ()Ljava/lang/String; public final fun getContentType ()Ljava/lang/String; public fun hashCode ()I - public fun match (Lio/ktor/http/ContentType;)Z + public final fun match (Lio/ktor/http/ContentType;)Z public final fun match (Ljava/lang/String;)Z public final fun withParameter (Ljava/lang/String;Ljava/lang/String;)Lio/ktor/http/ContentType; public final fun withoutParameters ()Lio/ktor/http/ContentType; @@ -229,7 +229,7 @@ public final class io/ktor/http/ContentType$Video { } public abstract interface class io/ktor/http/ContentTypeMatcher { - public abstract fun match (Lio/ktor/http/ContentType;)Z + public abstract fun contains (Lio/ktor/http/ContentType;)Z } public final class io/ktor/http/ContentTypesKt { diff --git a/ktor-http/common/src/io/ktor/http/ContentTypeMatcher.kt b/ktor-http/common/src/io/ktor/http/ContentTypeMatcher.kt index 9cace6767ca..aa406ac0187 100644 --- a/ktor-http/common/src/io/ktor/http/ContentTypeMatcher.kt +++ b/ktor-http/common/src/io/ktor/http/ContentTypeMatcher.kt @@ -11,5 +11,5 @@ interface ContentTypeMatcher { /** * Checks if `this` type matches a [contentType] type. */ - fun match(contentType: ContentType): Boolean + fun contains(contentType: ContentType): Boolean } diff --git a/ktor-http/common/src/io/ktor/http/ContentTypes.kt b/ktor-http/common/src/io/ktor/http/ContentTypes.kt index e2bbd77251c..e592277ea85 100644 --- a/ktor-http/common/src/io/ktor/http/ContentTypes.kt +++ b/ktor-http/common/src/io/ktor/http/ContentTypes.kt @@ -11,10 +11,19 @@ import io.ktor.utils.io.charsets.* * @property contentType represents a type part of the media type. * @property contentSubtype represents a subtype part of the media type. */ -class ContentType private constructor(val contentType: String, val contentSubtype: String, existingContent: String, parameters: List = emptyList()) - : HeaderValueWithParameters(existingContent, parameters), ContentTypeMatcher { - - constructor(contentType: String, contentSubtype: String, parameters: List = emptyList()) : this(contentType, contentSubtype, "$contentType/$contentSubtype", parameters) +class ContentType private constructor( + val contentType: String, + val contentSubtype: String, + existingContent: String, + parameters: List = emptyList() +) : HeaderValueWithParameters(existingContent, parameters) { + + constructor(contentType: String, contentSubtype: String, parameters: List = emptyList()) : this( + contentType, + contentSubtype, + "$contentType/$contentSubtype", + parameters + ) /** * Creates a copy of `this` type with the added parameter with the [name] and [value]. @@ -37,45 +46,51 @@ class ContentType private constructor(val contentType: String, val contentSubtyp fun withoutParameters(): ContentType = ContentType(contentType, contentSubtype) /** - * Checks if `this` type matches a [contentType] type taking into account placeholder symbols `*` and parameters in `this`. + * Checks if `this` type matches a [pattern] type taking into account placeholder symbols `*` and parameters. */ - override fun match(contentType: ContentType): Boolean { - if (this.contentType != "*" && !this.contentType.equals(contentType.contentType, ignoreCase = true)) + fun match(pattern: ContentType): Boolean { + if (pattern.contentType != "*" && !pattern.contentType.equals(contentType, ignoreCase = true)) { return false - if (contentSubtype != "*" && !contentSubtype.equals(contentType.contentSubtype, ignoreCase = true)) + } + + if (pattern.contentSubtype != "*" && !pattern.contentSubtype.equals(contentSubtype, ignoreCase = true)) { return false - for ((patternName, patternValue) in parameters) { + } + + for ((patternName, patternValue) in pattern.parameters) { val matches = when (patternName) { "*" -> { when (patternValue) { "*" -> true - else -> contentType.parameters.any { p -> p.value.equals(patternValue, ignoreCase = true) } + else -> parameters.any { p -> p.value.equals(patternValue, ignoreCase = true) } } } else -> { - val value = contentType.parameter(patternName) + val value = parameter(patternName) when (patternValue) { "*" -> value != null else -> value.equals(patternValue, ignoreCase = true) } } } - if (!matches) + + if (!matches) { return false + } } return true } /** - * Checks if `this` type matches a [contentType] string taking into account placeholder symbols `*` and parameters in `this`. + * Checks if `this` type matches a [pattern] type taking into account placeholder symbols `*` and parameters. */ - fun match(contentType: String): Boolean = match(parse(contentType)) + fun match(pattern: String): Boolean = match(parse(pattern)) override fun equals(other: Any?): Boolean = - other is ContentType && - contentType.equals(other.contentType, ignoreCase = true) && - contentSubtype.equals(other.contentSubtype, ignoreCase = true) && - parameters == other.parameters + other is ContentType && + contentType.equals(other.contentType, ignoreCase = true) && + contentSubtype.equals(other.contentSubtype, ignoreCase = true) && + parameters == other.parameters override fun hashCode(): Int { var result = contentType.toLowerCase().hashCode() @@ -241,6 +256,6 @@ class BadContentTypeFormatException(value: String) : Exception("Bad Content-Type fun ContentType.withCharset(charset: Charset): ContentType = withParameter("charset", charset.name) /** - * Extracts a [Charset] value from the given `Content-Type`, `Content-Disposition` or similar header value. + * Extracts a [Charset] value from the given `Content-Type`, `Content-Disposition` or similar header value. */ fun HeaderValueWithParameters.charset(): Charset? = parameter("charset")?.let { Charset.forName(it) } diff --git a/ktor-http/common/test/io/ktor/tests/http/ContentTypeMatchTest.kt b/ktor-http/common/test/io/ktor/tests/http/ContentTypeMatchTest.kt index 3eff195752b..9d4cf31d23d 100644 --- a/ktor-http/common/test/io/ktor/tests/http/ContentTypeMatchTest.kt +++ b/ktor-http/common/test/io/ktor/tests/http/ContentTypeMatchTest.kt @@ -7,19 +7,19 @@ package io.ktor.tests.http import io.ktor.http.* import kotlin.test.* -class ContentTypeMatchTest { +public class ContentTypeMatchTest { @Test fun testTypeAndSubtype() { - assertTrue { ContentType.parse("*").match("text/plain") } - assertTrue { ContentType.parse("* ").match("text/plain") } - assertTrue { ContentType.parse("*/*").match("text/plain") } - assertTrue { ContentType.parse("*/ *").match("text/plain") } - assertTrue { ContentType.parse("*/plain").match("text/plain") } - assertTrue { ContentType.parse("* /plain").match("text/plain") } - assertTrue { ContentType.parse("*/plain").match("text/PLAIN") } - assertTrue { ContentType.parse("text/*").match("text/plain") } + assertTrue { ContentType.parse("text/plain").match("*") } + assertTrue { ContentType.parse("text/plain").match("* ") } + assertTrue { ContentType.parse("text/plain").match("*/*") } + assertTrue { ContentType.parse("text/plain").match("*/ *") } + assertTrue { ContentType.parse("text/plain").match("*/plain") } + assertTrue { ContentType.parse("text/plain").match("* /plain") } + assertTrue { ContentType.parse("text/PLAIN").match("*/plain") } + assertTrue { ContentType.parse("text/plain").match("text/*") } assertTrue { ContentType.parse("text/plain").match("text/plain") } - assertTrue { ContentType.parse("TEXT/plain").match("text/plain") } + assertTrue { ContentType.parse("text/plain").match("TEXT/plain") } assertFailsWith { ContentType.parse("text/") } assertFailsWith { ContentType.parse("/plain") } @@ -32,37 +32,37 @@ class ContentTypeMatchTest { @Test fun testParametersConstants() { - assertTrue { ContentType.parse("*/*; a=1").match("a/b; a=1") } - assertTrue { ContentType.parse("*/*; a=1").match("a/b; A=1") } - assertFalse(ContentType.parse("*/*; a=2").match("a/b")) - assertFalse(ContentType.parse("*/*; a=2").match("a/b; a=1")) - assertFalse(ContentType.parse("*/*; a=2").match("a/b; A=1")) + assertTrue { ContentType.parse("a/b; a=1").match("*/*; a=1") } + assertTrue { ContentType.parse("a/b; A=1").match("*/*; a=1") } + assertFalse(ContentType.parse("a/b").match("*/*; a=2")) + assertFalse(ContentType.parse("a/b; a=1").match("*/*; a=2")) + assertFalse(ContentType.parse("a/b; A=1").match("*/*; a=2")) } @Test fun testParametersWithSubtype() { - assertTrue { ContentType.parse("a/b").match("a/b; a=1") } - assertTrue { ContentType.parse("a/b; a=XYZ").match("a/b; a=xyz") } + assertTrue { ContentType.parse("a/b; a=1").match("a/b") } + assertTrue { ContentType.parse("a/b; a=xyz").match("a/b; a=XYZ") } } @Test fun testParametersValueWildcard() { - assertTrue(ContentType.parse("*/*; a=*").match("a/b; a=1")) - assertFalse(ContentType.parse("*/*; a=*").match("a/b; b=1")) + assertTrue(ContentType.parse("a/b; a=1").match("*/*; a=*")) + assertFalse(ContentType.parse("a/b; b=1").match("*/*; a=*")) } @Test fun testParametersNameWildcard() { - assertTrue(ContentType.parse("*/*; *=1").match("a/b; a=1")) - assertTrue(ContentType.parse("*/*; *=x").match("a/b; a=X")) - assertFalse(ContentType.parse("*/*; *=1").match("a/b; a=2")) - assertFalse(ContentType.parse("*/*; *=x").match("a/b; a=y")) + assertTrue(ContentType.parse("a/b; a=1").match("*/*; *=1")) + assertTrue(ContentType.parse("a/b; a=X").match("*/*; *=x")) + assertFalse(ContentType.parse("a/b; a=2").match("*/*; *=1")) + assertFalse(ContentType.parse("a/b; a=y").match("*/*; *=x")) } @Test fun testParametersAllWildcard() { - assertTrue(ContentType.parse("*/*; *=*").match("a/b; a=2")) - assertTrue(ContentType.parse("*/*; *=*").match("a/b")) + assertTrue(ContentType.parse("a/b; a=2").match("*/*; *=*")) + assertTrue(ContentType.parse("a/b").match("*/*; *=*")) } @Test diff --git a/ktor-server/ktor-server-cio/jvm/test/io/ktor/tests/server/cio/CIOEngineTest.kt b/ktor-server/ktor-server-cio/jvm/test/io/ktor/tests/server/cio/CIOEngineTest.kt index d2cafad19a5..be326c41848 100644 --- a/ktor-server/ktor-server-cio/jvm/test/io/ktor/tests/server/cio/CIOEngineTest.kt +++ b/ktor-server/ktor-server-cio/jvm/test/io/ktor/tests/server/cio/CIOEngineTest.kt @@ -20,12 +20,14 @@ class CIOContentTest : ContentTestSuite(CIO) { init { enableHttp2 = false enableSsl = false } } + class CIOSustainabilityTest : SustainabilityTestSuite(CIO) { init { enableHttp2 = false diff --git a/ktor-server/ktor-server-core/jvm/src/io/ktor/features/Compression.kt b/ktor-server/ktor-server-core/jvm/src/io/ktor/features/Compression.kt index 3ec076b7c54..e860f9eaecf 100644 --- a/ktor-server/ktor-server-core/jvm/src/io/ktor/features/Compression.kt +++ b/ktor-server/ktor-server-core/jvm/src/io/ktor/features/Compression.kt @@ -391,7 +391,7 @@ fun ConditionsHolderBuilder.minimumSize(minSize: Long) { fun ConditionsHolderBuilder.matchContentType(vararg mimeTypes: ContentType) { condition { content -> val contentType = content.contentType ?: return@condition false - mimeTypes.any { it.match(contentType) } + mimeTypes.any { contentType.match(it) } } } @@ -404,6 +404,6 @@ fun ConditionsHolderBuilder.excludeContentType(vararg mimeTypes: ContentType) { ?: response.headers[HttpHeaders.ContentType]?.let { ContentType.parse(it) } ?: return@condition true - mimeTypes.none { excludePattern -> excludePattern.match(contentType) } + mimeTypes.none { excludePattern -> contentType.match(excludePattern) } } } diff --git a/ktor-server/ktor-server-core/jvm/src/io/ktor/features/ContentNegotiation.kt b/ktor-server/ktor-server-core/jvm/src/io/ktor/features/ContentNegotiation.kt index 48046d322f7..fc2084b3129 100644 --- a/ktor-server/ktor-server-core/jvm/src/io/ktor/features/ContentNegotiation.kt +++ b/ktor-server/ktor-server-core/jvm/src/io/ktor/features/ContentNegotiation.kt @@ -134,7 +134,7 @@ class ContentNegotiation(val registrations: List, } else { // select converters that match specified Accept header, in order of quality acceptItems.flatMap { (contentType, _) -> - feature.registrations.filter { contentType.match(it.contentType) } + feature.registrations.filter { it.contentType.match(contentType) } }.distinct() } @@ -163,7 +163,7 @@ class ContentNegotiation(val registrations: List, ) } val suitableConverter = - feature.registrations.firstOrNull { converter -> converter.contentType.match(requestContentType) } + feature.registrations.firstOrNull { converter -> requestContentType.match(converter.contentType) } ?: throw UnsupportedMediaTypeException(requestContentType) val converted = suitableConverter.converter.convertForReceive(this) diff --git a/ktor-server/ktor-server-core/jvm/src/io/ktor/request/ApplicationRequestProperties.kt b/ktor-server/ktor-server-core/jvm/src/io/ktor/request/ApplicationRequestProperties.kt index 117e8a4e74c..631a74ab8b5 100644 --- a/ktor-server/ktor-server-core/jvm/src/io/ktor/request/ApplicationRequestProperties.kt +++ b/ktor-server/ktor-server-core/jvm/src/io/ktor/request/ApplicationRequestProperties.kt @@ -99,7 +99,7 @@ fun ApplicationRequest.isChunked(): Boolean = header(HttpHeaders.TransferEncodin /** * Check if request body is multipart-encoded */ -fun ApplicationRequest.isMultipart(): Boolean = ContentType.MultiPart.Any.match(contentType()) +fun ApplicationRequest.isMultipart(): Boolean = contentType().match(ContentType.MultiPart.Any) /** * Request's `User-Agent` header value diff --git a/ktor-server/ktor-server-host-common/jvm/src/io/ktor/server/engine/DefaultTransform.kt b/ktor-server/ktor-server-host-common/jvm/src/io/ktor/server/engine/DefaultTransform.kt index ceb2ecfb0c0..b3ec2d06c9a 100644 --- a/ktor-server/ktor-server-host-common/jvm/src/io/ktor/server/engine/DefaultTransform.kt +++ b/ktor-server/ktor-server-host-common/jvm/src/io/ktor/server/engine/DefaultTransform.kt @@ -55,11 +55,11 @@ fun ApplicationReceivePipeline.installDefaultTransformations() { Parameters::class -> { val contentType = withContentType(call) { call.request.contentType() } when { - ContentType.Application.FormUrlEncoded.match(contentType) -> { + contentType.match(ContentType.Application.FormUrlEncoded) -> { val string = channel.readText(charset = call.request.contentCharset() ?: Charsets.ISO_8859_1) parseQueryString(string) } - ContentType.MultiPart.FormData.match(contentType) -> { + contentType.match(ContentType.MultiPart.FormData) -> { Parameters.build { multiPartData(channel).forEachPart { part -> if (part is PartData.FormItem) { diff --git a/ktor-server/ktor-server-test-host/jvm/src/io/ktor/server/testing/suites/ContentTestSuite.kt b/ktor-server/ktor-server-test-host/jvm/src/io/ktor/server/testing/suites/ContentTestSuite.kt index 5a1cf1b3713..289401d0e06 100644 --- a/ktor-server/ktor-server-test-host/jvm/src/io/ktor/server/testing/suites/ContentTestSuite.kt +++ b/ktor-server/ktor-server-test-host/jvm/src/io/ktor/server/testing/suites/ContentTestSuite.kt @@ -373,7 +373,10 @@ abstract class ContentTestSuite ()V public final fun close ()V @@ -459,6 +455,15 @@ public final class io/ktor/util/cio/ReadersKt { public static final fun use (Lio/ktor/utils/io/ByteWriteChannel;Lkotlin/jvm/functions/Function1;)V } +public final class io/ktor/util/cio/Semaphore { + public fun (I)V + public final fun acquire (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public final fun enter (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public final fun getLimit ()I + public final fun leave ()V + public final fun release ()V +} + public class io/ktor/util/collections/ConcurrentCollection : java/util/Collection, kotlin/jvm/internal/markers/KMutableCollection { public fun (Ljava/util/Collection;Lio/ktor/util/Lock;)V public fun add (Ljava/lang/Object;)Z diff --git a/ktor-utils/common/src/io/ktor/util/LazyVar.kt b/ktor-utils/common/src/io/ktor/util/LazyVar.kt deleted file mode 100644 index 6ff408b0780..00000000000 --- a/ktor-utils/common/src/io/ktor/util/LazyVar.kt +++ /dev/null @@ -1,22 +0,0 @@ -package io.ktor.util - -import kotlin.properties.* -import kotlin.reflect.* - -/** Like `by lazy`, but usable with `var` (i.e. mutable). */ -fun lazyVar(initializer: () -> T): ReadWriteProperty = LazyVar(initializer) - -private class LazyVar(initializer: () -> T) : ReadWriteProperty { - private var overriddenValue: T? = null - private var hasOverriddenValue = false - private val initializerValue by lazy(initializer) - - @Suppress("UNCHECKED_CAST") - override operator fun getValue(thisRef: Any?, property: KProperty<*>): T = - if (hasOverriddenValue) overriddenValue as T else initializerValue - - override operator fun setValue(thisRef: Any?, property: KProperty<*>, value: T) { - overriddenValue = value - hasOverriddenValue = true - } -} diff --git a/ktor-utils/jvm/src/io/ktor/util/cio/Semaphore.kt b/ktor-utils/jvm/src/io/ktor/util/cio/Semaphore.kt new file mode 100644 index 00000000000..7dde82bbed4 --- /dev/null +++ b/ktor-utils/jvm/src/io/ktor/util/cio/Semaphore.kt @@ -0,0 +1,42 @@ +/* + * Copyright 2014-2020 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +package io.ktor.util.cio + +import kotlinx.coroutines.sync.Semaphore + +@Deprecated( + "Ktor Semaphore is deprecated and will be removed in ktor 2.0.0. Consider using kotlinx.coroutines Semaphore instead.", + level = DeprecationLevel.WARNING, + replaceWith = ReplaceWith("Semaphore", "kotlinx.coroutines.sync.Semaphore") +) +class Semaphore(val limit: Int) { + private val delegate = Semaphore(limit) + + @Deprecated( + "Ktor Semaphore is deprecated and will be removed in ktor 2.0.0. Consider using kotlinx.coroutines Semaphore instead.", + level = DeprecationLevel.WARNING, + replaceWith = ReplaceWith("acquire()", "kotlinx.coroutines.sync.Semaphore") + ) + suspend fun enter() { + delegate.acquire() + } + + suspend fun acquire() { + delegate.acquire() + } + + @Deprecated( + "Ktor Semaphore is deprecated and will be removed in ktor 2.0.0. Consider using kotlinx.coroutines Semaphore instead.", + level = DeprecationLevel.WARNING, + replaceWith = ReplaceWith("release()", "kotlinx.coroutines.sync.Semaphore") + ) + fun leave() { + delegate.release() + } + + fun release() { + delegate.release() + } +}