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

ktx.async.http.kt Maven / Gradle / Ivy

Go to download

Kotlin coroutines support and asynchronous operations utilities for libGDX applications.

There is a newer version: 1.12.1-rc2
Show newest version
package ktx.async

import com.badlogic.gdx.Gdx
import com.badlogic.gdx.Net.*
import kotlinx.coroutines.*
import java.io.ByteArrayInputStream
import java.io.InputStream
import java.nio.charset.Charset
import java.util.Arrays
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException

/**
 * Executes a HTTP request asynchronously.
 * @param url URL that will be queried.
 * @param method HTTP method. GET by default. See [com.badlogic.gdx.Net.HttpMethods].
 * @param headers HTTP request headers mapped to their values. Empty by default.
 * @param timeout time before the request is cancelled. If the request was cancelled, an exception will be thrown
 *    (type of exception might vary on each platform).
 * @param content body of the request.
 * @param contentStream body of the request. Alternative to [content]. Pair of an [InputStream] along with its size.
 * @param followRedirects whether 301 and 302 redirects are followed. Defaults to true.
 * @param includeCredentials whether a cross-origin request will include credentials. Relevant only on web platforms.
 * @param onCancel executed when the HTTP request is cancelled through coroutine cancellation. Optional, should be
 *    passed only if coroutine's [Job.cancel] can be called.
 * @return [HttpRequestResult] storing HTTP response data.
 * @see HttpRequest
 */
suspend fun httpRequest(
    url: String,
    method: String = "GET",
    headers: Map = emptyMap(),
    timeout: Int = 0,
    content: String? = null,
    contentStream: Pair? = null,
    followRedirects: Boolean = true,
    includeCredentials: Boolean = false,
    onCancel: ((HttpRequest) -> Unit)? = null
): HttpRequestResult = coroutineScope {
  suspendCancellableCoroutine { continuation ->
    val httpRequest = HttpRequest(method).apply {
      this.url = url
      timeOut = timeout
      this.content = content
      this.followRedirects = followRedirects
      this.includeCredentials = includeCredentials
      contentStream?.let { setContent(it.first, it.second) }
      headers.forEach { (header, value) -> setHeader(header, value) }
    }
    val listener = KtxHttpResponseListener(httpRequest, continuation, onCancel)
    Gdx.net.sendHttpRequest(httpRequest, listener)
    continuation.invokeOnCancellation {
      if (!listener.completed) {
        Gdx.net.cancelHttpRequest(httpRequest)
      }
    }
  }
}

/**
 * Stores result of a [HttpRequest]. A safer alternative to [HttpResponse].
 * @param url URL of the queried resource.
 * @param method HTTP method of the request.
 * @param statusCode HTTP status code of the response. Might be set to -1 by internal LibGDX implementation or if the
 *    status could not be determined.
 * @param content response body stored as raw bytes.
 * @param headers HTTP header values of the response. Might be empty.
 */
class HttpRequestResult(
    val url: String,
    val method: String,
    val statusCode: Int,
    val content: ByteArray,
    val headers: Map>
) {
  /** Returns cached representation of the response stored as a string with default encoding.*/
  val contentAsString by lazy { getContentAsString() }
  /** Returns a new instance of [ByteArrayInputStream] with raw response bytes each time the getter is invoked. */
  val contentAsStream get() = ByteArrayInputStream(content)

  /**
   * @param header name of the HTTP header.
   * @return values assigned to the header or empty list if header is not present.
   */
  fun getHeader(header: String) = headers[header] ?: emptyList()

  /**
   * @param charset character encoding. Defaults to UTF-8.
   * @return [content] converted to a string using the selected encoding.
   */
  fun getContentAsString(charset: Charset = Charsets.UTF_8) = String(content, charset)

  override fun toString() = "HttpRequestResult(url=$url, method=$method, status=$statusCode)"
  override fun equals(other: Any?) = when (other) {
    null -> false
    other === this -> true
    is HttpRequestResult -> url == other.url && method == other.method && statusCode == other.statusCode
        && Arrays.equals(content, other.content) && headers == other.headers
    else -> false
  }

  override fun hashCode(): Int {
    var result = url.hashCode()
    result = 31 * result + method.hashCode()
    result = 31 * result + statusCode
    result = 31 * result + content.contentHashCode()
    result = 31 * result + headers.hashCode()
    return result
  }

  // Implementation note: LibGDX HttpRequestResult implementation can _quietly_ ignore closed input streams, which might
  // result in empty responses in multithreaded environments. We read the whole response into a byte array and return
  // this data object to avoid response content loss.
}

/**
 * Converts this non thread-safe [HttpResponse] to [HttpRequestResult] that reads and caches the HTTP result content
 * as byte array. Note that this method blocks the current thread until the HTTP result content is read.
 * @param requestData necessary to extract relevant data about the original request.
 * @return a new [HttpRequestResult] storing [HttpResponse] content.
 */
fun HttpResponse.toHttpRequestResult(requestData: HttpRequest) = HttpRequestResult(
    url = requestData.url,
    method = requestData.method,
    statusCode = this.status?.statusCode ?: -1, // -1 matches LibGDX default behaviour on unknown status.
    content = this.result ?: ByteArray(0),
    headers = this.headers ?: emptyMap()
)

/**
 * Internal implementation of [HttpResponseListener] based on coroutines.
 * @param httpRequest listens to the HTTP response of this request.
 * @param continuation will be resumed once the response is received.
 * @param onCancel optional operation executed if the coroutine is cancelled during the HTTP request.
 */
internal class KtxHttpResponseListener(
    val httpRequest: HttpRequest,
    val continuation: CancellableContinuation,
    val onCancel: ((HttpRequest) -> Unit)?
) : HttpResponseListener {
  @Volatile
  var completed = false
    private set

  override fun cancelled() = complete {
    onCancel?.invoke(httpRequest)
  }

  override fun failed(exception: Throwable) = complete {
    if (continuation.isActive) {
      continuation.resumeWithException(exception)
    }
  }

  override fun handleHttpResponse(httpResponse: HttpResponse) = complete {
    if (continuation.isActive) {
      continuation.resume(httpResponse.toHttpRequestResult(httpRequest))
    }
  }

  private inline fun complete(action: () -> Unit) {
    if (!completed) {
      completed = true
      action()
    }
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy