/src/main/scala/dispatch/Http.scala

http://scouchdb.googlecode.com/ · Scala · 226 lines · 161 code · 28 blank · 37 comment · 6 complexity · a617c6d81a4be109607990b7649d2b26 MD5 · raw file

  1. package dispatch
  2. import java.io.{InputStream,OutputStream,BufferedInputStream,BufferedOutputStream}
  3. import org.apache.http._
  4. import org.apache.http.client._
  5. import org.apache.http.impl.client.DefaultHttpClient
  6. import org.apache.http.client.methods._
  7. import org.apache.http.client.entity.UrlEncodedFormEntity
  8. import org.apache.http.client.utils.URLEncodedUtils
  9. import org.apache.http.entity.StringEntity
  10. import org.apache.http.entity.ByteArrayEntity
  11. import org.apache.http.message.BasicNameValuePair
  12. import org.apache.http.protocol.{HTTP, HttpContext}
  13. import org.apache.http.params.{HttpProtocolParams, BasicHttpParams}
  14. import org.apache.http.util.EntityUtils
  15. import org.apache.http.auth.{AuthScope, UsernamePasswordCredentials}
  16. case class StatusCode(code: Int, contents:String)
  17. extends Exception("Exceptional resoponse code: " + code + "\n" + contents)
  18. class Http(
  19. val host: Option[HttpHost],
  20. val headers: List[(String, String)],
  21. val creds: Option[(String, String)]
  22. ) {
  23. def this(host: HttpHost) = this(Some(host), Nil, None)
  24. def this(hostname: String, port: Int) = this(new HttpHost(hostname, port))
  25. def this(hostname: String) = this(new HttpHost(hostname))
  26. lazy val client = new ConfiguredHttpClient {
  27. for (h <- host; (name, password) <- creds) {
  28. getCredentialsProvider.setCredentials(
  29. new AuthScope(h.getHostName, h.getPort),
  30. new UsernamePasswordCredentials(name, password)
  31. )
  32. }
  33. }
  34. /** Uses bound host server in HTTPClient execute. */
  35. def execute(req: HttpUriRequest):HttpResponse = {
  36. // Http.log.info("%s %s", req.getMethod, req.getURI)
  37. host match {
  38. case None => client.execute(req)
  39. case Some(host) => client.execute(host, req)
  40. }
  41. }
  42. /** Sets authentication credentials for bound host. */
  43. def as (name: String, pass: String) = new Http(host, headers, Some((name, pass)))
  44. /** Add header */
  45. def << (k: String, v: String) = new Http(host, (k,v) :: headers, creds)
  46. /** eXecute wrapper */
  47. def x [T](req: HttpUriRequest) = new {
  48. /** handle response codes, response, and entity in block */
  49. def apply(block: (Int, HttpResponse, Option[HttpEntity]) => T) = {
  50. val res = execute(req)
  51. val ent = res.getEntity match {
  52. case null => None
  53. case ent => Some(ent)
  54. }
  55. try {
  56. block(res.getStatusLine.getStatusCode, res, ent) }
  57. finally { ent foreach (_.consumeContent) }
  58. }
  59. /** Handle reponse entity in block if reponse code returns true from chk. */
  60. def when(chk: Int => Boolean)(block: (HttpResponse, Option[HttpEntity]) => T) = this { (code, res, ent) =>
  61. if (chk(code)) block(res, ent)
  62. else {
  63. throw StatusCode(code,
  64. ent.map(EntityUtils.toString(_, HTTP.UTF_8)).getOrElse("")
  65. )
  66. }
  67. }
  68. /** Handle reponse entity in block when response code is 200 - 204 */
  69. def ok = (this when {code => (200 to 204) contains code}) _
  70. }
  71. /** Generally for the curried response function of Responder: >>, j$, <>, etc. */
  72. def apply[T](block: Http => T) = block(this)
  73. }
  74. /** Extension point for static request definitions. */
  75. class /(path: String) extends Request("/" + path) {
  76. def this(req: Request) = this(req.req.getURI.toString.substring(1))
  77. }
  78. /** Factory for requests from the root. */
  79. object / {
  80. def apply(path: String) = new Request("/" + path)
  81. }
  82. /** Wrapper to handle common requests, preconfigured as response wrapper for a
  83. * get request but defs return other method responders. */
  84. class Request(val req: HttpUriRequest) extends Responder {
  85. /** Start with GET by default. */
  86. def this(uri: String) = this(new HttpGet(uri))
  87. /** Append an element to this request's path */
  88. def / (path: String) = new Request(req.getURI + "/" + path)
  89. /** Put the given object.toString and return response wrapper. */
  90. def <<< (body: Any) = {
  91. val m = new HttpPut(req.getURI)
  92. m setEntity new StringEntity(body.toString, HTTP.UTF_8)
  93. HttpProtocolParams.setUseExpectContinue(m.getParams, false)
  94. new Request(m)
  95. }
  96. /** Put the given object.toString and return response wrapper. */
  97. def <<< (body: Array[byte], contentType: String) = {
  98. val m = new HttpPut(req.getURI)
  99. val e = new ByteArrayEntity(body)
  100. e setContentType(contentType)
  101. m setEntity e
  102. HttpProtocolParams.setUseExpectContinue(m.getParams, false)
  103. new Request(m)
  104. }
  105. /** Post the given key value sequence and return response wrapper. */
  106. def << (values: Map[String, Any]) = {
  107. val m = new HttpPost(req.getURI)
  108. m setEntity new UrlEncodedFormEntity(Http.map2ee(values), HTTP.UTF_8)
  109. new Request(m)
  110. }
  111. /** Post the given object as string and return response wrapper. */
  112. def << (body: Any) = {
  113. val m = new HttpPost(req.getURI)
  114. m setEntity new StringEntity(body.toString, HTTP.UTF_8)
  115. new Request(m)
  116. }
  117. import org.apache.http.message._
  118. def << (body: Any, contentType: String) = {
  119. val m = new HttpPost(req.getURI)
  120. val e = new StringEntity(body.toString)
  121. e setContentType(contentType)
  122. m setEntity e
  123. new Request(m)
  124. }
  125. /** Use <<? instead: the high precedence of ? is problematic. */
  126. @deprecated def ?< (values: Map[String, Any]) = <<? (values)
  127. /** Get with query parameters */
  128. def <<? (values: Map[String, Any]) = if(values.isEmpty) this else
  129. new Request(new HttpGet(req.getURI + Http ? (values)))
  130. /** HTTP Delete request. */
  131. def <--() = new Request(new HttpDelete(req.getURI))
  132. }
  133. trait Responder {
  134. val req: HttpUriRequest
  135. /** Execute and process response in block */
  136. def apply [T] (block: (Int, HttpResponse, Option[HttpEntity]) => T)(http: Http) = {
  137. http.headers foreach { case (k, v) => req.addHeader(k, v) }
  138. (http x req) (block)
  139. }
  140. /** Handle response and entity in block if OK. */
  141. def ok [T] (block: (HttpResponse, Option[HttpEntity]) => T)(http: Http) = {
  142. http.headers foreach { case (k, v) => req.addHeader(k, v) }
  143. (http x req) ok (block)
  144. }
  145. /** Handle response entity in block if OK. */
  146. def okee [T] (block: HttpEntity => T) = ok {
  147. case (_, Some(ent)) => block(ent)
  148. case (res, _) => error("response has no entity: " + res)
  149. } _
  150. /** Handle InputStream in block if OK. */
  151. def >> [T] (block: InputStream => T) = okee (ent => block(ent.getContent))
  152. /** Return response in String if OK. (Don't blow your heap, kids.) */
  153. def as_str = okee { EntityUtils.toString(_, HTTP.UTF_8) }
  154. /** Write to the given OutputStream. */
  155. def >>> [OS <: OutputStream](out: OS)(http: Http) = { okee { _.writeTo(out) } (http); out }
  156. /** Process response as XML document in block */
  157. def <> [T] (block: xml.NodeSeq => T) = >> { stm => block(xml.XML.load(stm)) }
  158. /** Process response as JsValue in block */
  159. def ># [T](block: json.Js.JsF[T]) = >> (stm => block(json.Js(stm)))
  160. /** Use ># instead: $ is forbidden. */
  161. @deprecated def $ [T](block: json.Js.JsF[T]) = >#(block)
  162. /** Ignore response body if OK. */
  163. def >| = ok ((r,e) => ()) _
  164. }
  165. class ConfiguredHttpClient extends DefaultHttpClient {
  166. override def createHttpParams = {
  167. val params = new BasicHttpParams
  168. HttpProtocolParams.setVersion(params, HttpVersion.HTTP_1_1)
  169. HttpProtocolParams.setContentCharset(params, HTTP.UTF_8)
  170. HttpProtocolParams.setUseExpectContinue(params, false)
  171. params
  172. }
  173. }
  174. /** May be used directly from any thread. */
  175. object Http extends Http(None, Nil, None) {
  176. import org.apache.http.conn.scheme.{Scheme,SchemeRegistry,PlainSocketFactory}
  177. import org.apache.http.conn.ssl.SSLSocketFactory
  178. import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager
  179. // import to support e.g. Http("http://example.com/" >>> System.out)
  180. implicit def str2req(str: String) = new Request(str)
  181. override lazy val client = new ConfiguredHttpClient {
  182. override def createClientConnectionManager() = {
  183. val registry = new SchemeRegistry()
  184. registry.register(new Scheme("http", PlainSocketFactory.getSocketFactory(), 80))
  185. registry.register(new Scheme("https", SSLSocketFactory.getSocketFactory(), 443))
  186. new ThreadSafeClientConnManager(getParams(), registry)
  187. }
  188. }
  189. // val log = net.lag.logging.Logger.get
  190. /** Convert repeating name value tuples to list of pairs for httpclient */
  191. def map2ee(values: Map[String, Any]) =
  192. new java.util.ArrayList[BasicNameValuePair](values.size) {
  193. values.foreach { case (k, v) => add(new BasicNameValuePair(k, v.toString)) }
  194. }
  195. /** Produce formatted query strings from a Map of parameters */
  196. def ?(values: Map[String, Any]) = if (values.isEmpty) "" else
  197. "?" + URLEncodedUtils.format(map2ee(values), HTTP.UTF_8)
  198. }