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

tech.sirwellington.alchemy.http.HttpResponse.kt Maven / Gradle / Ivy

Go to download

Part of the Alchemy Collection. REST without the MESS. Makes it dead-easy to call RESTful Web Services in Java. It is designed for interactions with APIs written in REST and JSON.

The newest version!
/*
 * Copyright © 2019. Sir Wellington.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 *
 * You may obtain a copy of the License at
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package tech.sirwellington.alchemy.http

import com.google.gson.*
import jdk.nashorn.internal.ir.annotations.Immutable
import tech.sirwellington.alchemy.annotations.arguments.Optional
import tech.sirwellington.alchemy.annotations.arguments.Required
import tech.sirwellington.alchemy.annotations.designs.patterns.BuilderPattern
import tech.sirwellington.alchemy.annotations.designs.patterns.BuilderPattern.Role.BUILDER
import tech.sirwellington.alchemy.annotations.designs.patterns.BuilderPattern.Role.PRODUCT
import tech.sirwellington.alchemy.annotations.designs.patterns.FactoryMethodPattern
import tech.sirwellington.alchemy.annotations.designs.patterns.FactoryMethodPattern.Role.FACTORY_METHOD
import tech.sirwellington.alchemy.arguments.Arguments.checkThat
import tech.sirwellington.alchemy.http.HttpAssertions.validHttpStatusCode
import tech.sirwellington.alchemy.http.HttpAssertions.validResponseClass
import tech.sirwellington.alchemy.http.HttpStatusCode.NOT_FOUND
import tech.sirwellington.alchemy.http.exceptions.JsonException
import java.util.Collections.unmodifiableMap

/**
 *
 * @see HttpResponse.Builder
 *
 *
 * @author SirWellington
 */
@Immutable
@FactoryMethodPattern(role = FactoryMethodPattern.Role.PRODUCT)
interface HttpResponse
{

    /**
     * HTTP OK are 200-208 or the 226 status code.
     *
     * @return true if the status code is "OK", false otherwise.
     *
     * @see  [https://en.wikipedia.org/wiki/List_of_HTTP_status_codes.2xx_Success](https://en.wikipedia.org/wiki/List_of_HTTP_status_codes.2xx_Success)
     */
    //Valid HTTP "OK" level Status Codes
    val isOk: Boolean
        get()
        {
            val statusCode = statusCode()
            return statusCode in 200..208 || statusCode == 226
        }

    /**
     * @return The HTTP Status code of the request.
     */
    fun statusCode(): Int

    /**
     * @return The [HttpStatusCode] corresponding to the [statusCode].
     */
    val status: HttpStatusCode?
        get() = HttpStatusCode.forCode(statusCode())

    /**
     * Whether the response corresponds to a [NOT_FOUND] status code.
     */
    val notFound get() = status == NOT_FOUND

    /**
     * @return The response headers returned by the REST Service.
     */
    @Optional
    fun responseHeaders(): Map

    /**
     * Get the Response Body as a String.
     *
     * @return The Response Body as a String
     */
    fun bodyAsString(): String

    /**
     * Get the Response Body in JSON format.
     *
     * @return The JSON Response Body
     *
     * @throws JsonException
     */
    @Throws(JsonException::class)
    fun body(): JsonElement

    /**
     * Get the Response Body as a custom POJO Type. (Plain Old Java Object) Ensure that the POJO is styled in typical
     * Java Bean/Value object style. Getters and setters are not required, although [Object.hashCode]
     * and [Object.equals] is recommended for any value type.
     *
     * @param       The type of the POJO
     * @param classOfT The Class of the POJO.
     *
     * @return An instance of `T`, mapped from the JSON Body.
     *
     * @throws JsonException If the [JSON Body][.body] could not be parsed.
     */
    @Throws(JsonException::class)
    fun  bodyAs(classOfT: Class): T

    /**
     * Use in cases where you expect the [Response Body][.body] to be a JSON Array. A [List] is
     * returned instead of an Array.
     *
     * @param       The type of the POJO
     * @param classOfT The Class of the POJO.
     *
     * @return A List of T, parse from the JSON Body.
     *
     * @throws JsonException
     */
    @Throws(JsonException::class)
    fun  bodyAsArrayOf(classOfT: Class): List

    /**
     * Tells you whether [this] and [other] are equal to each
     * other, according to:
     *
     * 1. The [status code][statusCode]
     * 2. [Response Headers][responseHeaders]
     * 3. [Response Body][bodyAsString]
     */
    fun equals(other: HttpResponse?): Boolean
    {
        if (other == null)
        {
            return false
        }

        if (this.statusCode() != other.statusCode())
        {
            return false
        }

        if (this.responseHeaders() != other.responseHeaders())
        {
            return false
        }

        if (this.bodyAsString() != other.bodyAsString())
        {
            return false
        }

        return true
    }

    //==============================================================================================
    // Builder Implementation
    //==============================================================================================
    @BuilderPattern(role = BUILDER)
    @FactoryMethodPattern(role = FactoryMethodPattern.Role.PRODUCT)
    class Builder
    {

        //Start negative to make sure that status code was set
        private var statusCode = -100
        private var responseHeaders = emptyMap()
        private var gson = GsonBuilder()
                .setDateFormat(Constants.DATE_FORMAT)
                .create()

        private var responseBody: JsonElement = JsonNull.INSTANCE

        fun copyFrom(other: HttpResponse): Builder
        {
            return this.withResponseBody(other.body())
                       .withStatusCode(other.statusCode())
                       .withResponseHeaders(other.responseHeaders())
        }

        @Throws(IllegalArgumentException::class)
        fun withStatusCode(statusCode: Int): Builder
        {
            checkThat(statusCode).isA(validHttpStatusCode())

            this.statusCode = statusCode
            return this
        }

        @Throws(IllegalArgumentException::class)
        fun withStatusCode(statusCode: HttpStatusCode): Builder
        {
            this.statusCode = statusCode.code
            return this
        }

        @Throws(IllegalArgumentException::class)
        fun withResponseHeaders(responseHeaders: Map?): Builder
        {
            val headers = responseHeaders ?: emptyMap()
            this.responseHeaders = headers

            return this
        }

        @Throws(IllegalArgumentException::class)
        fun withResponseBody(@Required json: JsonElement): Builder
        {
            this.responseBody = json
            return this
        }

        @Throws(IllegalArgumentException::class)
        fun usingGson(@Required gson: Gson): Builder
        {
            this.gson = gson
            return this
        }

        @Throws(IllegalStateException::class)
        fun build(): HttpResponse
        {
            checkThat(statusCode)
                    .throwing { ex -> IllegalStateException("Invalid status code supplied", ex) }
                    .isA(validHttpStatusCode())

            return ActualResponseObject(statusCode,
                                        unmodifiableMap(responseHeaders),
                                        gson,
                                        responseBody)
        }

        //==============================================================================================
        // Implementation
        //==============================================================================================
        @Immutable
        @BuilderPattern(role = PRODUCT)
        private data class ActualResponseObject constructor(private val statusCode: Int,
                                                            private val responseHeaders: Map,
                                                            private val gson: Gson,
                                                            private val responseBody: JsonElement) : HttpResponse
        {

            override fun statusCode(): Int
            {
                return statusCode
            }

            override fun responseHeaders(): Map
            {
                return responseHeaders
            }

            override fun bodyAsString(): String
            {
                return if (responseBody.isJsonPrimitive)
                {
                    responseBody.asString
                }
                else
                {
                    responseBody.toString()
                }
            }

            @Throws(JsonParseException::class)
            override fun body(): JsonElement
            {
                return gson.toJsonTree(responseBody)
            }

            @Throws(JsonParseException::class)
            override fun  bodyAs(classOfT: Class): T
            {
                checkThat(classOfT).isA(validResponseClass())

                try
                {
                    return if (responseBody.isJsonPrimitive)
                    {
                        val json = responseBody.asString
                        gson.fromJson(json, classOfT)
                    }
                    else
                    {
                        gson.fromJson(responseBody, classOfT)
                    }
                }
                catch (ex: Exception)
                {
                    throw JsonException("Failed to parse json to class: $classOfT", ex)
                }

            }

            @Throws(JsonException::class)
            override fun  bodyAsArrayOf(classOfT: Class): List
            {
                checkThat(classOfT).isA(validResponseClass())

                val emptyArray = java.lang.reflect.Array.newInstance(classOfT, 0) as Array
                val type = emptyArray::class.java

                try
                {
                    val array = gson.fromJson>(responseBody, type) ?: null
                    return array?.toList() ?: emptyList()
                }
                catch (ex: Exception)
                {
                    throw JsonException("Failed to parse json to class: " + classOfT, ex)
                }

            }

            override fun equals(other: Any?): Boolean
            {
                val other = other as? HttpResponse ?: return false
                return this.equals(other)
            }

            override fun toString(): String
            {
                return "ActualResponseObject(statusCode=$statusCode, responseHeaders=$responseHeaders, responseBody=$responseBody)"
            }


        }

        companion object
        {

            @JvmStatic
            @FactoryMethodPattern(role = FACTORY_METHOD)
            fun newInstance(): Builder
            {
                return Builder()
            }
        }

    }

    companion object
    {

        @FactoryMethodPattern(role = FACTORY_METHOD)
        fun builder(): Builder
        {
            return Builder.newInstance()
        }
    }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy