Skip to content

Commit de3d9ce

Browse files
committed
Ad Accept header
1 parent bb2a043 commit de3d9ce

File tree

16 files changed

+266
-107
lines changed

16 files changed

+266
-107
lines changed

kttp-api/src/main/kotlin/com/coditory/kttp/HttpRequestHead.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.coditory.kttp
22

3+
import com.coditory.kttp.headers.HttpHeaders
34
import java.net.URI
45

56
data class HttpRequestHead(

kttp-api/src/main/kotlin/com/coditory/kttp/HttpResponseHead.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package com.coditory.kttp
22

3+
import com.coditory.kttp.headers.HttpHeaders
4+
35
data class HttpResponseHead(
46
val status: HttpStatus,
57
val headers: HttpHeaders = HttpHeaders.empty(),
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package com.coditory.kttp.headers
2+
3+
data class Accept(
4+
val contentTypes: List<ContentType>,
5+
) {
6+
fun matches(contentType: ContentType): Boolean {
7+
return contentTypes.any { it.matches(contentType) }
8+
}
9+
10+
companion object {
11+
fun parse(values: List<String>): Accept? {
12+
val unsortedItems = values.flatMap { HttpHeaderValue.parse(it)?.items ?: emptyList() }
13+
val sortedContentTypes = HttpHeaderValue(unsortedItems).items
14+
.mapNotNull { ContentType.parse(it) }
15+
return if (sortedContentTypes.isEmpty()) null else Accept(sortedContentTypes)
16+
}
17+
18+
fun parse(value: String): Accept? {
19+
val header = HttpHeaderValue.parse(value) ?: return null
20+
val contentTypes = header.items.mapNotNull { ContentType.parse(it) }
21+
return if (contentTypes.isEmpty()) null else Accept(contentTypes)
22+
}
23+
}
24+
}

kttp-api/src/main/kotlin/com/coditory/kttp/ContentType.kt renamed to kttp-api/src/main/kotlin/com/coditory/kttp/headers/ContentType.kt

Lines changed: 19 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
1-
package com.coditory.kttp
1+
package com.coditory.kttp.headers
22

33
import java.nio.charset.Charset
4-
import kotlin.collections.iterator
54

65
class ContentType private constructor(
76
val contentType: String,
87
val contentSubtype: String,
9-
val parameters: HttpHeaderParams = HttpHeaderParams.empty(),
8+
val parameters: HttpHeaderParams = HttpHeaderParams.Companion.empty(),
109
) {
1110
val value by lazy { "$contentType/$contentSubtype" + parameters.toHttpString() }
1211

@@ -48,7 +47,7 @@ class ContentType private constructor(
4847
* ContentType("a", "b").match(ContentType("a", "*")) === true
4948
* ```
5049
*/
51-
fun match(pattern: ContentType): Boolean {
50+
fun matches(pattern: ContentType): Boolean {
5251
if (pattern.contentType != "*" && !pattern.contentType.equals(contentType, ignoreCase = true)) {
5352
return false
5453
}
@@ -118,22 +117,26 @@ class ContentType private constructor(
118117
val header = HttpHeaderValue.parse(value) ?: return null
119118
if (header.items.size != 1) throw BadContentTypeFormatException(value)
120119
val item = header.items.first()
120+
return parse(item)
121+
}
122+
123+
fun parse(item: HttpHeaderValueItem): ContentType? {
121124
val parts = item.value
122125
val slash = parts.indexOf('/')
123126
if (slash == -1) {
124127
if (parts.trim() == "*") return Any
125-
throw BadContentTypeFormatException(value)
128+
throw BadContentTypeFormatException(item.toHttpString())
126129
}
127130
val type = parts.substring(0, slash).trim()
128131
if (type.isEmpty()) {
129-
throw BadContentTypeFormatException(value)
132+
throw BadContentTypeFormatException(item.toHttpString())
130133
}
131134
val subtype = parts.substring(slash + 1).trim()
132135
if (type.contains(' ') || subtype.contains(' ')) {
133-
throw BadContentTypeFormatException(value)
136+
throw BadContentTypeFormatException(item.toHttpString())
134137
}
135138
if (subtype.isEmpty() || subtype.contains('/')) {
136-
throw BadContentTypeFormatException(value)
139+
throw BadContentTypeFormatException(item.toHttpString())
137140
}
138141
return ContentType(type, subtype, item.params)
139142
}
@@ -169,7 +172,7 @@ class ContentType private constructor(
169172
val ProblemXml: ContentType = ContentType(TYPE, "problem+xml")
170173

171174
operator fun contains(contentType: CharSequence): Boolean = contentType.startsWith("$TYPE/", ignoreCase = true)
172-
operator fun contains(contentType: ContentType): Boolean = contentType.match(Any)
175+
operator fun contains(contentType: ContentType): Boolean = contentType.matches(Any)
173176
}
174177

175178
object Audio {
@@ -180,7 +183,7 @@ class ContentType private constructor(
180183
val OGG: ContentType = ContentType(TYPE, "ogg")
181184

182185
operator fun contains(contentType: CharSequence): Boolean = contentType.startsWith("$TYPE/", ignoreCase = true)
183-
operator fun contains(contentType: ContentType): Boolean = contentType.match(Any)
186+
operator fun contains(contentType: ContentType): Boolean = contentType.matches(Any)
184187
}
185188

186189
object Image {
@@ -193,7 +196,7 @@ class ContentType private constructor(
193196
val XIcon: ContentType = ContentType(TYPE, "x-icon")
194197

195198
operator fun contains(contentSubtype: String): Boolean = contentSubtype.startsWith("$TYPE/", ignoreCase = true)
196-
operator fun contains(contentType: ContentType): Boolean = contentType.match(Any)
199+
operator fun contains(contentType: ContentType): Boolean = contentType.matches(Any)
197200
}
198201

199202
object Message {
@@ -202,7 +205,7 @@ class ContentType private constructor(
202205
val Http: ContentType = ContentType(TYPE, "http")
203206

204207
operator fun contains(contentSubtype: String): Boolean = contentSubtype.startsWith("$TYPE/", ignoreCase = true)
205-
operator fun contains(contentType: ContentType): Boolean = contentType.match(Any)
208+
operator fun contains(contentType: ContentType): Boolean = contentType.matches(Any)
206209
}
207210

208211
object MultiPart {
@@ -217,7 +220,7 @@ class ContentType private constructor(
217220
val ByteRanges: ContentType = ContentType(TYPE, "byteranges")
218221

219222
operator fun contains(contentType: CharSequence): Boolean = contentType.startsWith("$TYPE/", ignoreCase = true)
220-
operator fun contains(contentType: ContentType): Boolean = contentType.match(Any)
223+
operator fun contains(contentType: ContentType): Boolean = contentType.matches(Any)
221224
}
222225

223226
object Text {
@@ -233,7 +236,7 @@ class ContentType private constructor(
233236
val EventStream: ContentType = ContentType(TYPE, "event-stream")
234237

235238
operator fun contains(contentType: CharSequence): Boolean = contentType.startsWith("$TYPE/", ignoreCase = true)
236-
operator fun contains(contentType: ContentType): Boolean = contentType.match(Any)
239+
operator fun contains(contentType: ContentType): Boolean = contentType.matches(Any)
237240
}
238241

239242
object Video {
@@ -245,7 +248,7 @@ class ContentType private constructor(
245248
val QuickTime: ContentType = ContentType(TYPE, "quicktime")
246249

247250
operator fun contains(contentType: CharSequence): Boolean = contentType.startsWith("$TYPE/", ignoreCase = true)
248-
operator fun contains(contentType: ContentType): Boolean = contentType.match(Any)
251+
operator fun contains(contentType: ContentType): Boolean = contentType.matches(Any)
249252
}
250253

251254
object Font {
@@ -259,7 +262,7 @@ class ContentType private constructor(
259262
val Woff2: ContentType = ContentType(TYPE, "woff2")
260263

261264
operator fun contains(contentType: CharSequence): Boolean = contentType.startsWith("$TYPE/", ignoreCase = true)
262-
operator fun contains(contentType: ContentType): Boolean = contentType.match(Any)
265+
operator fun contains(contentType: ContentType): Boolean = contentType.matches(Any)
263266
}
264267
}
265268

kttp-api/src/main/kotlin/com/coditory/kttp/HttpHeaderParams.kt renamed to kttp-api/src/main/kotlin/com/coditory/kttp/headers/HttpHeaderParams.kt

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
1-
package com.coditory.kttp
1+
package com.coditory.kttp.headers
22

3-
import com.coditory.kttp.MutableHttpHeaderParams.Companion.headerParamValueToHttpString
3+
import com.coditory.kttp.HttpParams
4+
import com.coditory.kttp.HttpSerializable
5+
import com.coditory.kttp.MutableHttpParams
6+
import com.coditory.kttp.headers.MutableHttpHeaderParams.Companion.headerParamValueToHttpString
7+
import com.coditory.kttp.toMultiMap
48

59
interface HttpHeaderParams : HttpParams, HttpSerializable {
610
override fun toHttpString(builder: Appendable) {
@@ -76,7 +80,7 @@ interface HttpHeaderParams : HttpParams, HttpSerializable {
7680
class MutableHttpHeaderParams private constructor(
7781
private val params: MutableHttpParams,
7882
) : MutableHttpParams, HttpHeaderParams {
79-
constructor() : this(MutableHttpParams.empty())
83+
constructor() : this(MutableHttpParams.Companion.empty())
8084

8185
override fun asMap() = params.asMap()
8286

kttp-api/src/main/kotlin/com/coditory/kttp/HttpHeaderValue.kt renamed to kttp-api/src/main/kotlin/com/coditory/kttp/headers/HttpHeaderValue.kt

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1-
package com.coditory.kttp
1+
package com.coditory.kttp.headers
22

3-
data class HttpHeaderValue(
4-
val items: List<HttpHeaderValueItem>,
3+
import com.coditory.kttp.HttpSerializable
4+
5+
class HttpHeaderValue(
6+
items: List<HttpHeaderValueItem>,
57
) : HttpSerializable {
68
constructor(
79
value: CharSequence,
@@ -19,6 +21,27 @@ data class HttpHeaderValue(
1921
}
2022
}
2123

24+
val items = items.sorted()
25+
26+
fun with(other: HttpHeaderValue): HttpHeaderValue {
27+
return HttpHeaderValue(items + other.items)
28+
}
29+
30+
override fun toString(): String {
31+
return this::class.simpleName + items.toString()
32+
}
33+
34+
override fun equals(other: Any?): Boolean {
35+
if (this === other) return true
36+
if (javaClass != other?.javaClass) return false
37+
other as HttpHeaderValue
38+
return items == other.items
39+
}
40+
41+
override fun hashCode(): Int {
42+
return items.hashCode()
43+
}
44+
2245
companion object {
2346
fun parse(header: CharSequence) = HttpHeaderValueParser.parse(header)
2447
}
@@ -27,12 +50,20 @@ data class HttpHeaderValue(
2750
data class HttpHeaderValueItem(
2851
val value: CharSequence,
2952
val params: HttpHeaderParams = HttpHeaderParams.empty(),
30-
) : HttpSerializable {
53+
) : HttpSerializable, Comparable<HttpHeaderValueItem> {
3154
override fun toHttpString(builder: Appendable) {
3255
headerValueToHttpString(builder, value)
3356
params.toHttpString(builder)
3457
}
3558

59+
fun quality(): Float {
60+
return params["q"]?.toFloatOrNull() ?: 1.0f
61+
}
62+
63+
override fun compareTo(other: HttpHeaderValueItem): Int {
64+
return this.quality().compareTo(other.quality())
65+
}
66+
3667
companion object {
3768
private val quoteRegex = Regex("\"")
3869
internal fun headerValueToHttpString(builder: Appendable, value: CharSequence) {

kttp-api/src/main/kotlin/com/coditory/kttp/HttpHeaders.kt renamed to kttp-api/src/main/kotlin/com/coditory/kttp/headers/HttpHeaders.kt

Lines changed: 42 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
1-
package com.coditory.kttp
1+
package com.coditory.kttp.headers
2+
3+
import com.coditory.kttp.HttpParams
4+
import com.coditory.kttp.HttpSerializable
5+
import com.coditory.kttp.MutableHttpParams
6+
import com.coditory.kttp.toMultiMap
27

38
interface HttpHeaders : HttpParams, HttpSerializable {
49
fun contentType(): ContentType?
10+
fun accept(): ContentType?
511

612
override fun toHttpString(builder: Appendable) {
713
forEachEntry { key, value ->
@@ -189,20 +195,47 @@ interface HttpHeaders : HttpParams, HttpSerializable {
189195
class MutableHttpHeaders private constructor(
190196
private val params: MutableHttpParams,
191197
) : MutableHttpParams, HttpHeaders {
192-
constructor() : this(MutableHttpParams.empty())
198+
constructor() : this(MutableHttpParams.Companion.empty())
199+
200+
private var cache = mutableMapOf<String, Any?>()
201+
202+
private fun getParsed(key: String, parse: (v: String) -> Any?): Any? {
203+
return cache.getOrPut(key) {
204+
val value = get(HttpHeaders.ContentType)
205+
if (value == null) {
206+
null
207+
} else {
208+
parse(value) as Any
209+
}
210+
}
211+
}
193212

194-
private var contentType: ContentType? = null
213+
private fun setParsed(key: String, value: String?) {
214+
if (value == null) {
215+
remove(key)
216+
cache.remove(key)
217+
} else {
218+
set(key, value)
219+
cache.put(key, value)
220+
}
221+
}
195222

196223
override fun contentType(): ContentType? {
197-
if (contentType == null) {
198-
contentType = get(HttpHeaders.ContentType)?.let(ContentType::parse)
199-
}
200-
return contentType
224+
return getParsed(HttpHeaders.ContentType, ContentType::parse)
225+
?.let { it as ContentType }
201226
}
202227

203228
fun contentType(value: ContentType?) {
204-
contentType = value
205-
set(HttpHeaders.ContentType, value?.value)
229+
setParsed(HttpHeaders.ContentType, value?.value)
230+
}
231+
232+
override fun accept(): ContentType? {
233+
return getParsed(HttpHeaders.Accept, ContentType::parse)
234+
?.let { it as ContentType }
235+
}
236+
237+
fun accept(value: ContentType?) {
238+
setParsed(HttpHeaders.Accept, value?.value)
206239
}
207240

208241
override fun asMap() = params.asMap()

kttp-api/src/test/kotlin/com/coditory/kttp/HttpHeaderValueParserSpec.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
package com.coditory.kttp
22

3+
import com.coditory.kttp.headers.HttpHeaderParams
4+
import com.coditory.kttp.headers.HttpHeaderValue
5+
import com.coditory.kttp.headers.HttpHeaderValueItem
36
import io.kotest.core.spec.style.FunSpec
47
import io.kotest.core.tuple
58
import io.kotest.matchers.shouldBe
Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
package com.coditory.kttp.server
22

3-
import com.coditory.kttp.HttpParams
43
import com.coditory.kttp.HttpResponseHead
54
import com.coditory.kttp.HttpStatus
5+
import com.coditory.kttp.headers.HttpHeaders
6+
import kotlinx.coroutines.sync.Mutex
7+
import kotlinx.coroutines.sync.withLock
68
import kotlinx.io.Sink
79
import kotlinx.io.writeString
810

@@ -11,22 +13,26 @@ data class HttpExchange(
1113
val responseBody: Sink,
1214
) {
1315
val attributes = mutableMapOf<String, Any>()
14-
private var sentResponseHead = false
1516

16-
suspend fun sendResponseHead(status: HttpStatus, headers: HttpParams = HttpParams.empty()) {
17+
private val mutex = Mutex()
18+
private var sent = false
19+
20+
suspend fun sendResponseHead(status: HttpStatus, headers: HttpHeaders = HttpHeaders.empty()) {
1721
sendResponseHead(HttpResponseHead(status, headers))
1822
}
1923

2024
suspend fun sendResponseHead(response: HttpResponseHead) {
21-
require(!sentResponseHead) { "Response head was already sent" }
22-
responseBody.writeInt(response.status.code)
23-
responseBody.writeString("\n")
24-
response.headers.forEachEntry { name, value ->
25-
responseBody.writeString(name)
26-
responseBody.writeString(": ")
27-
responseBody.writeString(value)
25+
mutex.withLock {
26+
require(!sent) { "Response head was already sent" }
27+
responseBody.writeInt(response.status.code)
2828
responseBody.writeString("\n")
29+
sent = true
30+
response.headers.forEachEntry { name, value ->
31+
responseBody.writeString(name)
32+
responseBody.writeString(": ")
33+
responseBody.writeString(value)
34+
responseBody.writeString("\n")
35+
}
2936
}
30-
sentResponseHead = true
3137
}
3238
}

0 commit comments

Comments
 (0)