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

com.nrinaudo.fetch.Request.scala Maven / Gradle / Ivy

The newest version!
package com.nrinaudo.fetch

import org.apache.commons.codec.binary.Base64
import java.util.Date
import Headers._
import java.nio.charset.Charset
import java.net.URI

object Request {
  /** Type for underlying HTTP engines.
    *
    * Fetch comes with a default, `java.net.URLConnection` based [[com.nrinaudo.fetch.net.UrlEngine implementation]],
    * but it might not be applicable to all use-cases. Defining your own engine allows you to use another underlying
    * library, such as [[http://hc.apache.org/httpclient-3.x/ Apache HTTP client]].
    */
  type HttpEngine = (Url, Method, Option[RequestEntity], Headers) => Response[ResponseEntity]

  private def http(f: HttpEngine): HttpEngine = (url, method, body, headers) => {
    var h = headers

    // Sets body specific HTTP headers (or unsets them if necessary).
    body foreach {b =>
      h = h.set("Content-Type", b.mediaType)
      if(b.encoding == Encoding.Identity) h = h.remove("Content-Encoding")
      else                                h = h.set("Content-Encoding", b.encoding)
    }

    // I'm not entirely happy with forcing a default Accept header - it's perfectly legal for it to be empty. The
    // standard URLConnection forces a somewhat messed up default, however (image/gif, what were they thinking?),
    // and */* is curl's default behaviour - if it's good enough for curl, it's good enough for me.
    h = h.setIfEmpty("User-Agent", UserAgent).setIfEmpty("Accept", "*/*")

    f(url, method, body, h)
  }

  def from(uri: URI)(implicit engine: HttpEngine): Option[Request[Response[ResponseEntity]]] =
    Url.fromUri(uri).map(apply)

  def from(url: String)(implicit engine: HttpEngine): Option[Request[Response[ResponseEntity]]] =
    Url.parse(url).map(apply)

  /**
   * Creates a new instance of [[Request]].
   *
   * The newly created instance will default to [[Method.GET]].
   *
   * @param url    url on which the request will be performed.
   * @param engine HTTP engine to use when performing the request.
   */
  def apply(url: Url)(implicit engine: HttpEngine): Request[Response[ResponseEntity]] =
    new RequestImpl[Response[ResponseEntity]](url, Method.GET, new Headers(), http(engine))

  // TODO: have the version number be dynamic, somehow.
  val UserAgent = "Fetch/0.2"
}

/** Represents an HTTP request.
  *
  * Instances are created through the companion object. Once an instance is obtained, the request can be
  * configured through "raw" modification methods ({{{method}}}, {{{headers}}}...) as well as specialised helpers such
  * as {{{GET}}}, {{{acceptGzip}}} or {{{/}}}.
  */
trait Request[A] {
  // - Fields ----------------------------------------------------------------------------------------------------------
  // -------------------------------------------------------------------------------------------------------------------
  /** URL on which the request will be performed. */
  val url: Url
  /** HTTP method of the request. */
  val method: Method
  /** List of HTTP headers of the request. */
  val headers: Headers



  // - Abstract methods ------------------------------------------------------------------------------------------------
  // -------------------------------------------------------------------------------------------------------------------
  protected def copy(url: Url, method: Method, headers: Headers): Request[A]
  def apply(body: Option[RequestEntity]): A

  /** Applies the specified transformation to the request's eventual response.
    *
    * Application developers should be wary of a common pitfall: when working with responses that contain instances
    * of [[ResponseEntity]], they should always clean these up, either by reading their content (transforming it
    * to something else or calling [[ResponseEntity.empty]]) or explicitly ignoring them (by calling
    * [[ResponseEntity.ignore]]).
    *
    * This is a common source of issues when mapping error statuses to exceptions: each connection will be kept
    * alive until the remote host decides it has timed out.
    */
  def map[B](f: A => B): Request[B]



  // - Execution -------------------------------------------------------------------------------------------------------
  // -------------------------------------------------------------------------------------------------------------------
  def apply(): A = apply(None)
  def apply(body: RequestEntity): A = apply(Some(body))



  // - Field modification ----------------------------------------------------------------------------------------------
  // -------------------------------------------------------------------------------------------------------------------
  def url(value: Url): Request[A] = copy(value, method, headers)
  def method(value: Method): Request[A] = copy(url, value, headers)
  def headers(value: Headers): Request[A] = copy(url, method, value)



  // - Url manipulation ------------------------------------------------------------------------------------------------
  // -------------------------------------------------------------------------------------------------------------------
  def /(segment: String): Request[A] = url(url / segment)
  def ?(value: QueryString): Request[A] = url(url ? value)
  def &[T: ValueWriter](param: (String, T)): Request[A] = url(url & param)



  // - HTTP methods ----------------------------------------------------------------------------------------------------
  // -------------------------------------------------------------------------------------------------------------------
  def GET: Request[A] = method(Method.GET)
  def POST: Request[A] = method(Method.POST)
  def PUT: Request[A] = method(Method.PUT)
  def DELETE: Request[A] = method(Method.DELETE)
  def HEAD: Request[A] = method(Method.HEAD)
  def OPTIONS: Request[A] = method(Method.OPTIONS)
  def TRACE: Request[A] = method(Method.TRACE)
  def CONNECT: Request[A] = method(Method.CONNECT)
  def PATCH: Request[A] = method(Method.PATCH)
  def LINK: Request[A] = method(Method.LINK)
  def UNLINK: Request[A] = method(Method.UNLINK)



  // - Content negotiation ---------------------------------------------------------------------------------------------
  // -------------------------------------------------------------------------------------------------------------------
  /** Notifies the remote server about transfer encoding preferences.
    *
    * This maps to the [[http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.3 Accept-Encoding]] header.
    *
    * @param  encodings list of encodings to declare.
    */
  def acceptEncoding(encodings: Conneg[Encoding]*): Request[A] = header("Accept-Encoding", encodings)

  /** Returns the value of this instance's encoding header. */
  def acceptEncoding: Option[Seq[Conneg[Encoding]]] = header[Seq[Conneg[Encoding]]]("Accept-Encoding")

  /** Notifies the remote server that we accept GZIPed responses. */
  def acceptGzip: Request[A] = acceptEncoding(Encoding.Gzip)

  /** Notifies the remote server that we accept deflated responses. */
  def acceptDeflate: Request[A] = acceptEncoding(Encoding.Deflate)

  /** Notifies the remote server about response content type preferences.
    *
    * This maps to the [[http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.1 Accept]] header.
    *
    * @param types list of media types to declare.
    */
  def accept(types: Conneg[MediaType]*): Request[A] = header("Accept", types)

  /** Returns the value of this instance's content type preferences. */
  def accept: Option[Seq[Conneg[MediaType]]] = header[Seq[Conneg[MediaType]]]("Accept")

  /** Notifies the remote server about response charset preferences.
    *
    * This maps to the [[http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.2 Accept-Charset]] header.
    *
    * @param charsets list of charsets to declare.
    */
  def acceptCharset(charsets: Conneg[Charset]*): Request[A] = header("Accept-Charset", charsets)

  /** Returns the value of this instance's charset preferences. */
  def acceptCharset: Option[Seq[Conneg[Charset]]] = header[Seq[Conneg[Charset]]]("Accept-Charset")

  /** Notifies the remote server about response language preferences.
    *
    * This maps to the [[http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.4 Accept-Language]] header.
    *
    * @param languages list of languages to declare.
    */
  def acceptLanguage(languages: Conneg[Language]*): Request[A] = header("Accept-Language", languages)

  /** Returns the value of this instance's language preferences. */
  def acceptLanguage: Option[Seq[Conneg[Language]]] = header[Seq[Conneg[Language]]]("Accept-Language")



  // - Generic headers -------------------------------------------------------------------------------------------------
  // -------------------------------------------------------------------------------------------------------------------
  /** Sets the value of the specified header.
    *
    * This method expects an appropriate implicit [[ValueWriter]] to be in scope. Standard formats are declared
    * in [[Headers$ Headers]].
    */
  def header[T: ValueWriter](name: String, value: T): Request[A] = headers(headers.set(name, value))

  /** Returns the value of the specified header. */
  def header[T: ValueReader](name: String): Option[T] = headers.getOpt[T](name)




  // - Cache headers ---------------------------------------------------------------------------------------------------
  // -------------------------------------------------------------------------------------------------------------------
  def ifModifiedSince(date: Date): Request[A] = header("If-Modified-Since", date)

  def ifModifiedSince: Option[Date] = header[Date]("If-Modified-Since")

  def ifUnmodifiedSince(date: Date): Request[A] = header("If-Unmodified-Since", date)

  def ifUnmodifiedSince: Option[Date] = header[Date]("If-Unmodified-Since")

  def ifNoneMatch(tags: ETag*): Request[A] = header("If-None-Match", tags)

  def ifNoneMatch: Option[Seq[ETag]] = header[Seq[ETag]]("If-None-Match")

  def ifMatch(tags: ETag*): Request[A] = header("If-Match", tags)

  def ifMatch: Option[Seq[ETag]] = header[Seq[ETag]]("If-Match")

  def ifRange(tag: ETag): Request[A] = header("If-Range", tag)

  def ifRange(date: Date): Request[A] = header("If-Range", date)



  // - Misc. helpers ---------------------------------------------------------------------------------------------------
  // -------------------------------------------------------------------------------------------------------------------
  def range(ranges: ByteRange*): Request[A] =
    if(ranges.isEmpty) this
    else               header("Range", ranges)

  def range: Option[Seq[ByteRange]] = header[Seq[ByteRange]]("Range")

  def date(date: Date = new Date()): Request[A] = header("Date", date)

  def date: Option[Date] = header[Date]("Date")

  def userAgent(name: String): Request[A] = header("User-Agent", name)

  def userAgent: Option[String] = header[String]("User-Agent")

  def maxForwards(value: Int): Request[A] = header("Max-Forwards", value)

  def maxForwards: Option[Int] = header[Int]("Max-Forwards")

  // TODO: do we want to wrap user & pwd in an Authorization case class?
  def auth(user: String, pwd: String): Request[A] =
    header("Authorization", "Basic " + Base64.encodeBase64String((user + ':' + pwd).getBytes))
}

private class RequestImpl[A](override val url:     Url,
                             override val method:  Method  = Method.GET,
                             override val headers: Headers = new Headers(Map[String, String]()),
                             private val  engine:  (Url, Method, Option[RequestEntity], Headers) => A)
  extends Request[A] {
  override def copy(url: Url, method: Method, headers: Headers): Request[A] =
    new RequestImpl[A](url, method, headers, engine)

  override def apply(body: Option[RequestEntity]): A = engine.apply(url, method, body, headers)

  override def map[B](f: (A) => B): Request[B] =
    new RequestImpl(url, method, headers, (a, b, c, d) => f(engine(a, b, c, d)))
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy