All Downloads are FREE. Search and download functionalities are using the official Maven repository.

kotlin-client.libraries.jvm-okhttp.infrastructure.ApiClient.kt.mustache Maven / Gradle / Ivy

There is a newer version: 7.9.0
Show newest version
package {{packageName}}.infrastructure

import okhttp3.Credentials
import okhttp3.OkHttpClient
import okhttp3.RequestBody
{{#jvm-okhttp3}}
import okhttp3.MediaType
{{/jvm-okhttp3}}
{{#jvm-okhttp4}}
import okhttp3.RequestBody.Companion.asRequestBody
import okhttp3.RequestBody.Companion.toRequestBody
{{/jvm-okhttp4}}
import okhttp3.FormBody
{{#jvm-okhttp3}}
import okhttp3.HttpUrl
{{/jvm-okhttp3}}
{{#jvm-okhttp4}}
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
{{/jvm-okhttp4}}
import okhttp3.ResponseBody
{{#jvm-okhttp4}}
import okhttp3.MediaType.Companion.toMediaTypeOrNull
{{/jvm-okhttp4}}
import okhttp3.Request
import okhttp3.Headers
import okhttp3.MultipartBody
import java.io.File
import java.net.URLConnection
import java.util.Date
{{^threetenbp}}
import java.time.LocalDate
import java.time.LocalDateTime
import java.time.LocalTime
import java.time.OffsetDateTime
import java.time.OffsetTime
{{/threetenbp}}
{{#threetenbp}}
import org.threeten.bp.LocalDate
import org.threeten.bp.LocalDateTime
import org.threeten.bp.LocalTime
import org.threeten.bp.OffsetDateTime
import org.threeten.bp.OffsetTime
{{/threetenbp}}

{{#nonPublicApi}}internal {{/nonPublicApi}}open class ApiClient(val baseUrl: String) {
    {{#nonPublicApi}}internal {{/nonPublicApi}}companion object {
        protected const val ContentType = "Content-Type"
        protected const val Accept = "Accept"
        protected const val Authorization = "Authorization"
        protected const val JsonMediaType = "application/json"
        protected const val FormDataMediaType = "multipart/form-data"
        protected const val FormUrlEncMediaType = "application/x-www-form-urlencoded"
        protected const val XmlMediaType = "application/xml"

        val apiKey: MutableMap = mutableMapOf()
        val apiKeyPrefix: MutableMap = mutableMapOf()
        var username: String? = null
        var password: String? = null
        var accessToken: String? = null

        @JvmStatic
        val client: OkHttpClient by lazy {
            builder.build()
        }

        @JvmStatic
        val builder: OkHttpClient.Builder = OkHttpClient.Builder()
    }

    /**
     * Guess Content-Type header from the given file (defaults to "application/octet-stream").
     *
     * @param file The given file
     * @return The guessed Content-Type
     */
    protected fun guessContentTypeFromFile(file: File): String {
        val contentType = URLConnection.guessContentTypeFromName(file.name)
        return contentType ?: "application/octet-stream"
    }

    protected inline fun  requestBody(content: T, mediaType: String = JsonMediaType): RequestBody =
        when {
            {{#jvm-okhttp3}}
            content is File -> RequestBody.create(
                MediaType.parse(mediaType), content
            )
            {{/jvm-okhttp3}}
            {{#jvm-okhttp4}}
            content is File -> content.asRequestBody(
                mediaType.toMediaTypeOrNull()
            )
            {{/jvm-okhttp4}}
            mediaType == FormDataMediaType -> {
                MultipartBody.Builder()
                    .setType(MultipartBody.FORM)
                    .apply {
                        // content's type *must* be Map
                        @Suppress("UNCHECKED_CAST")
                        (content as Map).forEach { (key, value) ->
                            if (value is File) {
                                val partHeaders = Headers.{{#jvm-okhttp3}}of{{/jvm-okhttp3}}{{#jvm-okhttp4}}headersOf{{/jvm-okhttp4}}(
                                    "Content-Disposition",
                                    "form-data; name=\"$key\"; filename=\"${value.name}\""
                                )
                                {{#jvm-okhttp3}}
                                val fileMediaType = MediaType.parse(guessContentTypeFromFile(value))
                                addPart(partHeaders, RequestBody.create(fileMediaType, value))
                                {{/jvm-okhttp3}}
                                {{#jvm-okhttp4}}
                                val fileMediaType = guessContentTypeFromFile(value).toMediaTypeOrNull()
                                addPart(partHeaders, value.asRequestBody(fileMediaType))
                                {{/jvm-okhttp4}}
                            } else {
                                val partHeaders = Headers.{{#jvm-okhttp3}}of{{/jvm-okhttp3}}{{#jvm-okhttp4}}headersOf{{/jvm-okhttp4}}(
                                    "Content-Disposition",
                                    "form-data; name=\"$key\""
                                )
                                addPart(
                                    partHeaders,
                                    {{#jvm-okhttp3}}
                                    RequestBody.create(null, parameterToString(value))
                                    {{/jvm-okhttp3}}
                                    {{#jvm-okhttp4}}
                                    parameterToString(value).toRequestBody(null)
                                    {{/jvm-okhttp4}}
                                )
                            }
                        }
                    }.build()
            }
            mediaType == FormUrlEncMediaType -> {
                FormBody.Builder().apply {
                    // content's type *must* be Map
                    @Suppress("UNCHECKED_CAST")
                    (content as Map).forEach { (key, value) ->
                        add(key, parameterToString(value))
                    }
                }.build()
            }
            {{#jvm-okhttp3}}
            mediaType == JsonMediaType -> RequestBody.create(
                {{#moshi}}
                MediaType.parse(mediaType), Serializer.moshi.adapter(T::class.java).toJson(content)
                {{/moshi}}
                {{#gson}}
                MediaType.parse(mediaType), Serializer.gson.toJson(content, T::class.java)
                {{/gson}}
                {{#jackson}}
                MediaType.parse(mediaType), Serializer.jacksonObjectMapper.writeValueAsString(content)
                {{/jackson}}
            )
            {{/jvm-okhttp3}}
            {{#jvm-okhttp4}}
            mediaType == JsonMediaType -> {{#moshi}}Serializer.moshi.adapter(T::class.java).toJson(content){{/moshi}}{{#gson}}Serializer.gson.toJson(content, T::class.java){{/gson}}{{#jackson}}Serializer.jacksonObjectMapper.writeValueAsString(content){{/jackson}}.toRequestBody(
                mediaType.toMediaTypeOrNull()
            )
            {{/jvm-okhttp4}}
            mediaType == XmlMediaType -> throw UnsupportedOperationException("xml not currently supported.")
            // TODO: this should be extended with other serializers
            else -> throw UnsupportedOperationException("requestBody currently only supports JSON body and File body.")
        }

    protected inline fun  responseBody(body: ResponseBody?, mediaType: String? = JsonMediaType): T? {
        if(body == null) {
            return null
        }
        val bodyContent = body.string()
        if (bodyContent.isEmpty()) {
            return null
        }
        return when(mediaType) {
            JsonMediaType -> {{#moshi}}Serializer.moshi.adapter(T::class.java).fromJson(bodyContent){{/moshi}}{{#gson}}Serializer.gson.fromJson(bodyContent, T::class.java){{/gson}}{{#jackson}}Serializer.jacksonObjectMapper.readValue(bodyContent, T::class.java){{/jackson}}
            else ->  throw UnsupportedOperationException("responseBody currently only supports JSON body.")
        }
    }

    {{#hasAuthMethods}}
    protected fun updateAuthParams(requestConfig: RequestConfig) {
        {{#authMethods}}
        {{#isApiKey}}
        {{#isKeyInHeader}}
        if (requestConfig.headers["{{keyParamName}}"].isNullOrEmpty()) {
        {{/isKeyInHeader}}
        {{#isKeyInQuery}}
        if (requestConfig.query["{{keyParamName}}"].isNullOrEmpty()) {
        {{/isKeyInQuery}}
            if (apiKey["{{keyParamName}}"] != null) {
                if (apiKeyPrefix["{{keyParamName}}"] != null) {
                    {{#isKeyInHeader}}
                    requestConfig.headers["{{keyParamName}}"] = apiKeyPrefix["{{keyParamName}}"]!! + " " + apiKey["{{keyParamName}}"]!!
                    {{/isKeyInHeader}}
                    {{#isKeyInQuery}}
                    requestConfig.query["{{keyParamName}}"] = apiKeyPrefix["{{keyParamName}}"]!! + " " + apiKey["{{keyParamName}}"]!!
                    {{/isKeyInQuery}}
                } else {
                    {{#isKeyInHeader}}
                    requestConfig.headers["{{keyParamName}}"] = apiKey["{{keyParamName}}"]!!
                    {{/isKeyInHeader}}
                    {{#isKeyInQuery}}
                    requestConfig.query["{{keyParamName}}"] = apiKey["{{keyParamName}}"]!!
                    {{/isKeyInQuery}}
                }
            }
        }
        {{/isApiKey}}
        {{#isBasic}}
        {{#isBasicBasic}}
        if (requestConfig.headers[Authorization].isNullOrEmpty()) {
            username?.let { username ->
                password?.let { password ->
                    requestConfig.headers[Authorization] = Credentials.basic(username, password)
                }
            }
        }
        {{/isBasicBasic}}
        {{#isBasicBearer}}
        if (requestConfig.headers[Authorization].isNullOrEmpty()) {
            accessToken?.let { accessToken ->
                requestConfig.headers[Authorization] = "Bearer $accessToken"
            }
        }
        {{/isBasicBearer}}
        {{/isBasic}}
        {{#isOAuth}}
        if (requestConfig.headers[Authorization].isNullOrEmpty()) {
            accessToken?.let { accessToken ->
                requestConfig.headers[Authorization] = "Bearer $accessToken "
            }
        }
        {{/isOAuth}}
        {{/authMethods}}
    }
    {{/hasAuthMethods}}

    protected inline fun  request(requestConfig: RequestConfig, body : Any? = null): ApiInfrastructureResponse {
        {{#jvm-okhttp3}}
        val httpUrl = HttpUrl.parse(baseUrl) ?: throw IllegalStateException("baseUrl is invalid.")
        {{/jvm-okhttp3}}
        {{#jvm-okhttp4}}
        val httpUrl = baseUrl.toHttpUrlOrNull() ?: throw IllegalStateException("baseUrl is invalid.")
        {{/jvm-okhttp4}}
        {{#hasAuthMethods}}

        // take authMethod from operation
        updateAuthParams(requestConfig)
        {{/hasAuthMethods}}

        val url = httpUrl.newBuilder()
            .addPathSegments(requestConfig.path.trimStart('/'))
            .apply {
                requestConfig.query.forEach { query ->
                    query.value.forEach { queryValue ->
                        addQueryParameter(query.key, queryValue)
                    }
                }
            }.build()

        // take content-type/accept from spec or set to default (application/json) if not defined
        if (requestConfig.headers[ContentType].isNullOrEmpty()) {
            requestConfig.headers[ContentType] = JsonMediaType
        }
        if (requestConfig.headers[Accept].isNullOrEmpty()) {
            requestConfig.headers[Accept] = JsonMediaType
        }
        val headers = requestConfig.headers

        if(headers[ContentType] ?: "" == "") {
            throw kotlin.IllegalStateException("Missing Content-Type header. This is required.")
        }

        if(headers[Accept] ?: "" == "") {
            throw kotlin.IllegalStateException("Missing Accept header. This is required.")
        }

        // TODO: support multiple contentType options here.
        val contentType = (headers[ContentType] as String).substringBefore(";").toLowerCase()

        val request = when (requestConfig.method) {
            RequestMethod.DELETE -> Request.Builder().url(url).delete(requestBody(body, contentType))
            RequestMethod.GET -> Request.Builder().url(url)
            RequestMethod.HEAD -> Request.Builder().url(url).head()
            RequestMethod.PATCH -> Request.Builder().url(url).patch(requestBody(body, contentType))
            RequestMethod.PUT -> Request.Builder().url(url).put(requestBody(body, contentType))
            RequestMethod.POST -> Request.Builder().url(url).post(requestBody(body, contentType))
            RequestMethod.OPTIONS -> Request.Builder().url(url).method("OPTIONS", null)
        }.apply {
            headers.forEach { header -> addHeader(header.key, header.value) }
        }.build()

        val response = client.newCall(request).execute()
        val accept = response.header(ContentType)?.substringBefore(";")?.toLowerCase()

        // TODO: handle specific mapping types. e.g. Map>
        when {
            response.isRedirect -> return Redirection(
                    response.code{{#jvm-okhttp3}}(){{/jvm-okhttp3}},
                    response.headers{{#jvm-okhttp3}}(){{/jvm-okhttp3}}.toMultimap()
            )
            response.isInformational -> return Informational(
                    response.message{{#jvm-okhttp3}}(){{/jvm-okhttp3}},
                    response.code{{#jvm-okhttp3}}(){{/jvm-okhttp3}},
                    response.headers{{#jvm-okhttp3}}(){{/jvm-okhttp3}}.toMultimap()
            )
            response.isSuccessful -> return Success(
                    responseBody(response.body{{#jvm-okhttp3}}(){{/jvm-okhttp3}}, accept),
                    response.code{{#jvm-okhttp3}}(){{/jvm-okhttp3}},
                    response.headers{{#jvm-okhttp3}}(){{/jvm-okhttp3}}.toMultimap()
            )
            response.isClientError -> return ClientError(
                    response.message{{#jvm-okhttp3}}(){{/jvm-okhttp3}},
                    response.body{{#jvm-okhttp3}}(){{/jvm-okhttp3}}?.string(),
                    response.code{{#jvm-okhttp3}}(){{/jvm-okhttp3}},
                    response.headers{{#jvm-okhttp3}}(){{/jvm-okhttp3}}.toMultimap()
            )
            else -> return ServerError(
                    response.message{{#jvm-okhttp3}}(){{/jvm-okhttp3}},
                    response.body{{#jvm-okhttp3}}(){{/jvm-okhttp3}}?.string(),
                    response.code{{#jvm-okhttp3}}(){{/jvm-okhttp3}},
                    response.headers{{#jvm-okhttp3}}(){{/jvm-okhttp3}}.toMultimap()
            )
        }
    }

    protected fun parameterToString(value: Any?): String {
        when (value) {
            null -> {
                return ""
            }
            is Array<*> -> {
                return toMultiValue(value, "csv").toString()
            }
            is Iterable<*> -> {
                return toMultiValue(value, "csv").toString()
            }
            is OffsetDateTime, is OffsetTime, is LocalDateTime, is LocalDate, is LocalTime, is Date -> {
                return parseDateToQueryString(value)
            }
            else -> {
                return value.toString()
            }
        }
    }

    protected inline fun  parseDateToQueryString(value : T): String {
        {{#toJson}}
        /*
        .replace("\"", "") converts the json object string to an actual string for the query parameter.
        The moshi or gson adapter allows a more generic solution instead of trying to use a native
        formatter. It also easily allows to provide a simple way to define a custom date format pattern
        inside a gson/moshi adapter.
        */
        {{#moshi}}
        return Serializer.moshi.adapter(T::class.java).toJson(value).replace("\"", "")
        {{/moshi}}
        {{#gson}}
        return Serializer.gson.toJson(value, T::class.java).replace("\"", "")
        {{/gson}}
        {{#jackson}}
        return Serializer.jacksonObjectMapper.writeValueAsString(value).replace("\"", "")
        {{/jackson}}
        {{/toJson}}
        {{^toJson}}
        return value.toString()
        {{/toJson}}
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy