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

com.twitter.finatra.http.response.ResponseBuilder.scala Maven / Gradle / Ivy

The newest version!
package com.twitter.finatra.http.response

import com.google.common.net.{HttpHeaders, MediaType}
import com.twitter.finagle.http.{Cookie => FinagleCookie, _}
import com.twitter.finagle.netty3.ChannelBufferBuf
import com.twitter.finagle.stats.StatsReceiver
import com.twitter.finatra.http.contexts.RouteInfo
import com.twitter.finatra.http.exceptions.HttpResponseException
import com.twitter.finatra.http.internal.marshalling.MessageBodyManager
import com.twitter.finatra.http.marshalling.mustache.MustacheBodyComponent
import com.twitter.finatra.http.routing.FileResolver
import com.twitter.finatra.json.FinatraObjectMapper
import com.twitter.inject.Logging
import com.twitter.io.Buf
import com.twitter.util.{Future, Memoize}
import java.io.{BufferedInputStream, File, FileInputStream, InputStream}
import javax.inject.Inject
import org.apache.commons.io.FilenameUtils._
import org.apache.commons.io.IOUtils
import org.jboss.netty.buffer.{ChannelBuffer, ChannelBuffers}
import org.jboss.netty.handler.codec.http.{Cookie => NettyCookie, DefaultCookie}
import scala.runtime.BoxedUnit

object ResponseBuilder {
  val DefaultCharset = "charset=utf-8"
  val PlainTextContentType = "text/plain; " + DefaultCharset
}

//TODO: Generate more response permutations
class ResponseBuilder @Inject()(
  objectMapper: FinatraObjectMapper,
  fileResolver: FileResolver,
  messageBodyManager: MessageBodyManager,
  statsReceiver: StatsReceiver)
  extends Logging {

  /* Status Codes */

  def status(statusCode: Int): EnrichedResponse = status(Status(statusCode))

  def status(responseStatus: Status): EnrichedResponse = EnrichedResponse(responseStatus)

  def ok: EnrichedResponse = EnrichedResponse(Status.Ok)

  def ok(body: Any): EnrichedResponse = EnrichedResponse(Status.Ok).body(body)

  /**
   * Returns an `Ok` response with a written body, potentially based on values
   * contained within the `Request`.
   *
   * @note This version is useful when the `body` parameter requires custom
   * message body rendering and values in the `Request` are required for
   * decision making.
   *
   * @param request the HTTP Request associated with this response
   * @param body the response body, or the information needed to render the body
   */
  def ok(request: Request, body: Any): EnrichedResponse = EnrichedResponse(Status.Ok).body(request, body)

  def ok(body: String): EnrichedResponse = EnrichedResponse(Status.Ok).body(body)

  def noContent: EnrichedResponse = EnrichedResponse(Status.NoContent)

  def notAcceptable: EnrichedResponse = EnrichedResponse(Status.NotAcceptable)

  def notAcceptable(body: Any): EnrichedResponse = EnrichedResponse(Status.NotAcceptable).body(body)

  def created = EnrichedResponse(Status.Created)

  def created(body: Any) = EnrichedResponse(Status.Created).body(body)

  def accepted = EnrichedResponse(Status.Accepted)

  def accepted(body: Any) = EnrichedResponse(Status.Accepted).body(body)

  def movedPermanently = EnrichedResponse(Status.MovedPermanently)

  def movedPermanently(body: Any) = EnrichedResponse(Status.MovedPermanently).body(body)

  def found = EnrichedResponse(Status.Found)

  def notModified = EnrichedResponse(Status.NotModified)

  def temporaryRedirect = EnrichedResponse(Status.TemporaryRedirect)

  def methodNotAllowed = EnrichedResponse(Status.MethodNotAllowed)

  def badRequest = EnrichedResponse(Status.BadRequest)

  def badRequest(body: Any) = EnrichedResponse(Status.BadRequest).body(body)

  def conflict = EnrichedResponse(Status.Conflict)

  def conflict(body: Any) = EnrichedResponse(Status.Conflict).body(body)

  def unauthorized = EnrichedResponse(Status.Unauthorized)

  def unauthorized(body: Any) = EnrichedResponse(Status.Unauthorized).body(body)

  def forbidden = EnrichedResponse(Status.Forbidden)

  def forbidden(body: Any) = EnrichedResponse(Status.Forbidden).body(body)

  def notFound = EnrichedResponse(Status.NotFound)

  def notFound(body: String) = EnrichedResponse(Status.NotFound).plain(body)

  def notFound(body: Any) = EnrichedResponse(Status.NotFound).body(body)

  def preconditionFailed = EnrichedResponse(Status.PreconditionFailed)

  def preconditionFailed(body: Any) = EnrichedResponse(Status.PreconditionFailed).body(body)

  def requestEntityTooLarge = EnrichedResponse(Status.RequestEntityTooLarge)

  def requestEntityTooLarge(body: Any) = EnrichedResponse(Status.RequestEntityTooLarge).body(body)

  def gone = EnrichedResponse(Status.Gone)

  def gone(body: Any) = EnrichedResponse(Status.Gone).body(body)

  def internalServerError = EnrichedResponse(Status.InternalServerError)

  def internalServerError(body: Any) = EnrichedResponse(Status.InternalServerError).body(body)

  def notImplemented = EnrichedResponse(Status.NotImplemented)

  def serviceUnavailable = EnrichedResponse(Status.ServiceUnavailable)

  def clientClosed = EnrichedResponse(Status.ClientClosedRequest)

  object EnrichedResponse {
    def apply(s: Status): EnrichedResponse = EnrichedResponse(Response(Version.Http11, s))
  }

  /* Wrapper around Finagle Response which exposes a builder like API */
  case class EnrichedResponse(resp: Response)
    extends ResponseProxy {
    override val response = resp

    /* Public */

    def cookie(k: String, v: String): EnrichedResponse = {
      cookie(new FinagleCookie(new DefaultCookie(k, v)))
      this
    }

    def cookie(c: FinagleCookie): EnrichedResponse = {
      response.addCookie(c)
      this
    }

    def cookie(c: NettyCookie): EnrichedResponse = {
      response.addCookie(new FinagleCookie(c))
      this
    }

    def json(obj: Any) = {
      contentTypeJson()

      obj match {
        case bytes: Array[Byte] => body(bytes)
        case str: String => body(str)
        case _ =>
          response.withOutputStream { os =>
            objectMapper.writeValue(obj, os)
          }
      }
      this
    }

    def jsonError = {
      json(ErrorsResponse(status.reason.toLowerCase))
      this
    }

    def body(any: Any): EnrichedResponse = body(None, any)

    /**
     * Returns a response with a written body, potentially based on values
     * contained within the `Request`.
     *
     * @note This version is useful when the `any` parameter requires custom
     * message body rendering and values in the `Request` are required for
     * decision making.
     *
     * @param request the HTTP Request associated with this response
     * @param any the body, or the information needed to render the body
     */
    def body(request: Request, any: Any): EnrichedResponse = body(Some(request), any)

    def file(file: File): EnrichedResponse = {
      body(
        new BufferedInputStream(
          new FileInputStream(file)))

      contentType(
        fileResolver.getContentType(file.getName))
    }

    def body(b: Array[Byte]): EnrichedResponse = {
      response.content = Buf.ByteArray.Owned(b)
      this
    }

    def body(bodyStr: String): EnrichedResponse = {
      response.setContentString(bodyStr)
      this
    }

    def body(inputStream: InputStream): EnrichedResponse = {
      body(
        ChannelBufferBuf.Owned(
          ChannelBuffers.wrappedBuffer(
            IOUtils.toByteArray(inputStream)
          )
        )
      )
      this
    }

    @deprecated("use body(Buf)", "2015-08-20")
    def body(channelBuffer: ChannelBuffer): EnrichedResponse = {
      response.content = ChannelBufferBuf.Owned(channelBuffer)
      this
    }

    def body(buffer: Buf): EnrichedResponse = {
      response.content = buffer
      this
    }

    def contentTypeJson() = {
      contentType("application/json")
      this
    }

    def nothing = {
      this
    }

    def plain(any: Any): EnrichedResponse = {
      response.headerMap.set(HttpHeaders.CONTENT_TYPE, mediaToString(MediaType.PLAIN_TEXT_UTF_8))
      body(any)
    }

    def html(html: String) = {
      response.headerMap.set(HttpHeaders.CONTENT_TYPE, mediaToString(MediaType.HTML_UTF_8))
      body(html)
      this
    }

    def html(any: Any) = {
      response.headerMap.set(HttpHeaders.CONTENT_TYPE, mediaToString(MediaType.HTML_UTF_8))
      body(any)
      this
    }

    def location(uri: Any): EnrichedResponse = {
      location(uri.toString)
    }

    def location(uri: String): EnrichedResponse = {
      response.headerMap.set("Location", uri)
      this
    }

    def header(k: String, v: Any) = {
      response.headerMap.set(k, v.toString)
      this
    }

    def header(k: String, v: MediaType) = {
      response.headerMap.set(k, mediaToString(v))
      this
    }

    def headers(map: Map[String, String]) = {
      for ((k, v) <- map) {
        response.headerMap.set(k, v)
      }
      this
    }

    def headers(entries: (String, Any)*) = {
      for ((k, v) <- entries) {
        response.headerMap.set(k, v.toString)
      }
      this
    }

    def contentType(mimeType: String) = {
      response.headerMap.set(
        Fields.ContentType,
        mimeType + "; " + ResponseBuilder.DefaultCharset)
      this
    }

    def contentType(mimeType: MediaType) = {
      response.headerMap.set(
        Fields.ContentType,
        mediaToString(mimeType))
      this
    }

    def file(file: String): Response = {
      val fileWithSlash = if (file.startsWith("/")) file else "/" + file
      fileResolver.getInputStream(fileWithSlash) map { inputStream =>
        contentType(fileResolver.getContentType(file))
        body(inputStream)
      } getOrElse {
        notFound.plain(fileWithSlash + " not found")
      }
    }

    /**
     * Return the file (only if it's a file w/ an extension), otherwise return the index.
     * Note: This functionality is useful for "single-page" UI frameworks (e.g. AngularJS)
     * that perform client side routing.
     */
    def fileOrIndex(filePath: String, indexPath: String) = {
      if (isFile(filePath))
        file(filePath)
      else
        file(indexPath)
    }

    def view(template: String, obj: Any) = {
      html(MustacheBodyComponent(obj, template))
    }

    /* Exception Stats */

    def handled(
      request: Request,
      e: Throwable,
      details: String*) = {

      incrementCounter("handled", statsReceiver, request, e, details: _*)
    }

    /* Public Conversions */

    def toFuture: Future[Response] = Future.value(response)

    def toException: HttpResponseException = new HttpResponseException(response)

    def toFutureException[T]: Future[T] = Future.exception(toException)

    /* Private */

    // route/insights_engagement/POST/status/503/unhandled/NumberFormatException
    private[finatra] def unhandled(
      request: Request,
      e: Throwable,
      details: String*) = {

      error("Unhandled Exception", e)
      incrementCounter("unhandled", statsReceiver, request, e, details: _*)
    }

    private def incrementCounter(
      handleType: String,
      statsReceiver: StatsReceiver,
      request: Request,
      e: Throwable,
      details: String*): ResponseBuilder#EnrichedResponse = {

      val routeInfo = RouteInfo(request).getOrElse(throw new Exception("handled can only be used within a Finatra HTTP request callback"))

      val counter = statsReceiver.counter(Seq(
        "route",
        routeInfo.sanitizedPath,
        request.method.toString,
        "status",
        status.code.toString,
        handleType,
        e.getClass.getSimpleName) ++
        details: _*)

      counter.incr()

      this
    }

    private def isFile(requestPath: String) = {
      getExtension(requestPath).nonEmpty
    }

    private def body(request: Option[Request], any: Any): EnrichedResponse = {
      any match {
        case null => nothing
        case buf: Buf => body(buf)
        case bytes: Array[Byte] => body(bytes)
        case cbos: ChannelBuffer => body(ChannelBufferBuf.Owned(cbos))
        case "" => nothing
        case Unit => nothing
        case _: BoxedUnit => nothing
        case opt if opt == None => nothing
        case str: String => body(str)
        case _file: File => file(_file)
        case _ =>
          val writer = messageBodyManager.writer(any)
          val writerResponse = request match {
            case Some(req) => writer.write(req, any)
            case None      => writer.write(any)
          }
          body(writerResponse.body)
          contentType(writerResponse.contentType)
          headers(writerResponse.headers)
      }
      this
    }

    //optimized: MediaType.toString is a hotspot when profiling
    private val mediaToString = Memoize { mediaType: MediaType =>
      mediaType.toString
    }
  }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy