Skip to content

Commit 53398a0

Browse files
authored
Ktor server upgrade (#9088)
* Update to new ktor version #9087 * Update doc #9087 * Cleanup #9087
1 parent 73564bc commit 53398a0

File tree

31 files changed

+476
-405
lines changed

31 files changed

+476
-405
lines changed

docs/generators/kotlin-server.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ These options may be applied as additional-properties (cli) or configOptions (pl
1616
|featureCompression|Adds ability to compress outgoing content using gzip, deflate or custom encoder and thus reduce size of the response.| |true|
1717
|featureConditionalHeaders|Avoid sending content if client already has same content, by checking ETag or LastModified properties.| |false|
1818
|featureHSTS|Avoid sending content if client already has same content, by checking ETag or LastModified properties.| |true|
19+
|featureLocations|Generates routes in a typed way, for both: constructing URLs and reading the parameters.| |true|
1920
|groupId|Generated artifact package's organization (i.e. maven groupId).| |org.openapitools|
2021
|library|library template (sub-template)|<dl><dt>**ktor**</dt><dd>ktor framework</dd></dl>|ktor|
2122
|modelMutable|Create mutable models| |false|

modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/KotlinServerCodegen.java

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,15 +42,17 @@ public class KotlinServerCodegen extends AbstractKotlinCodegen {
4242
private Boolean hstsFeatureEnabled = true;
4343
private Boolean corsFeatureEnabled = false;
4444
private Boolean compressionFeatureEnabled = true;
45+
private Boolean locationsFeatureEnabled = true;
4546

46-
// This is here to potentially warn the user when an option is not supoprted by the target framework.
47+
// This is here to potentially warn the user when an option is not supported by the target framework.
4748
private Map<String, List<String>> optionsSupportedPerFramework = new ImmutableMap.Builder<String, List<String>>()
4849
.put(Constants.KTOR, Arrays.asList(
4950
Constants.AUTOMATIC_HEAD_REQUESTS,
5051
Constants.CONDITIONAL_HEADERS,
5152
Constants.HSTS,
5253
Constants.CORS,
53-
Constants.COMPRESSION
54+
Constants.COMPRESSION,
55+
Constants.LOCATIONS
5456
))
5557
.build();
5658

@@ -85,6 +87,8 @@ public KotlinServerCodegen() {
8587
artifactId = "kotlin-server";
8688
packageName = "org.openapitools.server";
8789

90+
typeMapping.put("array", "kotlin.collections.List");
91+
8892
// cliOptions default redefinition need to be updated
8993
updateOption(CodegenConstants.ARTIFACT_ID, this.artifactId);
9094
updateOption(CodegenConstants.PACKAGE_NAME, this.packageName);
@@ -110,6 +114,7 @@ public KotlinServerCodegen() {
110114
addSwitch(Constants.HSTS, Constants.HSTS_DESC, getHstsFeatureEnabled());
111115
addSwitch(Constants.CORS, Constants.CORS_DESC, getCorsFeatureEnabled());
112116
addSwitch(Constants.COMPRESSION, Constants.COMPRESSION_DESC, getCompressionFeatureEnabled());
117+
addSwitch(Constants.LOCATIONS, Constants.LOCATIONS_DESC, getLocationsFeatureEnabled());
113118
}
114119

115120
public Boolean getAutoHeadFeatureEnabled() {
@@ -156,6 +161,14 @@ public void setHstsFeatureEnabled(Boolean hstsFeatureEnabled) {
156161
this.hstsFeatureEnabled = hstsFeatureEnabled;
157162
}
158163

164+
public Boolean getLocationsFeatureEnabled() {
165+
return locationsFeatureEnabled;
166+
}
167+
168+
public void setLocationsFeatureEnabled(Boolean locationsFeatureEnabled) {
169+
this.locationsFeatureEnabled = locationsFeatureEnabled;
170+
}
171+
159172
public String getName() {
160173
return "kotlin-server";
161174
}
@@ -209,6 +222,12 @@ public void processOpts() {
209222
additionalProperties.put(Constants.COMPRESSION, getCompressionFeatureEnabled());
210223
}
211224

225+
if (additionalProperties.containsKey(Constants.LOCATIONS)) {
226+
setLocationsFeatureEnabled(convertPropertyToBooleanAndWriteBack(Constants.LOCATIONS));
227+
} else {
228+
additionalProperties.put(Constants.LOCATIONS, getLocationsFeatureEnabled());
229+
}
230+
212231
boolean generateApis = additionalProperties.containsKey(CodegenConstants.GENERATE_APIS) && (Boolean) additionalProperties.get(CodegenConstants.GENERATE_APIS);
213232
String packageFolder = (sourceFolder + File.separator + packageName).replace(".", File.separator);
214233
String resourcesFolder = "src/main/resources"; // not sure this can be user configurable.
@@ -223,7 +242,7 @@ public void processOpts() {
223242
supportingFiles.add(new SupportingFile("AppMain.kt.mustache", packageFolder, "AppMain.kt"));
224243
supportingFiles.add(new SupportingFile("Configuration.kt.mustache", packageFolder, "Configuration.kt"));
225244

226-
if (generateApis) {
245+
if (generateApis && locationsFeatureEnabled) {
227246
supportingFiles.add(new SupportingFile("Paths.kt.mustache", packageFolder, "Paths.kt"));
228247
}
229248

@@ -247,6 +266,8 @@ public static class Constants {
247266
public final static String CORS_DESC = "Ktor by default provides an interceptor for implementing proper support for Cross-Origin Resource Sharing (CORS). See enable-cors.org.";
248267
public final static String COMPRESSION = "featureCompression";
249268
public final static String COMPRESSION_DESC = "Adds ability to compress outgoing content using gzip, deflate or custom encoder and thus reduce size of the response.";
269+
public final static String LOCATIONS = "featureLocations";
270+
public final static String LOCATIONS_DESC = "Generates routes in a typed way, for both: constructing URLs and reading the parameters.";
250271
}
251272

252273
@Override

modules/openapi-generator/src/main/resources/kotlin-server/README.mustache

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22

33
## Requires
44

5-
* Kotlin 1.1.2
6-
* Gradle 3.3
5+
* Kotlin 1.4.31
6+
* Gradle 6.8.2
77

88
## Build
99

modules/openapi-generator/src/main/resources/kotlin-server/data_class.mustache

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import java.io.Serializable
1515
{{#parcelizeModels}}
1616
@Parcelize
1717
{{/parcelizeModels}}
18-
data class {{classname}} (
18+
data class {{classname}}(
1919
{{#requiredVars}}
2020
{{>data_class_req_var}}{{^-last}},
2121
{{/-last}}{{/requiredVars}}{{#hasRequired}}{{#hasOptional}},

modules/openapi-generator/src/main/resources/kotlin-server/enum_class.mustache

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
* {{{description}}}
33
* Values: {{#allowableValues}}{{#enumVars}}{{&name}}{{^-last}},{{/-last}}{{/enumVars}}{{/allowableValues}}
44
*/
5-
enum class {{classname}}(val value: {{dataType}}){
5+
enum class {{classname}}(val value: {{dataType}}) {
66
{{#allowableValues}}{{#enumVars}}
77
{{&name}}({{{value}}}){{^-last}},{{/-last}}{{#-last}};{{/-last}}
88
{{/enumVars}}{{/allowableValues}}
Lines changed: 73 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,85 +1,105 @@
11
package {{packageName}}.infrastructure
22

3-
import io.ktor.application.ApplicationCall
4-
import io.ktor.application.call
5-
import io.ktor.auth.Authentication
6-
import io.ktor.auth.AuthenticationFailedCause
7-
import io.ktor.auth.AuthenticationPipeline
8-
import io.ktor.auth.AuthenticationProvider
9-
import io.ktor.auth.Credential
10-
import io.ktor.auth.Principal
11-
import io.ktor.auth.UnauthorizedResponse
12-
import io.ktor.http.auth.HeaderValueEncoding
13-
import io.ktor.http.auth.HttpAuthHeader
14-
import io.ktor.request.ApplicationRequest
15-
import io.ktor.response.respond
3+
import io.ktor.application.*
4+
import io.ktor.auth.*
5+
import io.ktor.http.auth.*
6+
import io.ktor.request.*
7+
import io.ktor.response.*
168

179
enum class ApiKeyLocation(val location: String) {
1810
QUERY("query"),
1911
HEADER("header")
2012
}
21-
data class ApiKeyCredential(val value: String): Credential
22-
data class ApiPrincipal(val apiKeyCredential: ApiKeyCredential?) : Principal
23-
2413

14+
data class ApiKeyCredential(val value: String) : Credential
15+
data class ApiPrincipal(val apiKeyCredential: ApiKeyCredential?) : Principal
2516

2617
/**
2718
* Represents a Api Key authentication provider
2819
* @param name is the name of the provider, or `null` for a default provider
2920
*/
30-
class ApiKeyAuthenticationProvider(name: String?) : AuthenticationProvider(name) {
31-
internal var authenticationFunction: suspend ApplicationCall.(ApiKeyCredential) -> Principal? = { null }
21+
class ApiKeyAuthenticationProvider(configuration: Configuration) : AuthenticationProvider(configuration) {
3222
33-
var apiKeyName: String = "";
23+
private val authenticationFunction = configuration.authenticationFunction
3424
35-
var apiKeyLocation: ApiKeyLocation = ApiKeyLocation.QUERY;
25+
private val apiKeyName: String = configuration.apiKeyName
3626
37-
/**
38-
* Sets a validation function that will check given [ApiKeyCredential] instance and return [Principal],
39-
* or null if credential does not correspond to an authenticated principal
40-
*/
41-
fun validate(body: suspend ApplicationCall.(ApiKeyCredential) -> Principal?) {
42-
authenticationFunction = body
43-
}
44-
}
27+
private val apiKeyLocation: ApiKeyLocation = configuration.apiKeyLocation
4528
46-
fun Authentication.Configuration.apiKeyAuth(name: String? = null, configure: ApiKeyAuthenticationProvider.() -> Unit) {
47-
val provider = ApiKeyAuthenticationProvider(name).apply(configure)
48-
val apiKeyName = provider.apiKeyName
49-
val apiKeyLocation = provider.apiKeyLocation
50-
val authenticate = provider.authenticationFunction
29+
internal fun install() {
30+
pipeline.intercept(AuthenticationPipeline.RequestAuthentication) { context ->
31+
val credentials = call.request.apiKeyAuthenticationCredentials(apiKeyName, apiKeyLocation)
32+
val principal = credentials?.let { authenticationFunction(call, it) }
5133

52-
provider.pipeline.intercept(AuthenticationPipeline.RequestAuthentication) { context ->
53-
val credentials = call.request.apiKeyAuthenticationCredentials(apiKeyName, apiKeyLocation)
54-
val principal = credentials?.let { authenticate(call, it) }
34+
val cause = when {
35+
credentials == null -> AuthenticationFailedCause.NoCredentials
36+
principal == null -> AuthenticationFailedCause.InvalidCredentials
37+
else -> null
38+
}
5539

56-
val cause = when {
57-
credentials == null -> AuthenticationFailedCause.NoCredentials
58-
principal == null -> AuthenticationFailedCause.InvalidCredentials
59-
else -> null
60-
}
40+
if (cause != null) {
41+
context.challenge(apiKeyName, cause) {
42+
call.respond(
43+
UnauthorizedResponse(
44+
HttpAuthHeader.Parameterized(
45+
"API_KEY",
46+
mapOf("key" to apiKeyName),
47+
HeaderValueEncoding.QUOTED_ALWAYS
48+
)
49+
)
50+
)
51+
it.complete()
52+
}
53+
}
6154

62-
if (cause != null) {
63-
context.challenge(apiKeyName, cause) {
64-
// TODO: Verify correct response structure here.
65-
call.respond(UnauthorizedResponse(HttpAuthHeader.Parameterized("API_KEY", mapOf("key" to apiKeyName), HeaderValueEncoding.QUOTED_ALWAYS)))
66-
it.complete()
55+
if (principal != null) {
56+
context.principal(principal)
6757
}
6858
}
59+
}
60+
61+
class Configuration internal constructor(name: String?) : AuthenticationProvider.Configuration(name) {
62+
63+
internal var authenticationFunction: suspend ApplicationCall.(ApiKeyCredential) -> Principal? = {
64+
throw NotImplementedError(
65+
"Api Key auth validate function is not specified. Use apiKeyAuth { validate { ... } } to fix."
66+
)
67+
}
6968

70-
if (principal != null) {
71-
context.principal(principal)
69+
var apiKeyName: String = ""
70+
71+
var apiKeyLocation: ApiKeyLocation = ApiKeyLocation.QUERY
72+
73+
/**
74+
* Sets a validation function that will check given [ApiKeyCredential] instance and return [Principal],
75+
* or null if credential does not correspond to an authenticated principal
76+
*/
77+
fun validate(body: suspend ApplicationCall.(ApiKeyCredential) -> Principal?) {
78+
authenticationFunction = body
7279
}
7380
}
7481
}
7582

76-
fun ApplicationRequest.apiKeyAuthenticationCredentials(apiKeyName: String, apiKeyLocation: ApiKeyLocation): ApiKeyCredential? {
77-
val value: String? = when(apiKeyLocation) {
83+
fun Authentication.Configuration.apiKeyAuth(
84+
name: String? = null,
85+
configure: ApiKeyAuthenticationProvider.Configuration.() -> Unit
86+
) {
87+
val configuration = ApiKeyAuthenticationProvider.Configuration(name).apply(configure)
88+
val provider = ApiKeyAuthenticationProvider(configuration)
89+
provider.install()
90+
register(provider)
91+
}
92+
93+
fun ApplicationRequest.apiKeyAuthenticationCredentials(
94+
apiKeyName: String,
95+
apiKeyLocation: ApiKeyLocation
96+
): ApiKeyCredential? {
97+
val value: String? = when (apiKeyLocation) {
7898
ApiKeyLocation.QUERY -> this.queryParameters[apiKeyName]
7999
ApiKeyLocation.HEADER -> this.headers[apiKeyName]
80100
}
81-
when (value) {
82-
null -> return null
83-
else -> return ApiKeyCredential(value)
101+
return when (value) {
102+
null -> null
103+
else -> ApiKeyCredential(value)
84104
}
85105
}

modules/openapi-generator/src/main/resources/kotlin-server/libraries/ktor/AppMain.kt.mustache

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -28,39 +28,40 @@ import io.ktor.features.HSTS
2828
{{/featureHSTS}}
2929
import io.ktor.gson.GsonConverter
3030
import io.ktor.http.ContentType
31+
{{#featureLocations}}
3132
import io.ktor.locations.KtorExperimentalLocationsAPI
3233
import io.ktor.locations.Locations
33-
import io.ktor.metrics.Metrics
34+
{{/featureLocations}}
3435
import io.ktor.routing.Routing
3536
import java.util.concurrent.TimeUnit
36-
import io.ktor.util.KtorExperimentalAPI
3737
{{#hasAuthMethods}}
3838
import io.ktor.auth.Authentication
3939
import io.ktor.auth.oauth
40+
import io.ktor.metrics.dropwizard.DropwizardMetrics
4041
import org.openapitools.server.infrastructure.ApiKeyCredential
4142
import org.openapitools.server.infrastructure.ApiPrincipal
4243
import org.openapitools.server.infrastructure.apiKeyAuth
4344
{{/hasAuthMethods}}
4445
{{#generateApis}}{{#apiInfo}}{{#apis}}import {{apiPackage}}.{{classname}}
4546
{{/apis}}{{/apiInfo}}{{/generateApis}}
4647

47-
@KtorExperimentalAPI
4848
internal val settings = HoconApplicationConfig(ConfigFactory.defaultApplication(HTTP::class.java.classLoader))
4949

5050
object HTTP {
5151
val client = HttpClient(Apache)
5252
}
5353

54-
@KtorExperimentalAPI
54+
{{#featureLocations}}
5555
@KtorExperimentalLocationsAPI
56+
{{/featureLocations}}
5657
fun Application.main() {
5758
install(DefaultHeaders)
58-
install(Metrics) {
59+
install(DropwizardMetrics) {
5960
val reporter = Slf4jReporter.forRegistry(registry)
60-
.outputTo(log)
61-
.convertRatesTo(TimeUnit.SECONDS)
62-
.convertDurationsTo(TimeUnit.MILLISECONDS)
63-
.build()
61+
.outputTo(log)
62+
.convertRatesTo(TimeUnit.SECONDS)
63+
.convertDurationsTo(TimeUnit.MILLISECONDS)
64+
.build()
6465
reporter.start(10, TimeUnit.SECONDS)
6566
}
6667
{{#generateApis}}
@@ -82,7 +83,9 @@ fun Application.main() {
8283
{{#featureCompression}}
8384
install(Compression, ApplicationCompressionConfiguration()) // see http://ktor.io/features/compression.html
8485
{{/featureCompression}}
86+
{{#featureLocations}}
8587
install(Locations) // see http://ktor.io/features/locations.html
88+
{{/featureLocations}}
8689
{{#hasAuthMethods}}
8790
install(Authentication) {
8891
{{#authMethods}}
@@ -117,8 +120,8 @@ fun Application.main() {
117120
client = HttpClient(Apache)
118121
providerLookup = { ApplicationAuthProviders["{{{name}}}"] }
119122
urlProvider = { _ ->
120-
// TODO: define a callback url here.
121-
"/"
123+
// TODO: define a callback url here.
124+
"/"
122125
}
123126
}
124127
{{/bodyAllowed}}
@@ -138,8 +141,7 @@ fun Application.main() {
138141

139142
{{/generateApis}}
140143

141-
environment.monitor.subscribe(ApplicationStopping)
142-
{
144+
environment.monitor.subscribe(ApplicationStopping) {
143145
HTTP.client.close()
144146
}
145147
}

modules/openapi-generator/src/main/resources/kotlin-server/libraries/ktor/Configuration.kt.mustache

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,7 @@ import io.ktor.features.deflate
88
import io.ktor.features.gzip
99
import io.ktor.features.minimumSize
1010
import io.ktor.http.HttpMethod
11-
import io.ktor.util.KtorExperimentalAPI
12-
import java.time.Duration
13-
import java.util.concurrent.Executors
14-
15-
import {{packageName}}.settings
11+
import java.util.concurrent.TimeUnit
1612

1713
{{#featureCORS}}
1814
/**
@@ -49,7 +45,7 @@ internal fun ApplicationCORSConfiguration(): CORS.Configuration.() -> Unit {
4945
*/
5046
internal fun ApplicationHstsConfiguration(): HSTS.Configuration.() -> Unit {
5147
return {
52-
maxAge = Duration.ofDays(365)
48+
maxAgeInSeconds = TimeUnit.DAYS.toSeconds(365)
5349
includeSubDomains = true
5450
preload = false
5551
@@ -82,7 +78,6 @@ internal fun ApplicationCompressionConfiguration(): Compression.Configuration.()
8278
{{/featureCompression}}
8379

8480
// Defines authentication mechanisms used throughout the application.
85-
@KtorExperimentalAPI
8681
val ApplicationAuthProviders: Map<String, OAuthServerSettings> = listOf<OAuthServerSettings>(
8782
{{#authMethods}}
8883
{{#isOAuth}}
@@ -109,6 +104,3 @@ val ApplicationAuthProviders: Map<String, OAuthServerSettings> = listOf<OAuthSer
109104
// defaultScopes = listOf("public_profile")
110105
// )
111106
).associateBy { it.name }
112-
113-
// Provides an application-level fixed thread pool on which to execute coroutines (mainly)
114-
internal val ApplicationExecutors = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors() * 4)

0 commit comments

Comments
 (0)