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.PubNubError
import com.pubnub.api.PubNubException
import com.pubnub.api.retry.RetryableEndpointGroup
import com.pubnub.api.v2.BasePNConfiguration
import com.pubnub.api.v2.BasePNConfiguration.Companion.isValid
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 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
import java.util.function.Consumer

/**
 * 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: PubNubCore) :
    EndpointInterface {
        private var configOverride: BasePNConfiguration? = null
        final override val configuration: BasePNConfiguration
            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(),
            )
        }

        /**
         * 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)
            }
        }

        override fun overrideConfiguration(configuration: BasePNConfiguration) {
            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