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

com.twitter.finagle.http.RequestBuilder.scala Maven / Gradle / Ivy

package com.twitter.finagle.http

import com.twitter.finagle.http.netty.Bijections
import com.twitter.finagle.netty3.BufChannelBuffer
import com.twitter.util.Base64StringEncoder
import com.twitter.io.Buf
import java.net.URL
import org.jboss.netty.buffer.{ChannelBuffer, ChannelBuffers}
import org.jboss.netty.handler.codec.http.multipart.{DefaultHttpDataFactory, HttpPostRequestEncoder, HttpDataFactory}
import org.jboss.netty.handler.codec.http.{HttpRequest, HttpHeaders}
import scala.annotation.implicitNotFound
import scala.collection.JavaConversions._
import scala.collection.mutable.ListBuffer

import Bijections._

/*
 * HTML form element.
 */
sealed abstract class FormElement

/*
 * HTML form simple input field.
 */
case class SimpleElement(name: String, content: String) extends FormElement

/*
 * HTML form file input field.
 */
case class FileElement(name: String, content: Buf, contentType: Option[String] = None,
  filename: Option[String] = None) extends FormElement

/**
 * Provides a class for building [[org.jboss.netty.handler.codec.http.HttpRequest]]s.
 * The main class to use is [[com.twitter.finagle.http.RequestBuilder]], as so
 *
 * {{{
 * val getRequest = RequestBuilder()
 *   .setHeader(HttpHeaders.Names.USER_AGENT, "MyBot")
 *   .setHeader(HttpHeaders.Names.CONNECTION, HttpHeaders.Values.KEEP_ALIVE)
 *   .url(new URL("http://www.example.com"))
 *   .buildGet()
 * }}}
 *
 * The `RequestBuilder` requires the definition of `url`. In Scala,
 * this is statically type checked, and in Java the lack of any of
 * a url causes a runtime error.
 *
 * The `buildGet`, 'buildHead`, `buildPut`, and `buildPost` methods use an implicit argument
 * to statically typecheck the builder (to ensure completeness, see above).
 * The Java compiler cannot provide such implicit, so we provide separate
 * functions in Java to accomplish this. Thus, the Java code for the
 * above is
 *
 * {{{
 * HttpRequest getRequest =
 *   RequestBuilder.safeBuildGet(
 *     RequestBuilder.create()
 *       .setHeader(HttpHeaders.Names.USER_AGENT, "MyBot")
 *       .setHeader(HttpHeaders.Names.CONNECTION, HttpHeaders.Values.KEEP_ALIVE)
 *       .url(new URL("http://www.example.com")))
 * }}}
 *
 * Overall RequestBuilder's pretty barebones. It does provide certain protocol level support
 * for more involved requests. For example, it support easy creation of POST request to submit
 * multipart web forms with `buildMultipartPost` and default form post with `buildFormPost`.
 */

/**
 * Factory for [[com.twitter.finagle.http.RequestBuilder]] instances
 */
object RequestBuilder {
  @implicitNotFound("Http RequestBuilder is not correctly configured: HasUrl (exp: Yes): ${HasUrl}, HasForm (exp: Nothing) ${HasForm}.")
  trait RequestEvidence[HasUrl, HasForm]
  private object RequestEvidence {
    implicit object FullyConfigured extends RequestEvidence[RequestConfig.Yes, Nothing]
  }

  @implicitNotFound("Http RequestBuilder is not correctly configured for form post: HasUrl (exp: Yes): ${HasUrl}, HasForm (exp: Yes): ${HasForm}.")
  trait PostRequestEvidence[HasUrl, HasForm]
  private object PostRequestEvidence {
    implicit object FullyConfigured extends PostRequestEvidence[RequestConfig.Yes, RequestConfig.Yes]
  }

  type Complete = RequestBuilder[RequestConfig.Yes, Nothing]
  type CompleteForm = RequestBuilder[RequestConfig.Yes, RequestConfig.Yes]

  def apply() = new RequestBuilder()

  /**
   * Used for Java access.
   */
  def create() = apply()

  /**
   * Provides a typesafe `build` with content for Java.
   */
  def safeBuild(builder: Complete, method: Method, content: Option[Buf]): Request =
    builder.build(method, content)(RequestEvidence.FullyConfigured)

  /**
   * Provides a typesafe `buildGet` for Java.
   */
  def safeBuildGet(builder: Complete): Request =
    builder.buildGet()(RequestEvidence.FullyConfigured)

  /**
   * Provides a typesafe `buildHead` for Java.
   */
  def safeBuildHead(builder: Complete): Request =
    builder.buildHead()(RequestEvidence.FullyConfigured)

  /**
   * Provides a typesafe `buildDelete` for Java.
   */
  def safeBuildDelete(builder: Complete): Request =
    builder.buildDelete()(RequestEvidence.FullyConfigured)

  /**
   * Provides a typesafe `buildPut` for Java.
   */
  def safeBuildPut(builder: Complete, content: Buf): Request =
    builder.buildPut(content)(RequestEvidence.FullyConfigured)

  /**
   * Provides a typesafe `buildPost` for Java.
   */
  def safeBuildPost(builder: Complete, content: Buf): Request =
    builder.buildPost(content)(RequestEvidence.FullyConfigured)

  /**
   * Provides a typesafe `buildFormPost` for Java.
   */
  def safeBuildFormPost(builder: CompleteForm, multipart: Boolean): Request =
    builder.buildFormPost(multipart)(PostRequestEvidence.FullyConfigured)
}

object RequestConfig {
  sealed abstract trait Yes

  type FullySpecifiedConfig = RequestConfig[Yes, Nothing]
  type FullySpecifiedConfigForm = RequestConfig[Yes, Yes]
}

private[http] final case class RequestConfig[HasUrl, HasForm](
  url: Option[URL]                  = None,
  headers: Map[String, Seq[String]] = Map.empty,
  formElements: Seq[FormElement]    = Nil,
  version: Version                  = Version.Http11,
  proxied: Boolean                  = false
)

class RequestBuilder[HasUrl, HasForm] private[http](
  config: RequestConfig[HasUrl, HasForm]
) {
  import RequestConfig._

  type This = RequestBuilder[HasUrl, HasForm]

  private[this] val SCHEME_WHITELIST = Seq("http","https")

  private[http] def this() = this(RequestConfig())

  /*
   * Specify url as String
   */
  def url(u: String): RequestBuilder[Yes, HasForm] = url(new java.net.URL(u))

  /**
   * Specify the url to request. Sets the HOST header and possibly
   * the Authorization header using the authority portion of the URL.
   */
  def url(u: URL): RequestBuilder[Yes, HasForm] = {
    require(SCHEME_WHITELIST.contains(u.getProtocol), "url must be http(s)")
    val uri = u.toURI
    val host = uri.getHost.toLowerCase
    val hostValue =
      if (u.getPort == -1 || u.getDefaultPort == u.getPort)
        host
      else
        "%s:%d".format(host, u.getPort)
    val withHost = config.headers.updated(HttpHeaders.Names.HOST, Seq(hostValue))
    val userInfo =  uri.getUserInfo
    val updated =
      if (userInfo == null || userInfo.isEmpty)
        withHost
      else {
        val auth = "Basic " + Base64StringEncoder.encode(userInfo.getBytes)
        withHost.updated(HttpHeaders.Names.AUTHORIZATION, Seq(auth))
      }
    new RequestBuilder(config.copy(url = Some(u), headers = updated))
  }

  /*
   * Add simple form name/value pairs. In this mode, this RequestBuilder will only
   * be able to generate a multipart/form POST request.
   */
   def addFormElement(kv: (String, String)*): RequestBuilder[HasUrl, Yes] = {
     val elems = config.formElements
     val updated = kv.foldLeft(elems) { case (es, (k, v)) => es :+ new SimpleElement(k, v) }
     new RequestBuilder(config.copy(formElements = updated))
   }

  /*
   * Add a FormElement to a request. In this mode, this RequestBuilder will only
   * be able to generate a multipart/form POST request.
   */
  def add(elem: FormElement): RequestBuilder[HasUrl, Yes] = {
    val elems = config.formElements
    val updated = elems ++ Seq(elem)
    new RequestBuilder(config.copy(formElements = updated))
  }

  /*
   * Add a group of FormElements to a request. In this mode, this RequestBuilder will only
   * be able to generate a multipart/form POST request.
   */
  def add(elems: Seq[FormElement]): RequestBuilder[HasUrl, Yes] = {
    val first = this.add(elems.head)
    elems.tail.foldLeft(first) { (b, elem) => b.add(elem) }
  }

  /**
   * Declare the HTTP protocol version be HTTP/1.0
   */
  def http10(): This =
    new RequestBuilder(config.copy(version = Version.Http10))

  /**
   * Set a new header with the specified name and value.
   */
  def setHeader(name: String, value: String): This = {
    val updated = config.headers.updated(name, Seq(value))
    new RequestBuilder(config.copy(headers = updated))
  }

  /**
   * Set a new header with the specified name and values.
   */
  def setHeader(name: String, values: Seq[String]): This = {
    val updated = config.headers.updated(name, values)
    new RequestBuilder(config.copy(headers = updated))
  }

  /**
   * Set a new header with the specified name and values.
   *
   * Java convenience variant.
   */
  def setHeader(name: String, values: java.lang.Iterable[String]): This = {
    setHeader(name, values.toSeq)
  }

  /**
   * Add a new header with the specified name and value.
   */
  def addHeader(name: String, value: String): This = {
    val values = config.headers.get(name).getOrElse(Seq())
    val updated = config.headers.updated(
      name, values ++ Seq(value))
    new RequestBuilder(config.copy(headers = updated))
  }

  /**
   * Add group of headers expressed as a Map
   */
  def addHeaders(headers: Map[String, String]): This = {
      headers.foldLeft(this) { case (b, (k, v)) => b.addHeader(k, v) }
  }

  /**
   * Declare the request will be proxied. Results in using the
   * absolute URI in the request line.
   */
  def proxied(): This = proxied(None)

  /**
   * Declare the request will be proxied. Results in using the
   * absolute URI in the request line and setting the Proxy-Authorization
   * header using the provided {{ProxyCredentials}}.
   */
  def proxied(credentials: ProxyCredentials): This = proxied(Some(credentials))

  /**
   * Declare the request will be proxied. Results in using the
   * absolute URI in the request line and optionally setting the
   * Proxy-Authorization header using the provided {{ProxyCredentials}}.
   */
  def proxied(credentials: Option[ProxyCredentials]): This = {
    val headers: Map[String,Seq[String]] = credentials map { creds =>
      config.headers.updated(HttpHeaders.Names.PROXY_AUTHORIZATION, Seq(creds.basicAuthorization))
    } getOrElse config.headers

    new RequestBuilder(config.copy(headers = headers, proxied = true))
  }

  /**
   * Construct an HTTP request with a specified method.
   */
  def build(method: Method, content: Option[Buf])(
    implicit HTTP_REQUEST_BUILDER_IS_NOT_FULLY_SPECIFIED: RequestBuilder.RequestEvidence[HasUrl, HasForm]
  ): Request = {
    content match {
      case Some(content) => withContent(method, content)
      case None => withoutContent(method)
    }
  }

  /**
   * Construct an HTTP GET request.
   */
  def buildGet()(
    implicit HTTP_REQUEST_BUILDER_IS_NOT_FULLY_SPECIFIED: RequestBuilder.RequestEvidence[HasUrl, HasForm]
  ): Request = withoutContent(Method.Get)

  /**
   * Construct an HTTP HEAD request.
   */
  def buildHead()(
    implicit HTTP_REQUEST_BUILDER_IS_NOT_FULLY_SPECIFIED: RequestBuilder.RequestEvidence[HasUrl, HasForm]
  ): Request = withoutContent(Method.Head)

  /**
   * Construct an HTTP DELETE request.
   */
  def buildDelete()(
    implicit HTTP_REQUEST_BUILDER_IS_NOT_FULLY_SPECIFIED: RequestBuilder.RequestEvidence[HasUrl, HasForm]
  ): Request = withoutContent(Method.Delete)

  /**
   * Construct an HTTP POST request.
   */
  def buildPost(content: Buf)(
    implicit HTTP_REQUEST_BUILDER_IS_NOT_FULLY_SPECIFIED: RequestBuilder.RequestEvidence[HasUrl, HasForm]
  ): Request = withContent(Method.Post, content)

  /**
   * Construct an HTTP PUT request.
   */
  def buildPut(content: Buf)(
    implicit HTTP_REQUEST_BUILDER_IS_NOT_FULLY_SPECIFIED: RequestBuilder.RequestEvidence[HasUrl, HasForm]
  ): Request = withContent(Method.Put, content)

  /**
   * Construct a form post request.
   */
  def buildFormPost(multipart: Boolean = false) (
    implicit HTTP_REQUEST_BUILDER_IS_NOT_FULLY_SPECIFIED: RequestBuilder.PostRequestEvidence[HasUrl, HasForm]
  ): Request = {
    val dataFactory = new DefaultHttpDataFactory(false) // we don't use disk
    val req = withoutContent(Method.Post)
    val encoder = new HttpPostRequestEncoder(dataFactory, req.httpRequest, multipart)

    config.formElements.foreach {
      case FileElement(name, content, contentType, filename) =>
        HttpPostRequestEncoderEx.addBodyFileUpload(encoder, dataFactory, req.httpRequest)(
          name, filename.getOrElse(""),
          BufChannelBuffer(content),
          contentType.getOrElse(null),
          false)

      case SimpleElement(name, value) =>
        encoder.addBodyAttribute(name, value)
    }
    val encodedReq = encoder.finalizeRequest()

    if (encodedReq.isChunked) {
      val encodings = encodedReq.headers.getAll(HttpHeaders.Names.TRANSFER_ENCODING)
      encodings.remove(HttpHeaders.Values.CHUNKED)
      if (encodings.isEmpty)
        encodedReq.headers.remove(HttpHeaders.Names.TRANSFER_ENCODING)
      else
        encodedReq.headers.set(HttpHeaders.Names.TRANSFER_ENCODING, encodings)

      val chunks = new ListBuffer[ChannelBuffer]
      while (encoder.hasNextChunk) {
        chunks += encoder.nextChunk().getContent()
      }
      encodedReq.setContent(ChannelBuffers.wrappedBuffer(chunks:_*))
    }

    from(encodedReq)
  }

  // absoluteURI if proxied, otherwise relativeURI
  private[this] def resource(): String = {
    val url = config.url.get
    if (config.proxied) {
      return url.toString
    } else {
      val builder = new StringBuilder()

      val path = url.getPath
      if (path == null || path.isEmpty)
        builder.append("/")
      else
        builder.append(path)

      val query = url.getQuery
      if (query != null && !query.isEmpty)
        builder.append("?%s".format(query))

      builder.toString
    }
  }

  private[http] def withoutContent(method: Method): Request = {
    val req = Request(config.version, method, resource)
    config.headers foreach { case (field, values) =>
      values foreach { v => req.headers.add(field, v) }
    }
    req
  }

  private[http] def withContent(method: Method, content: Buf): Request = {
    require(content != null)
    val req = withoutContent(method)
    req.content = content
    req.headers.set(HttpHeaders.Names.CONTENT_LENGTH, content.length.toString)
    req
  }
}

/**
 * Add a missing method to HttpPostRequestEncoder to allow specifying a ChannelBuffer directly as
 * content of a file. This logic should eventually move to netty.
 */
private object HttpPostRequestEncoderEx {
  //TODO: HttpPostBodyUtil not accessible from netty 3.5.0.Final jar
  //      This HttpPostBodyUtil simulates what we need.
  object HttpPostBodyUtil {
    val DEFAULT_TEXT_CONTENT_TYPE = "text/plain"
    val DEFAULT_BINARY_CONTENT_TYPE = "application/octet-stream"
    object TransferEncodingMechanism {
      val BINARY = "binary"
      val BIT7 = "7bit"
    }
  }

  /*
   * allow specifying post body as ChannelBuffer, the logic is adapted from netty code.
   */
  def addBodyFileUpload(encoder: HttpPostRequestEncoder, factory: HttpDataFactory, request: HttpRequest)
    (name: String, filename: String, content: ChannelBuffer, contentType: String, isText: Boolean) {
    require(name != null)
    require(filename != null)
    require(content != null)

    val scontentType =
      if (contentType == null) {
        if (isText) {
          HttpPostBodyUtil.DEFAULT_TEXT_CONTENT_TYPE
        } else {
          HttpPostBodyUtil.DEFAULT_BINARY_CONTENT_TYPE
        }
      } else {
        contentType
      }
    val contentTransferEncoding =
      if (!isText) {
        HttpPostBodyUtil.TransferEncodingMechanism.BINARY
      } else {
        HttpPostBodyUtil.TransferEncodingMechanism.BIT7
      }

    val fileUpload = factory.createFileUpload(request, name, filename, scontentType, contentTransferEncoding, null, content.readableBytes)
    fileUpload.setContent(content)
    encoder.addBodyHttpData(fileUpload)
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy