[kotlin][client] fix encoding of individual parts of a multipart request (#11911)

* [kotlin][client] fix encoding (and Content-Type headers) of individual parts of a multipart request

* [kotlin][client] fix incorrect handling of binary downloads

* [kotlin][client] update samples
This commit is contained in:
Anton Koscejev
2022-04-13 19:52:01 +02:00
committed by GitHub
parent 498ba58881
commit 7851dfe148
91 changed files with 855 additions and 690 deletions

View File

@@ -34,6 +34,7 @@ import org.openapitools.client.infrastructure.ClientError
import org.openapitools.client.infrastructure.ServerException
import org.openapitools.client.infrastructure.ServerError
import org.openapitools.client.infrastructure.MultiValueMap
import org.openapitools.client.infrastructure.PartConfig
import org.openapitools.client.infrastructure.RequestConfig
import org.openapitools.client.infrastructure.RequestMethod
import org.openapitools.client.infrastructure.ResponseType
@@ -524,7 +525,7 @@ class PetApi(basePath: kotlin.String = defaultBasePath) : ApiClient(basePath) {
fun updatePetWithFormWithHttpInfo(petId: kotlin.Long, name: kotlin.String?, status: kotlin.String?) : ApiResponse<Unit?> {
val localVariableConfig = updatePetWithFormRequestConfig(petId = petId, name = name, status = status)
return request<Map<String, Any?>, Unit>(
return request<Map<String, PartConfig<*>>, Unit>(
localVariableConfig
)
}
@@ -537,8 +538,10 @@ class PetApi(basePath: kotlin.String = defaultBasePath) : ApiClient(basePath) {
* @param status Updated status of the pet (optional)
* @return RequestConfig
*/
fun updatePetWithFormRequestConfig(petId: kotlin.Long, name: kotlin.String?, status: kotlin.String?) : RequestConfig<Map<String, Any?>> {
val localVariableBody = mapOf("name" to name, "status" to status)
fun updatePetWithFormRequestConfig(petId: kotlin.Long, name: kotlin.String?, status: kotlin.String?) : RequestConfig<Map<String, PartConfig<*>>> {
val localVariableBody = mapOf(
"name" to PartConfig(body = name, headers = mutableMapOf()),
"status" to PartConfig(body = status, headers = mutableMapOf()),)
val localVariableQuery: MultiValueMap = mutableMapOf()
val localVariableHeaders: MutableMap<String, String> = mutableMapOf("Content-Type" to "application/x-www-form-urlencoded")
@@ -599,7 +602,7 @@ class PetApi(basePath: kotlin.String = defaultBasePath) : ApiClient(basePath) {
fun uploadFileWithHttpInfo(petId: kotlin.Long, additionalMetadata: kotlin.String?, file: java.io.File?) : ApiResponse<ModelApiResponse?> {
val localVariableConfig = uploadFileRequestConfig(petId = petId, additionalMetadata = additionalMetadata, file = file)
return request<Map<String, Any?>, ModelApiResponse>(
return request<Map<String, PartConfig<*>>, ModelApiResponse>(
localVariableConfig
)
}
@@ -612,8 +615,10 @@ class PetApi(basePath: kotlin.String = defaultBasePath) : ApiClient(basePath) {
* @param file file to upload (optional)
* @return RequestConfig
*/
fun uploadFileRequestConfig(petId: kotlin.Long, additionalMetadata: kotlin.String?, file: java.io.File?) : RequestConfig<Map<String, Any?>> {
val localVariableBody = mapOf("additionalMetadata" to additionalMetadata, "file" to file)
fun uploadFileRequestConfig(petId: kotlin.Long, additionalMetadata: kotlin.String?, file: java.io.File?) : RequestConfig<Map<String, PartConfig<*>>> {
val localVariableBody = mapOf(
"additionalMetadata" to PartConfig(body = additionalMetadata, headers = mutableMapOf()),
"file" to PartConfig(body = file, headers = mutableMapOf()),)
val localVariableQuery: MultiValueMap = mutableMapOf()
val localVariableHeaders: MutableMap<String, String> = mutableMapOf("Content-Type" to "multipart/form-data")
localVariableHeaders["Accept"] = "application/json"

View File

@@ -33,6 +33,7 @@ import org.openapitools.client.infrastructure.ClientError
import org.openapitools.client.infrastructure.ServerException
import org.openapitools.client.infrastructure.ServerError
import org.openapitools.client.infrastructure.MultiValueMap
import org.openapitools.client.infrastructure.PartConfig
import org.openapitools.client.infrastructure.RequestConfig
import org.openapitools.client.infrastructure.RequestMethod
import org.openapitools.client.infrastructure.ResponseType

View File

@@ -33,6 +33,7 @@ import org.openapitools.client.infrastructure.ClientError
import org.openapitools.client.infrastructure.ServerException
import org.openapitools.client.infrastructure.ServerError
import org.openapitools.client.infrastructure.MultiValueMap
import org.openapitools.client.infrastructure.PartConfig
import org.openapitools.client.infrastructure.RequestConfig
import org.openapitools.client.infrastructure.RequestMethod
import org.openapitools.client.infrastructure.ResponseType

View File

@@ -11,6 +11,7 @@ import okhttp3.ResponseBody
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.Request
import okhttp3.Headers
import okhttp3.Headers.Companion.toHeaders
import okhttp3.MultipartBody
import okhttp3.Call
import okhttp3.Callback
@@ -66,53 +67,46 @@ open class ApiClient(val baseUrl: String) {
return contentType ?: "application/octet-stream"
}
protected inline fun <reified T> requestBody(content: T, mediaType: String = JsonMediaType): RequestBody =
protected inline fun <reified T> requestBody(content: T, mediaType: String?): RequestBody =
when {
content is File -> content.asRequestBody(mediaType.toMediaTypeOrNull())
mediaType == FormDataMediaType -> {
mediaType == FormDataMediaType ->
MultipartBody.Builder()
.setType(MultipartBody.FORM)
.apply {
// content's type *must* be Map<String, Any?>
// content's type *must* be Map<String, PartConfig<*>>
@Suppress("UNCHECKED_CAST")
(content as Map<String, Any?>).forEach { (key, value) ->
if (value is File) {
val partHeaders = Headers.headersOf(
"Content-Disposition",
"form-data; name=\"$key\"; filename=\"${value.name}\""
)
val fileMediaType = guessContentTypeFromFile(value).toMediaTypeOrNull()
addPart(partHeaders, value.asRequestBody(fileMediaType))
} else {
val partHeaders = Headers.headersOf(
"Content-Disposition",
"form-data; name=\"$key\""
)
addPart(
partHeaders,
parameterToString(value).toRequestBody(null)
)
(content as Map<String, PartConfig<*>>).forEach { (name, part) ->
val contentType = part.headers.remove("Content-Type")
val bodies = if (part.body is Iterable<*>) part.body else listOf(part.body)
bodies.forEach { body ->
val headers = part.headers.toMutableMap() +
("Content-Disposition" to "form-data; name=\"$name\"" + if (body is File) "; filename=\"${body.name}\"" else "")
addPart(headers.toHeaders(),
requestSingleBody(body, contentType))
}
}
}.build()
}
else -> requestSingleBody(content, mediaType)
}
protected inline fun <reified T> requestSingleBody(content: T, mediaType: String?): RequestBody =
when {
content is File -> content.asRequestBody((mediaType ?: guessContentTypeFromFile(content)).toMediaTypeOrNull())
mediaType == FormUrlEncMediaType -> {
FormBody.Builder().apply {
// content's type *must* be Map<String, Any?>
// content's type *must* be Map<String, PartConfig<*>>
@Suppress("UNCHECKED_CAST")
(content as Map<String, Any?>).forEach { (key, value) ->
add(key, parameterToString(value))
(content as Map<String, PartConfig<*>>).forEach { (name, part) ->
add(name, parameterToString(part.body))
}
}.build()
}
mediaType.startsWith("application/") && mediaType.endsWith("json") ->
mediaType == null || mediaType.startsWith("application/") && mediaType.endsWith("json") ->
if (content == null) {
EMPTY_REQUEST
} else {
Serializer.moshi.adapter(T::class.java).toJson(content)
.toRequestBody(
mediaType.toMediaTypeOrNull()
)
.toRequestBody((mediaType ?: JsonMediaType).toMediaTypeOrNull())
}
mediaType == XmlMediaType -> throw UnsupportedOperationException("xml not currently supported.")
// TODO: this should be extended with other serializers
@@ -124,10 +118,6 @@ open class ApiClient(val baseUrl: String) {
if(body == null) {
return null
}
val bodyContent = body.string()
if (bodyContent.isEmpty()) {
return null
}
if (T::class.java == File::class.java) {
// return tempfile
val f = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
@@ -137,13 +127,15 @@ open class ApiClient(val baseUrl: String) {
createTempFile("tmp.net.medicineone.teleconsultationandroid.openapi.openapicommon", null)
}
f.deleteOnExit()
val out = BufferedWriter(FileWriter(f))
out.write(bodyContent)
out.close()
body.byteStream().use { java.nio.file.Files.copy(it, f.toPath(), java.nio.file.StandardCopyOption.REPLACE_EXISTING) }
return f as T
}
val bodyContent = body.string()
if (bodyContent.isEmpty()) {
return null
}
return when {
mediaType==null || (mediaType.startsWith("application/") && mediaType.endsWith("json")) ->
mediaType==null || (mediaType.startsWith("application/") && mediaType.endsWith("json")) ->
Serializer.moshi.adapter<T>().fromJson(bodyContent)
else -> throw UnsupportedOperationException("responseBody currently only supports JSON body.")
}

View File

@@ -0,0 +1,11 @@
package org.openapitools.client.infrastructure
/**
* Defines a config object for a given part of a multi-part request.
* NOTE: Headers is a Map<String,String> because rfc2616 defines
* multi-valued headers as csv-only.
*/
data class PartConfig<T>(
val headers: MutableMap<String, String> = mutableMapOf(),
val body: T? = null
)