/** * TrackerMap * Copyright (C) 2021-2022 Iván Ávalos , Henoch Ojeda * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ package mx.trackermap.TrackerMap.client.infrastructure import io.ktor.client.* import io.ktor.client.call.* import io.ktor.client.features.* import io.ktor.client.features.json.* import io.ktor.client.features.json.serializer.KotlinxSerializer import io.ktor.client.features.logging.DEFAULT import io.ktor.client.features.logging.LogLevel import io.ktor.client.features.logging.Logger import io.ktor.client.features.logging.Logging import io.ktor.client.request.* import io.ktor.client.request.forms.FormDataContent import io.ktor.client.statement.* import io.ktor.http.* import io.ktor.util.* import kotlinx.serialization.json.Json as KotlinJson open class ApiClient( val sessionManager: SessionManager ) { companion object { protected const val ApiContentType = "Content-Type" protected const val ApiAccept = "Accept" protected const val ApiJsonMediaType = "application/json" protected const val ApiFormDataMediaType = "multipart/form-data" protected const val ApiFormURLType = "application/x-www-form-urlencoded" protected const val ApiXmlMediaType = "application/xml" val client: HttpClient = HttpClientProvider().getHttpClient().config { install(HttpTimeout) { connectTimeoutMillis = 20_000 requestTimeoutMillis = 20_000 } install(JsonFeature) { serializer = KotlinxSerializer( KotlinJson { ignoreUnknownKeys = true useAlternativeNames = false } ) } install(Logging) { logger = Logger.DEFAULT level = LogLevel.ALL } } val defaultHeaders: Map = mapOf( ApiContentType to ApiJsonMediaType, ApiAccept to ApiJsonMediaType ) } protected inline fun fillRequest( requestBuilder: HttpRequestBuilder, content: T, mediaType: String = ApiJsonMediaType ) { when { mediaType == ApiFormDataMediaType && content is Map<*, *> -> { val parametersBuilder = ParametersBuilder() content.forEach { map -> if (map.key is String && map.value is String) { parametersBuilder[map.key as String] = map.value as String } } parametersBuilder.build() requestBuilder.contentType(ContentType.MultiPart.FormData) requestBuilder.body = parametersBuilder } mediaType == ApiJsonMediaType -> { requestBuilder.contentType(ContentType.Application.Json) if (content != null) { requestBuilder.body = content } } mediaType == ApiFormURLType && content is Map<*, *> -> { val parametersBuilder = ParametersBuilder() content.forEach { item -> parametersBuilder[item.key as String] = item.value as String } requestBuilder.body = FormDataContent(parametersBuilder.build()) } mediaType == ApiXmlMediaType -> TODO("xml not currently supported.") // TODO: this should be extended with other serializers else -> TODO("requestBody currently only supports JSON body and File body.") } } protected suspend inline fun request( requestConfig: RequestConfig, body: Any? = null ): ApiInfrastructureResponse { val httpUrl: Url try { httpUrl = Url(sessionManager.baseUrl) } catch (e: URLDecodeException) { throw IllegalStateException("baseUrl is invalid.") } val urlBuilder = URLBuilder(httpUrl) .path("${httpUrl.encodedPath.trimStart('/')}${requestConfig.path}") requestConfig.query.forEach { query -> query.value.forEach { queryValue -> urlBuilder.parameters.append(query.key, queryValue) } } val url = urlBuilder.build() val headers = defaultHeaders + requestConfig.headers if ((headers[ApiContentType] ?: "") == "") { throw IllegalStateException("Missing Content-Type header. This is required.") } if ((headers[ApiAccept] ?: "") == "") { throw IllegalStateException("Missing Accept header. This is required.") } // TODO: support multiple contentType,accept options here. val contentType = (headers[ApiContentType] as String).substringBefore(";").lowercase() val accept = (headers[ApiAccept] as String).substringBefore(";").lowercase() val request = HttpRequestBuilder() request.url(url) request.accept(ContentType.parse(accept)) when (requestConfig.method) { RequestMethod.DELETE -> { request.method = HttpMethod.Delete } RequestMethod.GET -> { request.method = HttpMethod.Get } RequestMethod.HEAD -> { request.method = HttpMethod.Head } RequestMethod.PATCH -> { request.method = HttpMethod.Patch fillRequest(request, body, contentType) } RequestMethod.PUT -> { request.method = HttpMethod.Put fillRequest(request, body, contentType) } RequestMethod.POST -> { request.method = HttpMethod.Post fillRequest(request, body, contentType) } RequestMethod.OPTIONS -> { request.method = HttpMethod.Options } } if (sessionManager.token.isNotEmpty()) { request.headers["Cookie"] = sessionManager.token } val response: HttpResponse = client.request(request) // TODO: handle specific mapping types. e.g. Map> when (response.status.value) { in 300..399 -> return Redirection( response.status.value, response.headers.toMap() ) in 100..199 -> return Informational( response.status.description, response.status.value, response.headers.toMap() ) in 200..299 -> return Success( response.receive(), response.status.value, response.headers.toMap() ) in 400..499 -> return ClientError( response.receive(), response.status.value, response.headers.toMap() ) else -> return ServerError( null, response.receive(), response.status.value, response.headers.toMap() ) } } }