Reduce required JDK version for DefaultHeaders feature

This commit is contained in:
Sergey Mashkov
2019-10-01 02:00:44 +03:00
parent d512e310cf
commit b6b99cd1fa
4 changed files with 106 additions and 22 deletions

View File

@@ -507,6 +507,7 @@ public final class io/ktor/util/date/DateJvmKt {
public static final fun GMTDate (IIIILio/ktor/util/date/Month;I)Lio/ktor/util/date/GMTDate;
public static final fun GMTDate (Ljava/lang/Long;)Lio/ktor/util/date/GMTDate;
public static synthetic fun GMTDate$default (Ljava/lang/Long;ILjava/lang/Object;)Lio/ktor/util/date/GMTDate;
public static final fun toDate (Ljava/util/Calendar;Ljava/lang/Long;)Lio/ktor/util/date/GMTDate;
public static final fun toJvmDate (Lio/ktor/util/date/GMTDate;)Ljava/util/Date;
}

View File

@@ -8,7 +8,9 @@ import io.ktor.application.*
import io.ktor.http.*
import io.ktor.response.*
import io.ktor.util.*
import java.time.*
import io.ktor.util.date.*
import kotlinx.atomicfu.*
import java.util.*
/**
* Adds standard HTTP headers `Date` and `Server` and provides ability to specify other headers
@@ -16,12 +18,10 @@ import java.time.*
*/
class DefaultHeaders(config: Configuration) {
private val headers = config.headers.build()
private val UTC = ZoneId.of("UTC")!! // it is very important to get it like that
private val zoneUTCRules = UTC.rules
private val clock = config.clock
private var cachedDateTimeStamp: Long = 0L
@Volatile private var cachedDateText: String = ZonedDateTime.now(GreenwichMeanTime).toHttpDateString()
private val cachedDateText = atomic("")
/**
* Configuration for [DefaultHeaders] feature.
@@ -36,6 +36,12 @@ class DefaultHeaders(config: Configuration) {
* Adds standard header property [name] with the specified [value].
*/
fun header(name: String, value: String) = headers.append(name, value)
/**
* Provides time source. Useful for testing.
*/
@InternalAPI
var clock: () -> Long = { System.currentTimeMillis() }
}
private fun intercept(call: ApplicationCall) {
@@ -43,31 +49,33 @@ class DefaultHeaders(config: Configuration) {
headers.forEach { name, value -> value.forEach { call.response.header(name, it) } }
}
// ZonedDateTime.now allocates too much so we reimplement it
private fun now(): ZonedDateTime {
// we shouldn't use ZoneOffset.UTC here otherwise we get to many allocations inside of Java Time implementation
val instant = Clock.system(UTC).instant()
val offset = zoneUTCRules.getOffset(instant)
val ldt = LocalDateTime.ofEpochSecond(instant.epochSecond, instant.nano, offset)
return ZonedDateTime.ofInstant(ldt, offset, UTC)
}
private fun appendDateHeader(call: ApplicationCall) {
val captureCached = cachedDateTimeStamp
val currentTimeStamp = System.currentTimeMillis()
if (captureCached + 1000 < currentTimeStamp) {
val currentTimeStamp = clock()
if (captureCached + DATE_CACHE_TIMEOUT_MILLISECONDS <= currentTimeStamp) {
cachedDateTimeStamp = currentTimeStamp
cachedDateText = now().toHttpDateString()
cachedDateText.value = now(currentTimeStamp).toHttpDate()
}
call.response.header("Date", cachedDateText)
call.response.header(HttpHeaders.Date, cachedDateText.value)
}
private fun now(time: Long): GMTDate {
return calendar.get().toDate(time)
}
/**
* Installable feature for [DefaultHeaders].
*/
companion object Feature : ApplicationFeature<Application, Configuration, DefaultHeaders> {
private val GreenwichMeanTime: ZoneId = ZoneId.of("GMT")
private const val DATE_CACHE_TIMEOUT_MILLISECONDS = 1000
private val GMT_TIMEZONE = TimeZone.getTimeZone("GMT")!!
private val calendar = object : ThreadLocal<Calendar>() {
override fun initialValue(): Calendar {
return Calendar.getInstance(GMT_TIMEZONE, Locale.ROOT)
}
}
override val key = AttributeKey<DefaultHeaders>("Default Headers")

View File

@@ -0,0 +1,74 @@
/*
* 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.server.features
import io.ktor.application.*
import io.ktor.features.*
import io.ktor.http.*
import io.ktor.response.*
import io.ktor.server.testing.*
import kotlin.test.*
class DefaultHeadersTest {
@Test
fun testDate(): Unit = withTestApplication {
var now = 1569882841014
application.install(DefaultHeaders) {
clock = { now }
}
application.intercept(ApplicationCallPipeline.Call) {
call.respondText("OK")
}
handleRequest(HttpMethod.Get, "/").let { call ->
assertEquals("Mon, 30 Sep 2019 22:34:01 GMT", call.response.headers[HttpHeaders.Date])
}
now += 999
handleRequest(HttpMethod.Get, "/").let { call ->
assertEquals("Mon, 30 Sep 2019 22:34:01 GMT", call.response.headers[HttpHeaders.Date])
}
now++
handleRequest(HttpMethod.Get, "/").let { call ->
assertEquals("Mon, 30 Sep 2019 22:34:02 GMT", call.response.headers[HttpHeaders.Date])
}
}
@Test
fun testCustomHeader(): Unit = withTestApplication {
application.install(DefaultHeaders) {
header("X-Test", "123")
}
handleRequest(HttpMethod.Get, "/").let { call ->
assertEquals("123", call.response.headers["X-Test"])
}
}
@Test
fun testDefaultServerHeader(): Unit = withTestApplication {
application.install(DefaultHeaders) {
}
handleRequest(HttpMethod.Get, "/").let { call ->
assertTrue { "ktor" in call.response.headers[HttpHeaders.Server]!! }
}
}
@Test
fun testCustomServerHeader(): Unit = withTestApplication {
application.install(DefaultHeaders) {
header(HttpHeaders.Server, "MyServer")
}
handleRequest(HttpMethod.Get, "/").let { call ->
assertEquals("MyServer", call.response.headers[HttpHeaders.Server])
}
}
}

View File

@@ -4,6 +4,7 @@
package io.ktor.util.date
import io.ktor.util.*
import java.util.*
private val GMT_TIMEZONE = TimeZone.getTimeZone("GMT")
@@ -31,8 +32,8 @@ actual fun GMTDate(
set(Calendar.MILLISECOND, 0)
}.toDate(timestamp = null)
private fun Calendar.toDate(timestamp: Long?): GMTDate {
@InternalAPI
fun Calendar.toDate(timestamp: Long?): GMTDate {
timestamp?.let { timeInMillis = it }
val seconds = get(Calendar.SECOND)