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

io.v47.tmdb.http.impl.HttpClientImpl.kt Maven / Gradle / Ivy

/**
 * BSD 3-Clause License
 *
 * Copyright (c) 2022, the tmdb-api-client authors
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * 1. Redistributions of source code must retain the above copyright notice, this
 *    list of conditions and the following disclaimer.
 *
 * 2. Redistributions in binary form must reproduce the above copyright notice,
 *    this list of conditions and the following disclaimer in the documentation
 *    and/or other materials provided with the distribution.
 *
 * 3. Neither the name of the copyright holder nor the names of its
 *    contributors may be used to endorse or promote products derived from
 *    this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
 * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
package io.v47.tmdb.http.impl

import com.fasterxml.jackson.databind.ObjectMapper
import io.reactivex.rxjava3.core.Flowable
import io.v47.tmdb.http.*
import io.v47.tmdb.http.api.ErrorResponse
import io.v47.tmdb.http.api.RawErrorResponse
import io.v47.tmdb.http.api.toErrorResponse
import org.reactivestreams.Publisher
import java.net.URI
import java.net.URLEncoder
import java.net.http.HttpClient as JHttpClient
import java.net.http.HttpRequest as JHttpRequest
import java.net.http.HttpResponse as JHttpResponse

internal class HttpClientImpl(
    private val objectMapper: ObjectMapper,
    private val baseUrl: String = ""
) : HttpClient {
    companion object {
        private const val OK = 200
    }

    private val uriVariableRegex = Regex("""\{(\w+)}""", RegexOption.IGNORE_CASE)
    private val imageErrorRegex = Regex("""(.+?)""", RegexOption.IGNORE_CASE)

    private val rawClient = JHttpClient
        .newBuilder()
        .followRedirects(JHttpClient.Redirect.NORMAL)
        .build()!!

    override fun execute(
        request: HttpRequest,
        responseType: TypeInfo
    ): Publisher> {
        val jsonBody = (responseType as? TypeInfo.Simple)?.type != ByteArray::class.java

        return Flowable.fromFuture(
            rawClient.sendAsync(
                request.toJHttpRequest(jsonBody),
                JHttpResponse.BodyHandlers.ofByteArray()
            )
        ).map { resp ->
            if (resp.statusCode() == OK)
                resp.toHttpResponse(if (jsonBody) responseType else null)
            else
                HttpResponseImpl(
                    resp.statusCode(),
                    resp.headers().map(),
                    createErrorResponse(resp)
                )
        }
    }

    private fun createErrorResponse(jResponse: JHttpResponse) =
        runCatching {
            objectMapper.readValue(jResponse.body(), RawErrorResponse::class.java).toErrorResponse()
        }.getOrElse {
            val str = String(jResponse.body())
            val imageErrorMatch = imageErrorRegex.find(str)

            val msg = if (imageErrorMatch != null)
                imageErrorMatch.groupValues[1]
            else
                str

            ErrorResponse(msg, jResponse.statusCode())
        }

    private fun HttpRequest.toJHttpRequest(json: Boolean = true) =
        JHttpRequest.newBuilder(URI(createUri()))
            .apply {
                val actualBody = if (body is ByteArray)
                    body as ByteArray
                else if (body != null)
                    objectMapper.writeValueAsBytes(body)
                else
                    null

                when (method) {
                    HttpMethod.Get -> GET()
                    HttpMethod.Post -> POST(JHttpRequest.BodyPublishers.ofByteArray(actualBody))
                    HttpMethod.Put -> PUT(JHttpRequest.BodyPublishers.ofByteArray(actualBody))
                    HttpMethod.Delete -> DELETE()
                }

                header(
                    "Accept", if (json)
                        "application/json"
                    else
                        "*/*"
                )

                header(
                    "Content-Type",
                    if (body !is ByteArray)
                        "application/json"
                    else
                        "application/octet-stream"
                )
            }.build()

    private fun HttpRequest.createUri(): String {
        val uriSB = StringBuilder(baseUrl)

        if (!url.startsWith("/"))
            uriSB.append('/')

        uriSB.append(url.replace(uriVariableRegex) { mr ->
            val name = mr.groupValues[1]
            uriVariables[name]?.toString()
                ?: throw IllegalArgumentException("No value specified for URI variable $name")
        })

        if (query.isNotEmpty()) {
            uriSB.append("?")

            var first = true
            query.map { (name, values) ->
                if (first)
                    first = false
                else
                    uriSB.append('&')

                uriSB.append(URLEncoder.encode(name, Charsets.UTF_8))
                uriSB.append('=')
                var valueFirst = true
                values.forEach { value ->
                    if (valueFirst)
                        valueFirst = false
                    else
                        uriSB.append(',')

                    uriSB.append(URLEncoder.encode(value.toString(), Charsets.UTF_8))
                }
            }
        }

        return uriSB.toString()
    }

    private fun JHttpResponse.toHttpResponse(typeInfo: TypeInfo?) =
        HttpResponseImpl(
            statusCode(),
            headers().map(),
            if (typeInfo != null)
                parseBody(typeInfo)
            else
                body()
        )

    private fun JHttpResponse.parseBody(typeInfo: TypeInfo) =
        objectMapper.readValue(body(), typeInfo.fullType as Class<*>)

    override fun close() = Unit
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy