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

wvlet.airframe.http.Http.scala Maven / Gradle / Ivy

There is a newer version: 24.12.2
Show newest version
/*
 * 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 wvlet.airframe.http

import wvlet.airframe.http.HttpBackend.DefaultBackend
import wvlet.airframe.http.HttpMessage.{Request, Response}
import wvlet.airframe.http.client.SyncClient

import java.time.{Instant, ZoneId, ZoneOffset}
import java.time.format.DateTimeFormatter
import java.util.Locale
import scala.concurrent.{ExecutionContext, ExecutionContextExecutor, Future}
import scala.language.higherKinds

/**
  * An entry point to access airframe-http functionalities
  */
object Http {

  // Standard HttpFilter
  abstract class Filter extends HttpFilter[Request, Response, Future] {
    protected implicit lazy val executorContext: ExecutionContext = compat.defaultExecutionContext
    override protected def backend: HttpBackend[Request, Response, Future] =
      HttpBackend.DefaultBackend
  }
  // Standard HttpContext
  abstract class Context extends HttpContext[Request, Response, Future] {
    override protected def backend: HttpBackend[Request, Response, Future] =
      HttpBackend.DefaultBackend
  }

  /**
    * An entry point for building a new HttpClient
    */
  def client: HttpClientConfig = HttpClientConfig()

  /**
    * Create a new request
    */
  def request(method: String, uri: String) =
    HttpMessage.Request.empty.withMethod(method).withUri(uri)

  /**
    * Create a new request
    */
  def request(uri: String): HttpMessage.Request = request(HttpMethod.GET, uri)
  def GET(uri: String)                          = request(HttpMethod.GET, uri)
  def POST(uri: String)                         = request(HttpMethod.POST, uri)
  def DELETE(uri: String)                       = request(HttpMethod.DELETE, uri)
  def PUT(uri: String)                          = request(HttpMethod.PUT, uri)
  def PATCH(uri: String)                        = request(HttpMethod.PATCH, uri)

  def response(status: HttpStatus = HttpStatus.Ok_200): HttpMessage.Response = {
    HttpMessage.Response.empty.withStatus(status)
  }

  def response(status: HttpStatus, content: String): HttpMessage.Response = {
    response(status).withContent(content)
  }

  /**
    * Create an exception to redirect (status code = 302) the request to the target locationUrl
    *
    * @param locationUrl
    * @param status
    * @return
    */
  def redirectException(
      locationUrl: String,
      status: HttpStatus = HttpStatus.Found_302
  ): HttpServerException = {
    new HttpServerException(status).withHeader(HttpHeader.Location, locationUrl)
  }

  /**
    * Create a new server exception that can be used to exit the Endpoint or RPC process.
    */
  def serverException(status: HttpStatus): HttpServerException = {
    new HttpServerException(status)
  }

  /**
    * Create a new server exception that can be used to exit the Endpoint or RPC process. The content type will be the
    * same with the Accept type
    */
  def serverException(request: Request, status: HttpStatus): HttpServerException = {
    val e = new HttpServerException(status)
    if (request.acceptsMsgPack) {
      e.withContentTypeMsgPack
    } else {
      e.withContentTypeJson
    }
  }

  /**
    * Create a new server exception with an explicit cause
    */
  def serverException(status: HttpStatus, cause: Throwable): HttpServerException = {
    new HttpServerException(status, cause.getMessage, cause)
  }

  private[http] def parseAcceptHeader(value: Option[String]): Seq[String] = {
    value
      .map(_.split(",").map(_.trim).filter(_.nonEmpty).toSeq)
      .getOrElse(Seq.empty)
  }

  private[http] def formatInstant(date: Instant): String = {
    val HttpDateFormat: DateTimeFormatter =
      DateTimeFormatter
        .ofPattern("EEE, dd MMM yyyy HH:mm:ss 'GMT'")
        .withLocale(Locale.ENGLISH)
        .withZone(ZoneId.of("GMT"))

    date.atOffset(ZoneOffset.UTC).format(HttpDateFormat)
  }

}

/**
  * HttpRequest[Req] wraps native request classes (e.g., okhttp's Response, finagle Response, etc.) so that we can
  * implement common logic for various backends.
  *
  * @tparam Req
  */
trait HttpRequest[Req] {
  protected def adapter: HttpRequestAdapter[Req]
  def toRaw: Req
  def toHttpRequest: HttpMessage.Request = adapter.httpRequestOf(toRaw)

  def header: HttpMultiMap = adapter.headerOf(toRaw)

  def message: HttpMessage.Message = adapter.messageOf(toRaw)
  def contentType: Option[String]  = adapter.contentTypeOf(toRaw)
  def contentBytes: Array[Byte]    = adapter.contentBytesOf(toRaw)
  def contentString: String        = adapter.contentStringOf(toRaw)
  def accept: Seq[String] =
    Http.parseAcceptHeader(header.get(HttpHeader.Accept))
  def acceptsMsgPack: Boolean = {
    accept.contains(HttpHeader.MediaType.ApplicationMsgPack) ||
    // legacy header
    accept.contains("application/x-msgpack")
  }

  def acceptsJson: Boolean = {
    accept.contains(HttpHeader.MediaType.ApplicationJson) ||
    // Plain JSON header without encoding type
    accept.contains("application/json")
  }
}

/**
  * HttpResponse[Resp] wraps native response classes (e.g., okhttp's Response, finagle Response, etc.) so that we can
  * implement common logic for various backends.
  *
  * @tparam Resp
  */
trait HttpResponse[Resp] {
  protected def adapter: HttpResponseAdapter[Resp]
  def toRaw: Resp
  def toHttpResponse: HttpMessage.Response = adapter.httpResponseOf(toRaw)

  def status: HttpStatus   = adapter.statusOf(toRaw)
  def header: HttpMultiMap = adapter.headerOf(toRaw)

  def message: HttpMessage.Message = adapter.messageOf(toRaw)
  def contentType: Option[String]  = adapter.contentTypeOf(toRaw)
  def contentBytes: Array[Byte]    = adapter.contentBytesOf(toRaw)
  def contentString: String        = adapter.contentStringOf(toRaw)
}

/**
  * A type class to bridge the original requests and backend-specific request types (e.g., finagle, okhttp, etc.)
  *
  * @tparam Req
  */
trait HttpRequestAdapter[Req] {
  def requestType: Class[Req]

  def methodOf(request: Req): String

  /**
    * [/path](?[query params...])
    *
    * @param request
    * @return
    */
  def uriOf(request: Req): String
  def pathOf(request: Req): String
  def queryOf(request: Req): HttpMultiMap
  def headerOf(request: Req): HttpMultiMap
  def messageOf(request: Req): HttpMessage.Message
  def contentStringOf(request: Req): String = messageOf(request).toContentString
  def contentBytesOf(request: Req): Array[Byte] =
    messageOf(request).toContentBytes
  def contentTypeOf(request: Req): Option[String]
  def pathComponentsOf(request: Req): IndexedSeq[String] = {
    pathOf(request).replaceFirst("/", "").split("/").toIndexedSeq
  }
  def httpRequestOf(request: Req): HttpMessage.Request = {
    Http
      .request(methodOf(request), uriOf(request))
      .withHeader(headerOf(request))
      .withContent(messageOf(request))
  }
  def wrap(request: Req): HttpRequest[Req]
}

/**
  * A type class to bridge the original response type and HttpResponse
  *
  * @tparam Resp
  */
trait HttpResponseAdapter[Resp] {
  def statusOf(resp: Resp): HttpStatus = HttpStatus.ofCode(statusCodeOf(resp))
  def statusCodeOf(resp: Resp): Int

  def messageOf(resp: Resp): HttpMessage.Message
  def contentStringOf(resp: Resp): String     = messageOf(resp).toContentString
  def contentBytesOf(resp: Resp): Array[Byte] = messageOf(resp).toContentBytes
  def contentTypeOf(resp: Resp): Option[String]
  def headerOf(resp: Resp): HttpMultiMap

  def httpResponseOf(resp: Resp): HttpMessage.Response = {
    Http
      .response(statusOf(resp))
      .withHeader(headerOf(resp))
      .withContent(messageOf(resp))
  }
  def wrap(resp: Resp): HttpResponse[Resp]
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy