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

com.pubnub.internal.EndpointCore.kt Maven / Gradle / Ivy

package com.pubnub.internal

import com.google.gson.JsonElement
import com.pubnub.api.Endpoint
import com.pubnub.api.PubNubError
import com.pubnub.api.PubNubException
import com.pubnub.api.retry.RetryableEndpointGroup
import com.pubnub.api.v2.PNConfiguration
import com.pubnub.api.v2.PNConfiguration.Companion.isValid
import com.pubnub.api.v2.PNConfigurationOverride
import com.pubnub.api.v2.callbacks.Consumer
import com.pubnub.api.v2.callbacks.Result
import com.pubnub.internal.managers.RetrofitManager
import com.pubnub.internal.retry.RetryableBase
import com.pubnub.internal.retry.RetryableCallback
import com.pubnub.internal.retry.RetryableRestCaller
import com.pubnub.internal.v2.PNConfigurationImpl
import org.slf4j.LoggerFactory
import retrofit2.Call
import retrofit2.Response
import java.io.IOException
import java.net.ConnectException
import java.net.SocketTimeoutException
import java.net.UnknownHostException

/**
 * Base class for all PubNub API operation implementations.
 *
 * @param Input Server's response.
 * @param Output Parsed and encapsulated response for endusers.
 * @property pubnub The client instance.
 */
abstract class EndpointCore protected constructor(protected val pubnub: PubNubImpl) : Endpoint {
    private var configOverride: PNConfiguration? = null
    val configuration: PNConfiguration
        get() = configOverride ?: pubnub.configuration

    protected val retrofitManager: RetrofitManager
        get() = configOverride?.let { configOverrideNonNull ->
            RetrofitManager(pubnub.retrofitManager, configOverrideNonNull)
        } ?: pubnub.retrofitManager

    private val log = LoggerFactory.getLogger(this.javaClass.simpleName)

    private lateinit var cachedCallback: Consumer>
    private lateinit var call: Call
    private var silenceFailures = false
    private val retryableRestCaller by lazy {
        RetryableRestCaller(
            configuration.retryConfiguration,
            getEndpointGroupName(),
            isEndpointRetryable(),
        )
    }

    override fun overrideConfiguration(action: PNConfigurationOverride.Builder.() -> Unit): Endpoint {
        overrideConfigurationInternal(PNConfigurationImpl.Builder(configuration).apply(action).build())
        return this
    }

    override fun overrideConfiguration(configuration: PNConfiguration): Endpoint {
        overrideConfigurationInternal(configuration)
        return this
    }

    /**
     * Key-value object to pass with every PubNub API operation. Used for debugging purposes.
     * todo: it should be removed!
     */
    val queryParam: MutableMap = mutableMapOf()

    /**
     * Executes the call synchronously. This function blocks the thread.
     *
     * @return A parsed and encapsulated response if the request has been successful, `null` otherwise.
     *
     * @throws [PubNubException] if anything goes wrong with the request.
     */
    override fun sync(): Output {
        validateParams()
        call = doWork(createBaseParams())
        val response = retryableRestCaller.execute(call)
        return handleResponse(response)
    }

    private fun handleResponse(response: Response): Output {
        when {
            response.isSuccessful -> {
                return checkAndCreateResponse(response)
            }

            else -> {
                val (errorString, errorJson) = extractErrorBody(response)
                throw createException(response, errorString, errorJson)
            }
        }
    }

    /**
     * Executes the call asynchronously. This function does not block the thread.
     *
     * @param callback The callback to receive the response in.
     */
    override fun async(callback: Consumer>) {
        cachedCallback = callback

        try {
            validateParams()
            call = doWork(createBaseParams())
        } catch (pubnubException: PubNubException) {
            callback.accept(Result.failure(pubnubException))
            return
        }

        call.enqueue(
            object : RetryableCallback(
                call = call,
                retryConfiguration = configuration.retryConfiguration,
                endpointGroupName = getEndpointGroupName(),
                isEndpointRetryable = isEndpointRetryable(),
                executorService = pubnub.executorService,
            ) {
                override fun onFinalResponse(
                    call: Call,
                    response: Response,
                ) {
                    when {
                        response.isSuccessful -> {
                            // query params
                            try {
                                Result.success(checkAndCreateResponse(response))
                            } catch (e: PubNubException) {
                                Result.failure(e)
                            }.let { result ->
                                callback.accept(result)
                            }
                        }

                        else -> {
                            val (errorString, errorJson) = extractErrorBody(response)

                            callback.accept(
                                Result.failure(
                                    createException(
                                        response,
                                        errorString,
                                        errorJson,
                                    ),
                                ),
                            )
                        }
                    }
                }

                override fun onFinalFailure(
                    call: Call,
                    t: Throwable,
                ) {
                    if (silenceFailures) {
                        return
                    }

                    val error: PubNubError =
                        when (t) {
                            is UnknownHostException, is ConnectException -> {
                                PubNubError.CONNECT_EXCEPTION
                            }

                            is SocketTimeoutException -> {
                                PubNubError.SUBSCRIBE_TIMEOUT
                            }

                            is IOException -> {
                                PubNubError.PARSING_ERROR
                            }

                            is IllegalStateException -> {
                                PubNubError.PARSING_ERROR
                            }

                            else -> {
                                PubNubError.HTTP_ERROR
                            }
                        }

                    val pubnubException =
                        PubNubException(
                            errorMessage = t.toString(),
                            pubnubError = error,
                            cause = t,
                            remoteAction = this@EndpointCore,
                        )
                    callback.accept(Result.failure(pubnubException))
                }
            },
        )
    }

    protected fun createBaseParams(): HashMap {
        val map = hashMapOf()

        map += queryParam

        map["pnsdk"] = pubnub.generatePnsdk()
        map["uuid"] = configuration.userId.value

        if (configuration.includeInstanceIdentifier) {
            map["instanceid"] = pubnub.instanceId
        }

        if (configuration.includeRequestIdentifier) {
            map["requestid"] = pubnub.requestId()
        }

        if (isAuthRequired()) {
            val token = pubnub.tokenManager.getToken()
            if (token != null) {
                map["auth"] = token
            } else if (configuration.authKey.isValid()) {
                map["auth"] = configuration.authKey
            }
        }
        return map
    }

    /**
     * Cancel the operation but do not alert anybody, useful for restarting the heartbeats and subscribe loops.
     */
    override fun silentCancel() {
        if (::call.isInitialized) {
            if (!call.isCanceled) {
                silenceFailures = true
                call.cancel()
            }
        }
    }

    private fun createException(
        response: Response,
        errorString: String? = null,
        errorBody: JsonElement? = null,
    ): PubNubException {
        val errorChannels = mutableListOf()
        val errorGroups = mutableListOf()

        if (errorBody != null) {
            if (pubnub.mapper.isJsonObject(errorBody) &&
                pubnub.mapper.hasField(
                    errorBody,
                    "payload",
                )
            ) {
                val payloadBody = pubnub.mapper.getField(errorBody, "payload")!!

                if (pubnub.mapper.hasField(payloadBody, "channels")) {
                    val iterator = pubnub.mapper.getArrayIterator(payloadBody, "channels")
                    while (iterator.hasNext()) {
                        errorChannels.add(pubnub.mapper.elementToString(iterator.next())!!)
                    }
                }

                if (pubnub.mapper.hasField(payloadBody, "channel-groups")) {
                    val iterator = pubnub.mapper.getArrayIterator(payloadBody, "channel-groups")
                    while (iterator.hasNext()) {
                        val node = iterator.next()

                        val channelGroupName =
                            pubnub.mapper.elementToString(node)!!.let {
                                if (it.first().toString() == ":") {
                                    it.substring(1)
                                } else {
                                    it
                                }
                            }

                        errorGroups.add(channelGroupName)
                    }
                }
            }
        }

        val affectedChannels =
            errorChannels.ifEmpty {
                try {
                    getAffectedChannels()
                } catch (e: UninitializedPropertyAccessException) {
                    emptyList()
                }
            }

        val affectedChannelGroups =
            errorGroups.ifEmpty {
                try {
                    getAffectedChannelGroups()
                } catch (e: UninitializedPropertyAccessException) {
                    emptyList()
                }
            }

        return PubNubException(
            pubnubError = PubNubError.HTTP_ERROR,
            errorMessage = errorString,
            jso = errorBody?.toString(),
            statusCode = response.code(),
            affectedCall = call,
            retryAfterHeaderValue = response.headers()[RetryableBase.RETRY_AFTER_HEADER_NAME]?.toIntOrNull(),
            affectedChannels = affectedChannels,
            affectedChannelGroups = affectedChannelGroups,
            requestInfo =
                PubNubException.RequestInfo(
                    tlsEnabled = response.raw().request.url.isHttps,
                    origin = response.raw().request.url.host,
                    uuid = response.raw().request.url.queryParameter("uuid"),
                    authKey = response.raw().request.url.queryParameter("auth"),
                    clientRequest = response.raw().request,
                ),
            remoteAction = this,
        )
    }

    override fun retry() {
        silenceFailures = false
        async(cachedCallback)
    }

    private fun extractErrorBody(response: Response): Pair {
        val errorBodyString =
            try {
                response.errorBody()?.string()
            } catch (e: IOException) {
                "N/A"
            }

        val errorBodyJson =
            try {
                pubnub.mapper.fromJson(errorBodyString, JsonElement::class.java)
            } catch (e: PubNubException) {
                null
            }

        return errorBodyString to errorBodyJson
    }

    private fun checkAndCreateResponse(input: Response): Output {
        try {
            return createResponse(input)
        } catch (pubnubException: PubNubException) {
            throw pubnubException.copy(
                statusCode = input.code(),
                jso = pubnub.mapper.toJson(input.body()),
                affectedCall = call,
            )
        } catch (e: KotlinNullPointerException) {
            throw PubNubException(
                pubnubError = PubNubError.PARSING_ERROR,
                errorMessage = e.toString(),
                affectedCall = call,
                statusCode = input.code(),
                jso = pubnub.mapper.toJson(input.body()),
                cause = e,
            )
        } catch (e: IllegalStateException) {
            throw PubNubException(
                pubnubError = PubNubError.PARSING_ERROR,
                errorMessage = e.toString(),
                affectedCall = call,
                statusCode = input.code(),
                jso = pubnub.mapper.toJson(input.body()),
                cause = e,
            )
        } catch (e: IndexOutOfBoundsException) {
            throw PubNubException(
                pubnubError = PubNubError.PARSING_ERROR,
                errorMessage = e.toString(),
                affectedCall = call,
                statusCode = input.code(),
                jso = pubnub.mapper.toJson(input.body()),
                cause = e,
            )
        } catch (e: NullPointerException) {
            throw PubNubException(
                pubnubError = PubNubError.PARSING_ERROR,
                errorMessage = e.toString(),
                affectedCall = call,
                statusCode = input.code(),
                jso = pubnub.mapper.toJson(input.body()),
                cause = e,
            )
        } catch (e: IllegalArgumentException) {
            throw PubNubException(
                pubnubError = PubNubError.PARSING_ERROR,
                errorMessage = e.toString(),
                affectedCall = call,
                statusCode = input.code(),
                jso = pubnub.mapper.toJson(input.body()),
                cause = e,
            )
        } catch (e: TypeCastException) {
            throw PubNubException(
                pubnubError = PubNubError.PARSING_ERROR,
                errorMessage = e.toString(),
                affectedCall = call,
                statusCode = input.code(),
                jso = pubnub.mapper.toJson(input.body()),
                cause = e,
            )
        } catch (e: ClassCastException) {
            throw PubNubException(
                pubnubError = PubNubError.PARSING_ERROR,
                errorMessage = e.toString(),
                affectedCall = call,
                statusCode = input.code(),
                jso = pubnub.mapper.toJson(input.body()),
                cause = e,
            )
        } catch (e: UninitializedPropertyAccessException) {
            throw PubNubException(
                pubnubError = PubNubError.PARSING_ERROR,
                errorMessage = e.toString(),
                affectedCall = call,
                statusCode = input.code(),
                jso = pubnub.mapper.toJson(input.body()),
                cause = e,
            )
        }
    }

    protected open fun getAffectedChannels() = emptyList()

    protected open fun getAffectedChannelGroups(): List = emptyList()

    protected open fun validateParams() {
        if (isSubKeyRequired() && !configuration.subscribeKey.isValid()) {
            throw PubNubException(PubNubError.SUBSCRIBE_KEY_MISSING)
        }
        if (isPubKeyRequired() && !configuration.publishKey.isValid()) {
            throw PubNubException(PubNubError.PUBLISH_KEY_MISSING)
        }
    }

    fun overrideConfigurationInternal(configuration: PNConfiguration) {
        this.configOverride = configuration
    }

    protected abstract fun doWork(queryParams: HashMap): Call

    protected abstract fun createResponse(input: Response): Output

    protected open fun isSubKeyRequired() = true

    protected open fun isPubKeyRequired() = false

    protected open fun isAuthRequired() = true

    protected abstract fun getEndpointGroupName(): RetryableEndpointGroup

    protected open fun isEndpointRetryable(): Boolean = true
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy