Skip to content

Commit f402126

Browse files
authored
[Kotlin][client] fix file upload (#5548)
* [kotlin] fix file upload * [kotlin] fix file upload * [kotlin] fix file upload * [kotlin][client] fix jackson integration * [kotlin] fix file upload * [kotlin] fix file upload
1 parent ce8cdcd commit f402126

File tree

22 files changed

+856
-89
lines changed

22 files changed

+856
-89
lines changed

modules/openapi-generator/src/main/resources/kotlin-client/api.mustache

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ import {{packageName}}.infrastructure.toMultiValue
3232
@Suppress("UNCHECKED_CAST"){{/returnType}}
3333
@Throws(UnsupportedOperationException::class, ClientException::class, ServerException::class)
3434
fun {{operationId}}({{#allParams}}{{{paramName}}}: {{{dataType}}}{{^required}}?{{/required}}{{#hasMore}}, {{/hasMore}}{{/allParams}}) : {{#returnType}}{{{returnType}}}{{#nullableReturnType}}?{{/nullableReturnType}}{{/returnType}}{{^returnType}}Unit{{/returnType}} {
35-
val localVariableBody: kotlin.Any? = {{#hasBodyParam}}{{#bodyParams}}{{{paramName}}}{{/bodyParams}}{{/hasBodyParam}}{{^hasBodyParam}}{{^hasFormParams}}null{{/hasFormParams}}{{#hasFormParams}}mapOf({{#formParams}}"{{{baseName}}}" to "${{{paramName}}}"{{#hasMore}}, {{/hasMore}}{{/formParams}}){{/hasFormParams}}{{/hasBodyParam}}
35+
val localVariableBody: kotlin.Any? = {{#hasBodyParam}}{{#bodyParams}}{{{paramName}}}{{/bodyParams}}{{/hasBodyParam}}{{^hasBodyParam}}{{^hasFormParams}}null{{/hasFormParams}}{{#hasFormParams}}mapOf({{#formParams}}"{{{baseName}}}" to {{{paramName}}}{{#hasMore}}, {{/hasMore}}{{/formParams}}){{/hasFormParams}}{{/hasBodyParam}}
3636
val localVariableQuery: MultiValueMap = {{^hasQueryParams}}mutableMapOf()
3737
{{/hasQueryParams}}{{#hasQueryParams}}mutableMapOf<kotlin.String, List<kotlin.String>>()
3838
.apply {
@@ -55,7 +55,7 @@ import {{packageName}}.infrastructure.toMultiValue
5555
{{/queryParams}}
5656
}
5757
{{/hasQueryParams}}
58-
val localVariableHeaders: MutableMap<String, String> = mutableMapOf({{#hasFormParams}}"Content-Type" to {{^consumes}}"multipart/form-data"{{/consumes}}{{#consumes.0}}"{{MediaType}}"{{/consumes.0}}{{/hasFormParams}}{{^hasHeaderParams}}){{/hasHeaderParams}}{{#hasHeaderParams}}{{#hasFormParams}}, {{/hasFormParams}}{{#headerParams}}"{{baseName}}" to {{#isContainer}}{{{paramName}}}.joinToString(separator = collectionDelimiter("{{collectionFormat}}")){{/isContainer}}{{^isContainer}}{{{paramName}}}.toString(){{/isContainer}}{{#hasMore}}, {{/hasMore}}{{/headerParams}}){{/hasHeaderParams}}
58+
val localVariableHeaders: MutableMap<String, String> = mutableMapOf({{#hasFormParams}}"Content-Type" to {{^consumes}}"multipart/form-data"{{/consumes}}{{#consumes.0}}"{{{mediaType}}}"{{/consumes.0}}{{/hasFormParams}}{{^hasHeaderParams}}){{/hasHeaderParams}}{{#hasHeaderParams}}{{#hasFormParams}}, {{/hasFormParams}}{{#headerParams}}"{{baseName}}" to {{#isContainer}}{{{paramName}}}.joinToString(separator = collectionDelimiter("{{collectionFormat}}")){{/isContainer}}{{^isContainer}}{{{paramName}}}.toString(){{/isContainer}}{{#hasMore}}, {{/hasMore}}{{/headerParams}}){{/hasHeaderParams}}
5959
val localVariableConfig = RequestConfig(
6060
RequestMethod.{{httpMethod}},
6161
"{{path}}"{{#pathParams}}.replace("{"+"{{baseName}}"+"}", "${{{paramName}}}"){{/pathParams}},

modules/openapi-generator/src/main/resources/kotlin-client/libraries/jvm-okhttp/infrastructure/ApiClient.kt.mustache

Lines changed: 95 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,25 @@ import okhttp3.ResponseBody
2222
import okhttp3.MediaType.Companion.toMediaTypeOrNull
2323
{{/jvm-okhttp4}}
2424
import okhttp3.Request
25+
import okhttp3.Headers
26+
import okhttp3.MultipartBody
2527
import java.io.File
28+
import java.net.URLConnection
29+
import java.util.Date
30+
{{^threetenbp}}
31+
import java.time.LocalDate
32+
import java.time.LocalDateTime
33+
import java.time.LocalTime
34+
import java.time.OffsetDateTime
35+
import java.time.OffsetTime
36+
{{/threetenbp}}
37+
{{#threetenbp}}
38+
import org.threeten.bp.LocalDate
39+
import org.threeten.bp.LocalDateTime
40+
import org.threeten.bp.LocalTime
41+
import org.threeten.bp.OffsetDateTime
42+
import org.threeten.bp.OffsetTime
43+
{{/threetenbp}}
2644

2745
{{#nonPublicApi}}internal {{/nonPublicApi}}open class ApiClient(val baseUrl: String) {
2846
{{#nonPublicApi}}internal {{/nonPublicApi}}companion object {
@@ -49,6 +67,17 @@ import java.io.File
4967
val builder: OkHttpClient.Builder = OkHttpClient.Builder()
5068
}
5169

70+
/**
71+
* Guess Content-Type header from the given file (defaults to "application/octet-stream").
72+
*
73+
* @param file The given file
74+
* @return The guessed Content-Type
75+
*/
76+
protected fun guessContentTypeFromFile(file: File): String {
77+
val contentType = URLConnection.guessContentTypeFromName(file.name)
78+
return contentType ?: "application/octet-stream"
79+
}
80+
5281
protected inline fun <reified T> requestBody(content: T, mediaType: String = JsonMediaType): RequestBody =
5382
when {
5483
{{#jvm-okhttp3}}
@@ -61,12 +90,50 @@ import java.io.File
6190
mediaType.toMediaTypeOrNull()
6291
)
6392
{{/jvm-okhttp4}}
64-
mediaType == FormDataMediaType || mediaType == FormUrlEncMediaType -> {
93+
mediaType == FormDataMediaType -> {
94+
MultipartBody.Builder()
95+
.setType(MultipartBody.FORM)
96+
.apply {
97+
// content's type *must* be Map<String, Any?>
98+
@Suppress("UNCHECKED_CAST")
99+
(content as Map<String, Any?>).forEach { (key, value) ->
100+
if (value is File) {
101+
val partHeaders = Headers.{{#jvm-okhttp3}}of{{/jvm-okhttp3}}{{#jvm-okhttp4}}headersOf{{/jvm-okhttp4}}(
102+
"Content-Disposition",
103+
"form-data; name=\"$key\"; filename=\"${value.name}\""
104+
)
105+
{{#jvm-okhttp3}}
106+
val fileMediaType = MediaType.parse(guessContentTypeFromFile(value))
107+
addPart(partHeaders, RequestBody.create(fileMediaType, value))
108+
{{/jvm-okhttp3}}
109+
{{#jvm-okhttp4}}
110+
val fileMediaType = guessContentTypeFromFile(value).toMediaTypeOrNull()
111+
addPart(partHeaders, value.asRequestBody(fileMediaType))
112+
{{/jvm-okhttp4}}
113+
} else {
114+
val partHeaders = Headers.{{#jvm-okhttp3}}of{{/jvm-okhttp3}}{{#jvm-okhttp4}}headersOf{{/jvm-okhttp4}}(
115+
"Content-Disposition",
116+
"form-data; name=\"$key\""
117+
)
118+
addPart(
119+
partHeaders,
120+
{{#jvm-okhttp3}}
121+
RequestBody.create(null, parameterToString(value))
122+
{{/jvm-okhttp3}}
123+
{{#jvm-okhttp4}}
124+
parameterToString(value).toRequestBody(null)
125+
{{/jvm-okhttp4}}
126+
)
127+
}
128+
}
129+
}.build()
130+
}
131+
mediaType == FormUrlEncMediaType -> {
65132
FormBody.Builder().apply {
66-
// content's type *must* be Map<String, Any>
133+
// content's type *must* be Map<String, Any?>
67134
@Suppress("UNCHECKED_CAST")
68-
(content as Map<String,String>).forEach { (key, value) ->
69-
add(key, value)
135+
(content as Map<String, Any?>).forEach { (key, value) ->
136+
add(key, parameterToString(value))
70137
}
71138
}.build()
72139
}
@@ -79,7 +146,7 @@ import java.io.File
79146
MediaType.parse(mediaType), Serializer.gson.toJson(content, T::class.java)
80147
{{/gson}}
81148
{{#jackson}}
82-
MediaType.parse(mediaType), Serializer.jackson.toJson(content, T::class.java)
149+
MediaType.parse(mediaType), Serializer.jacksonObjectMapper.writeValueAsString(content)
83150
{{/jackson}}
84151
)
85152
{{/jvm-okhttp3}}
@@ -254,7 +321,26 @@ import java.io.File
254321
}
255322
}
256323

257-
{{^jackson}}
324+
protected fun parameterToString(value: Any?): String {
325+
when (value) {
326+
null -> {
327+
return ""
328+
}
329+
is Array<*> -> {
330+
return toMultiValue(value, "csv").toString()
331+
}
332+
is Iterable<*> -> {
333+
return toMultiValue(value, "csv").toString()
334+
}
335+
is OffsetDateTime, is OffsetTime, is LocalDateTime, is LocalDate, is LocalTime, is Date -> {
336+
return parseDateToQueryString<Any>(value)
337+
}
338+
else -> {
339+
return value.toString()
340+
}
341+
}
342+
}
343+
258344
protected inline fun <reified T: Any> parseDateToQueryString(value : T): String {
259345
{{#toJson}}
260346
/*
@@ -269,10 +355,12 @@ import java.io.File
269355
{{#gson}}
270356
return Serializer.gson.toJson(value, T::class.java).replace("\"", "")
271357
{{/gson}}
358+
{{#jackson}}
359+
return Serializer.jacksonObjectMapper.writeValueAsString(value).replace("\"", "")
360+
{{/jackson}}
272361
{{/toJson}}
273362
{{^toJson}}
274363
return value.toString()
275364
{{/toJson}}
276365
}
277-
{{/jackson}}
278366
}

samples/client/petstore/kotlin-gson/src/main/kotlin/org/openapitools/client/apis/PetApi.kt

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -291,9 +291,9 @@ class PetApi(basePath: kotlin.String = "http://petstore.swagger.io/v2") : ApiCli
291291
*/
292292
@Throws(UnsupportedOperationException::class, ClientException::class, ServerException::class)
293293
fun updatePetWithForm(petId: kotlin.Long, name: kotlin.String?, status: kotlin.String?) : Unit {
294-
val localVariableBody: kotlin.Any? = mapOf("name" to "$name", "status" to "$status")
294+
val localVariableBody: kotlin.Any? = mapOf("name" to name, "status" to status)
295295
val localVariableQuery: MultiValueMap = mutableMapOf()
296-
val localVariableHeaders: MutableMap<String, String> = mutableMapOf("Content-Type" to "")
296+
val localVariableHeaders: MutableMap<String, String> = mutableMapOf("Content-Type" to "application/x-www-form-urlencoded")
297297
val localVariableConfig = RequestConfig(
298298
RequestMethod.POST,
299299
"/pet/{petId}".replace("{"+"petId"+"}", "$petId"),
@@ -334,9 +334,9 @@ class PetApi(basePath: kotlin.String = "http://petstore.swagger.io/v2") : ApiCli
334334
@Suppress("UNCHECKED_CAST")
335335
@Throws(UnsupportedOperationException::class, ClientException::class, ServerException::class)
336336
fun uploadFile(petId: kotlin.Long, additionalMetadata: kotlin.String?, file: java.io.File?) : ApiResponse {
337-
val localVariableBody: kotlin.Any? = mapOf("additionalMetadata" to "$additionalMetadata", "file" to "$file")
337+
val localVariableBody: kotlin.Any? = mapOf("additionalMetadata" to additionalMetadata, "file" to file)
338338
val localVariableQuery: MultiValueMap = mutableMapOf()
339-
val localVariableHeaders: MutableMap<String, String> = mutableMapOf("Content-Type" to "")
339+
val localVariableHeaders: MutableMap<String, String> = mutableMapOf("Content-Type" to "multipart/form-data")
340340
val localVariableConfig = RequestConfig(
341341
RequestMethod.POST,
342342
"/pet/{petId}/uploadImage".replace("{"+"petId"+"}", "$petId"),

samples/client/petstore/kotlin-gson/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt

Lines changed: 71 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,16 @@ import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
1010
import okhttp3.ResponseBody
1111
import okhttp3.MediaType.Companion.toMediaTypeOrNull
1212
import okhttp3.Request
13+
import okhttp3.Headers
14+
import okhttp3.MultipartBody
1315
import java.io.File
16+
import java.net.URLConnection
17+
import java.util.Date
18+
import java.time.LocalDate
19+
import java.time.LocalDateTime
20+
import java.time.LocalTime
21+
import java.time.OffsetDateTime
22+
import java.time.OffsetTime
1423

1524
open class ApiClient(val baseUrl: String) {
1625
companion object {
@@ -37,17 +46,55 @@ open class ApiClient(val baseUrl: String) {
3746
val builder: OkHttpClient.Builder = OkHttpClient.Builder()
3847
}
3948

49+
/**
50+
* Guess Content-Type header from the given file (defaults to "application/octet-stream").
51+
*
52+
* @param file The given file
53+
* @return The guessed Content-Type
54+
*/
55+
protected fun guessContentTypeFromFile(file: File): String {
56+
val contentType = URLConnection.guessContentTypeFromName(file.name)
57+
return contentType ?: "application/octet-stream"
58+
}
59+
4060
protected inline fun <reified T> requestBody(content: T, mediaType: String = JsonMediaType): RequestBody =
4161
when {
4262
content is File -> content.asRequestBody(
4363
mediaType.toMediaTypeOrNull()
4464
)
45-
mediaType == FormDataMediaType || mediaType == FormUrlEncMediaType -> {
65+
mediaType == FormDataMediaType -> {
66+
MultipartBody.Builder()
67+
.setType(MultipartBody.FORM)
68+
.apply {
69+
// content's type *must* be Map<String, Any?>
70+
@Suppress("UNCHECKED_CAST")
71+
(content as Map<String, Any?>).forEach { (key, value) ->
72+
if (value is File) {
73+
val partHeaders = Headers.headersOf(
74+
"Content-Disposition",
75+
"form-data; name=\"$key\"; filename=\"${value.name}\""
76+
)
77+
val fileMediaType = guessContentTypeFromFile(value).toMediaTypeOrNull()
78+
addPart(partHeaders, value.asRequestBody(fileMediaType))
79+
} else {
80+
val partHeaders = Headers.headersOf(
81+
"Content-Disposition",
82+
"form-data; name=\"$key\""
83+
)
84+
addPart(
85+
partHeaders,
86+
parameterToString(value).toRequestBody(null)
87+
)
88+
}
89+
}
90+
}.build()
91+
}
92+
mediaType == FormUrlEncMediaType -> {
4693
FormBody.Builder().apply {
47-
// content's type *must* be Map<String, Any>
94+
// content's type *must* be Map<String, Any?>
4895
@Suppress("UNCHECKED_CAST")
49-
(content as Map<String,String>).forEach { (key, value) ->
50-
add(key, value)
96+
(content as Map<String, Any?>).forEach { (key, value) ->
97+
add(key, parameterToString(value))
5198
}
5299
}.build()
53100
}
@@ -172,6 +219,26 @@ open class ApiClient(val baseUrl: String) {
172219
}
173220
}
174221

222+
protected fun parameterToString(value: Any?): String {
223+
when (value) {
224+
null -> {
225+
return ""
226+
}
227+
is Array<*> -> {
228+
return toMultiValue(value, "csv").toString()
229+
}
230+
is Iterable<*> -> {
231+
return toMultiValue(value, "csv").toString()
232+
}
233+
is OffsetDateTime, is OffsetTime, is LocalDateTime, is LocalDate, is LocalTime, is Date -> {
234+
return parseDateToQueryString<Any>(value)
235+
}
236+
else -> {
237+
return value.toString()
238+
}
239+
}
240+
}
241+
175242
protected inline fun <reified T: Any> parseDateToQueryString(value : T): String {
176243
/*
177244
.replace("\"", "") converts the json object string to an actual string for the query parameter.

samples/client/petstore/kotlin-jackson/src/main/kotlin/org/openapitools/client/apis/PetApi.kt

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -291,9 +291,9 @@ class PetApi(basePath: kotlin.String = "http://petstore.swagger.io/v2") : ApiCli
291291
*/
292292
@Throws(UnsupportedOperationException::class, ClientException::class, ServerException::class)
293293
fun updatePetWithForm(petId: kotlin.Long, name: kotlin.String?, status: kotlin.String?) : Unit {
294-
val localVariableBody: kotlin.Any? = mapOf("name" to "$name", "status" to "$status")
294+
val localVariableBody: kotlin.Any? = mapOf("name" to name, "status" to status)
295295
val localVariableQuery: MultiValueMap = mutableMapOf()
296-
val localVariableHeaders: MutableMap<String, String> = mutableMapOf("Content-Type" to "")
296+
val localVariableHeaders: MutableMap<String, String> = mutableMapOf("Content-Type" to "application/x-www-form-urlencoded")
297297
val localVariableConfig = RequestConfig(
298298
RequestMethod.POST,
299299
"/pet/{petId}".replace("{"+"petId"+"}", "$petId"),
@@ -334,9 +334,9 @@ class PetApi(basePath: kotlin.String = "http://petstore.swagger.io/v2") : ApiCli
334334
@Suppress("UNCHECKED_CAST")
335335
@Throws(UnsupportedOperationException::class, ClientException::class, ServerException::class)
336336
fun uploadFile(petId: kotlin.Long, additionalMetadata: kotlin.String?, file: java.io.File?) : ApiResponse {
337-
val localVariableBody: kotlin.Any? = mapOf("additionalMetadata" to "$additionalMetadata", "file" to "$file")
337+
val localVariableBody: kotlin.Any? = mapOf("additionalMetadata" to additionalMetadata, "file" to file)
338338
val localVariableQuery: MultiValueMap = mutableMapOf()
339-
val localVariableHeaders: MutableMap<String, String> = mutableMapOf("Content-Type" to "")
339+
val localVariableHeaders: MutableMap<String, String> = mutableMapOf("Content-Type" to "multipart/form-data")
340340
val localVariableConfig = RequestConfig(
341341
RequestMethod.POST,
342342
"/pet/{petId}/uploadImage".replace("{"+"petId"+"}", "$petId"),

0 commit comments

Comments
 (0)