├── .gitignore ├── .gitmodules ├── .travis.yml ├── LICENSE.txt ├── README.markdown ├── core └── src │ └── main │ ├── ls │ ├── 0.8.6.json │ ├── 0.8.7.json │ ├── 0.8.8.json │ └── 0.8.9.json │ └── scala │ ├── callbacks.scala │ ├── executor.scala │ ├── handlers.scala │ ├── logging.scala │ ├── package.scala │ └── requests.scala ├── docs ├── 00.markdown ├── 01.markdown ├── 02.markdown ├── 03.markdown ├── 04.markdown ├── 05 │ ├── 00.markdown │ ├── a.markdown │ └── b.markdown ├── 06 │ ├── 00.markdown │ ├── a.markdown │ ├── b.markdown │ └── c.markdown ├── 07 │ ├── 00.markdown │ └── a.markdown ├── 08.markdown ├── 09.markdown ├── 99.markdown ├── dispatch.css └── template.properties ├── futures └── src │ └── main │ ├── ls │ ├── 0.8.6.json │ ├── 0.8.7.json │ ├── 0.8.8.json │ └── 0.8.9.json │ └── scala │ └── Futures.scala ├── http+json └── src │ ├── main │ ├── ls │ │ ├── 0.8.6.json │ │ ├── 0.8.7.json │ │ ├── 0.8.8.json │ │ └── 0.8.9.json │ └── scala │ │ └── JsHttp.scala │ └── test │ └── scala │ └── JsonSpec.scala ├── http-gae └── src │ └── main │ ├── ls │ ├── 0.8.6.json │ ├── 0.8.7.json │ ├── 0.8.8.json │ └── 0.8.9.json │ └── scala │ ├── AppEngineHttp.scala │ └── gae.scala ├── http └── src │ ├── main │ ├── ls │ │ ├── 0.8.6.json │ │ ├── 0.8.7.json │ │ ├── 0.8.8.json │ │ └── 0.8.9.json │ └── scala │ │ ├── ConfiguredHttpClient.scala │ │ ├── Http.scala │ │ ├── https.scala │ │ └── thread │ │ └── thread.scala │ └── test │ └── scala │ └── HttpSpec.scala ├── json └── src │ ├── main │ ├── ls │ │ ├── 0.8.6.json │ │ ├── 0.8.7.json │ │ ├── 0.8.8.json │ │ └── 0.8.9.json │ └── scala │ │ ├── Json.scala │ │ └── JsonExtractor.scala │ └── test │ ├── resources │ └── test.json │ └── scala │ └── JsonSpec.scala ├── jsoup └── src │ ├── main │ ├── ls │ │ ├── 0.8.7.json │ │ ├── 0.8.8.json │ │ └── 0.8.9.json │ └── scala │ │ └── JSoupHttp.scala │ └── test │ ├── resources │ ├── Human.html │ └── test.html │ └── scala │ └── JSoupSpec.scala ├── mime └── src │ └── main │ ├── ls │ ├── 0.8.6.json │ ├── 0.8.7.json │ ├── 0.8.8.json │ └── 0.8.9.json │ └── scala │ └── Mime.scala ├── nio └── src │ └── main │ ├── ls │ ├── 0.8.6.json │ ├── 0.8.7.json │ ├── 0.8.8.json │ └── 0.8.9.json │ └── scala │ └── nio.scala ├── notes ├── 0.7.0.markdown ├── 0.7.1.markdown ├── 0.7.2.markdown ├── 0.7.3.markdown ├── 0.7.4.markdown ├── 0.7.5.markdown ├── 0.7.6.markdown ├── 0.7.7.markdown ├── 0.7.8.markdown ├── 0.8.0.Beta1.markdown ├── 0.8.0.Beta3.markdown ├── 0.8.0.Beta4.markdown ├── 0.8.0.Beta5.markdown ├── 0.8.0.markdown ├── 0.8.1.markdown ├── 0.8.10.markdown ├── 0.8.2.markdown ├── 0.8.3.markdown ├── 0.8.4.markdown ├── 0.8.5.markdown ├── 0.8.6.markdown ├── 0.8.7.markdown ├── 0.8.8.markdown ├── 0.8.9.markdown └── about.markdown ├── oauth └── src │ ├── main │ ├── ls │ │ ├── 0.8.6.json │ │ ├── 0.8.7.json │ │ ├── 0.8.8.json │ │ └── 0.8.9.json │ └── scala │ │ ├── OAuth.scala │ │ ├── foursquare.scala │ │ └── twitter.scala │ └── test │ └── scala │ ├── OAuthSpec.scala │ └── foursquare.scala ├── project ├── build.properties ├── build.scala └── plugins.sbt ├── tagsoup └── src │ ├── main │ ├── ls │ │ ├── 0.8.7.json │ │ ├── 0.8.8.json │ │ └── 0.8.9.json │ └── scala │ │ └── TagSoupHttp.scala │ └── test │ ├── resources │ ├── Human.html │ └── test.html │ └── scala │ └── TagSoupSpec.scala └── temp /.gitignore: -------------------------------------------------------------------------------- 1 | .classpath 2 | .project 3 | .settings 4 | target 5 | scratch.scala 6 | lib_managed 7 | project/boot 8 | project/plugins/project 9 | src_managed 10 | *.test.properties 11 | .idea 12 | *.iml 13 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "databinder-dispatch/tasks"] 2 | path = databinder-dispatch/tasks 3 | url = technically.us:db-buildr-tasks 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: scala 2 | 3 | scala: 4 | - 2.9.0 5 | - 2.9.0-1 6 | - 2.9.1 7 | - 2.9.1-1 8 | - 2.9.2 9 | - 2.9.3 10 | - 2.10.4 11 | - 2.11.1 12 | -------------------------------------------------------------------------------- /README.markdown: -------------------------------------------------------------------------------- 1 | This is the classic version of Dispatch and is unmaintained. 2 | 3 | Find Dispatch Classic's documentation at http://dispatch-classic.databinder.net/ 4 | 5 | If you're looking for the modern Dispatch, check out https://github.com/dispatch/reboot 6 | -------------------------------------------------------------------------------- /core/src/main/ls/0.8.6.json: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "organization":"net.databinder", 4 | "name":"dispatch-core", 5 | "version":"0.8.6", 6 | "description":"Core interfaces, applied by dispatch-http and dispatch-nio executors", 7 | "site":"http://dispatch.databinder.net/", 8 | "tags":[], 9 | "docs":"", 10 | "licenses": [], 11 | "resolvers": ["http://scala-tools.org/repo-releases"], 12 | "dependencies": [{ 13 | "organization":"org.apache.httpcomponents", 14 | "name": "httpclient", 15 | "version": "4.1.2" 16 | }], 17 | "scalas": ["2.8.0","2.8.1","2.8.2","2.9.0","2.9.0-1","2.9.1"], 18 | "sbt": false 19 | } -------------------------------------------------------------------------------- /core/src/main/ls/0.8.7.json: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "organization":"net.databinder", 4 | "name":"dispatch-core", 5 | "version":"0.8.7", 6 | "description":"Core interfaces, applied by dispatch-http and dispatch-nio executors", 7 | "site":"http://dispatch.databinder.net/", 8 | "tags":[], 9 | "docs":"", 10 | "licenses": [], 11 | "resolvers": ["http://scala-tools.org/repo-releases"], 12 | "dependencies": [{ 13 | "organization":"org.apache.httpcomponents", 14 | "name": "httpclient", 15 | "version": "4.1.2" 16 | }], 17 | "scalas": ["2.8.0","2.8.1","2.8.2","2.9.0","2.9.0-1","2.9.1"], 18 | "sbt": false 19 | } -------------------------------------------------------------------------------- /core/src/main/ls/0.8.8.json: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "organization":"net.databinder", 4 | "name":"dispatch-core", 5 | "version":"0.8.8", 6 | "description":"Core interfaces, applied by dispatch-http and dispatch-nio executors", 7 | "site":"http://dispatch.databinder.net/", 8 | "tags":[], 9 | "docs":"", 10 | "licenses": [{ 11 | "name": "LGPL v3", 12 | "url": "http://www.gnu.org/licenses/lgpl.txt" 13 | }], 14 | "resolvers": ["http://scala-tools.org/repo-releases"], 15 | "dependencies": [{ 16 | "organization":"org.apache.httpcomponents", 17 | "name": "httpclient", 18 | "version": "4.1.3" 19 | }], 20 | "scalas": ["2.8.0","2.8.1","2.8.2","2.9.0","2.9.0-1","2.9.1"], 21 | "sbt": false 22 | } -------------------------------------------------------------------------------- /core/src/main/ls/0.8.9.json: -------------------------------------------------------------------------------- 1 | { 2 | "organization" : "net.databinder", 3 | "name" : "dispatch-core", 4 | "version" : "0.8.9", 5 | "description" : "Core interfaces, applied by dispatch-http and dispatch-nio executors", 6 | "site" : "http://dispatch.databinder.net/", 7 | "tags" : [ ], 8 | "docs" : "", 9 | "resolvers" : [ "https://oss.sonatype.org/content/repositories/releases" ], 10 | "dependencies" : [ { 11 | "organization" : "org.apache.httpcomponents", 12 | "name" : "httpclient", 13 | "version" : "4.1.3" 14 | } ], 15 | "scalas" : [ "2.8.0", "2.8.1", "2.8.2", "2.9.0", "2.9.0-1", "2.9.1", "2.9.1-1", "2.9.2" ], 16 | "licenses" : [ { 17 | "name" : "LGPL v3", 18 | "url" : "http://www.gnu.org/licenses/lgpl.txt" 19 | } ], 20 | "sbt" : false 21 | } -------------------------------------------------------------------------------- /core/src/main/scala/callbacks.scala: -------------------------------------------------------------------------------- 1 | package dispatch.classic 2 | 3 | import org.apache.http.HttpResponse 4 | import util.control.Exception._ 5 | 6 | object Callback { 7 | type Function = (HttpResponse, Array[Byte], Int) => Unit 8 | type Finish[T] = HttpResponse => T 9 | 10 | def apply[T](request: Request, 11 | function: Callback.Function, 12 | finish: Callback.Finish[T]): Callback[T] = 13 | Callback(request, function, finish, nothingCatcher) 14 | 15 | 16 | def apply(request: Request, function: Callback.Function): Callback[Unit] = 17 | Callback(request, function, { _ => () }) 18 | 19 | def strings[T](req: Request, block: (String => Unit)) = { 20 | @volatile var charset: Option[String] = None 21 | Callback( 22 | req, 23 | (res, bytes, len) => { 24 | charset orElse { 25 | charset = (for { 26 | ct <- res.getHeaders("Content-Type").headOption 27 | elem <- ct.getElements.headOption 28 | param <- Option(elem.getParameterByName("charset")) 29 | } yield param.getValue()) orElse Some(Request.factoryCharset) 30 | charset 31 | } map { cs => block(new String(bytes, 0, len, cs)) } 32 | } 33 | ) 34 | } 35 | 36 | /** Divide input up by given regex. Buffers across inputs so strings are 37 | * only split on the divider, and handles any leftovers in finish. Skips 38 | * empty strings. */ 39 | def stringsBy[T](divider: String) 40 | (req: Request, block: (String => Unit)) = { 41 | var buffer = "" 42 | strings( 43 | req, 44 | { string => 45 | val strings = (buffer + string).split(divider, -1) 46 | strings.take(strings.length - 1).filter { !_.isEmpty }.foreach(block) 47 | buffer = strings.last 48 | } 49 | ) ^> { res => 50 | if (!buffer.isEmpty) block(buffer) 51 | } 52 | } 53 | /** callback transformer for strings split on the newline character, newline removed */ 54 | def lines[T] = stringsBy[T]("[\n\r]+")_ 55 | } 56 | 57 | case class Callback[T](request: Request, 58 | function: Callback.Function, 59 | finish: Callback.Finish[T], 60 | listener: ExceptionListener) { 61 | def ^> [T](finish: Callback.Finish[T]) = copy(finish={ res => 62 | this.finish(res) 63 | finish(res) 64 | }) 65 | /** Set an exception listener */ 66 | def ^!(listener: ExceptionListener) = this.copy(listener = listener) 67 | } 68 | 69 | trait ImplicitCallbackVerbs { 70 | implicit def toCallbackVerbs(req: Request) = new CallbackVerbs(req) 71 | implicit def stringToCallbackVerbs(str: String) = new CallbackVerbs(new Request(str)) 72 | } 73 | 74 | class CallbackVerbs(subject: Request) { 75 | import Callback._ 76 | def ^[T](callback: Function) = Callback(subject, callback) 77 | def ^-[T](callback: String => Unit) = strings(subject, callback) 78 | /** strings split on the newline character, newline removed */ 79 | def ^--[T](callback: String => Unit) = lines(subject, callback) 80 | } 81 | -------------------------------------------------------------------------------- /core/src/main/scala/executor.scala: -------------------------------------------------------------------------------- 1 | package dispatch.classic 2 | 3 | import org.apache.http.{HttpHost,HttpRequest,HttpResponse,HttpEntity} 4 | import org.apache.http.message.AbstractHttpMessage 5 | import org.apache.http.util.EntityUtils 6 | import org.apache.http.client.methods._ 7 | 8 | /** Defines request execution and response status code behaviors. Implemented methods are finalized 9 | as any overrides would be lost when instantiating delegate executors, is in Threads#future. 10 | Delegates should chain to parent `pack` and `execute` implementations. */ 11 | trait HttpExecutor extends RequestLogging { 12 | /** Type of value returned from request execution */ 13 | type HttpPackage[T] 14 | /** Execute the request against an HttpClient */ 15 | def execute[T](host: HttpHost, 16 | creds: Option[Credentials], 17 | req: HttpRequestBase, 18 | block: HttpResponse => T, 19 | listener: ExceptionListener): HttpPackage[T] 20 | 21 | def executeWithCallback[T](host: HttpHost, credsopt: Option[Credentials], 22 | req: HttpRequestBase, block: Callback[T]): HttpPackage[T] 23 | 24 | @deprecated("Use x[T](hand: Handler[T]) instead. Construct a Handler if needed.", "0.8.0") 25 | final def x[T](req: Request)(block: Handler.F[T]): HttpPackage[T] = { 26 | x(Handler(req, block)) 27 | } 28 | /** Execute full request-response handler, response in package. */ 29 | final def x[T](hand: Handler[T]): HttpPackage[T] = { 30 | val req = hand.request 31 | val request = make_message(hand.request) 32 | log.info("%s %s", req.host.getHostName, request.getRequestLine) 33 | req.headers.reverse.foreach { 34 | case (key, value) => request.addHeader(key, value) 35 | } 36 | execute(req.host, req.creds, request, { res => 37 | val ent = res.getEntity match { 38 | case null => None 39 | case ent => Some(ent) 40 | } 41 | val result = hand.block(res.getStatusLine.getStatusCode, res, ent) 42 | consumeContent(ent) 43 | result 44 | }, hand.listener) 45 | } 46 | /** Allow executor to release any resources for an entity */ 47 | def consumeContent(entity: Option[HttpEntity]): Unit 48 | /** Apply Response Handler if reponse code returns true from chk. */ 49 | final def when[T](chk: Int => Boolean)(hand: Handler[T]) = 50 | x(hand.copy(block= { 51 | case (code, res, ent) if chk(code) => hand.block(code, res, ent) 52 | case (code, _, Some(ent)) => 53 | throw StatusCode(code, EntityUtils.toString(ent, hand.request.defaultCharset)) 54 | case (code, _, _) => 55 | throw StatusCode(code, "[no entity]") 56 | })) 57 | 58 | /** Apply handler block when response code is 200 - 204 */ 59 | final def apply[T](hand: Handler[T]): HttpPackage[T] = 60 | (this when {code => (200 to 204) contains code})(hand) 61 | 62 | def make_message(req: Request) = { 63 | req.method.toUpperCase match { 64 | case HttpGet.METHOD_NAME => new HttpGet(req.path) 65 | case HttpHead.METHOD_NAME => new HttpHead(req.path) 66 | case HttpDelete.METHOD_NAME => new HttpDelete(req.path) 67 | case HttpOptions.METHOD_NAME => new HttpOptions(req.path) 68 | case method => 69 | val message = method match { 70 | case HttpPost.METHOD_NAME => new HttpPost(req.path) 71 | case HttpPut.METHOD_NAME => new HttpPut(req.path) 72 | } 73 | req.body.foreach(message.setEntity) 74 | message 75 | } 76 | } 77 | 78 | /** Apply handler block when response code is 200 - 204 */ 79 | final def apply[T](callback: Callback[T]) = { 80 | val req = callback.request 81 | val request = make_message(req) 82 | log.info("%s %s", req.host.getHostName, request.getRequestLine) 83 | req.headers.reverse.foreach { 84 | case (key, value) => request.addHeader(key, value) 85 | } 86 | executeWithCallback(req.host, req.creds, request, callback) 87 | } 88 | private var isShutdown = false 89 | /** Release resources held by the executor. */ 90 | def shutdown() { 91 | shutdownClient(); 92 | isShutdown = true 93 | } 94 | protected def shutdownClient(): Unit 95 | /** Call shutdown if not already shutdown, issue warning */ 96 | override def finalize() { 97 | if (!isShutdown) { 98 | log.warn( 99 | "Shutting down garbage-collected HttpExecutor--" + 100 | "Call shutdown() explicitly to avoid resource leaks!" 101 | ) 102 | shutdown() 103 | } 104 | super.finalize() 105 | } 106 | } 107 | 108 | trait BlockingCallback { self: HttpExecutor => 109 | def executeWithCallback[T](host: HttpHost, 110 | credsopt: Option[Credentials], 111 | req: HttpRequestBase, 112 | callback: Callback[T]): HttpPackage[T] = 113 | execute(host, credsopt, req, { res => 114 | res.getEntity match { 115 | case null => callback.finish(res) 116 | case entity => 117 | val stm = entity.getContent 118 | val buf = new Array[Byte](8 * 1024) 119 | var count = 0 120 | while ({count = stm.read(buf); count} > -1) 121 | callback.function(res, buf, count) 122 | stm.close() 123 | callback.finish(res) 124 | } 125 | }, callback.listener) 126 | } 127 | 128 | case class StatusCode(code: Int, contents:String) 129 | extends Exception("Unexpected response code: " + code + "\n" + contents) 130 | 131 | /** Simple info and warn logger */ 132 | trait Logger { 133 | def info(msg: String, items: Any*) 134 | def warn(msg: String, items: Any*) 135 | } 136 | 137 | trait RequestLogging { 138 | lazy val log: Logger = make_logger 139 | 140 | /** Logger for this executor, logs to console. */ 141 | def make_logger = 142 | new Logger { 143 | def info(msg: String, items: Any*) { 144 | println("INF: [console logger] dispatch: " + 145 | msg.format(items: _*)) 146 | } 147 | def warn(msg: String, items: Any*) { 148 | println("WARN: [console logger] dispatch: " + 149 | msg.format(items: _*)) 150 | } 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /core/src/main/scala/handlers.scala: -------------------------------------------------------------------------------- 1 | package dispatch.classic 2 | 3 | import org.apache.http.{HttpResponse,HttpEntity} 4 | import org.apache.http.util.EntityUtils 5 | import java.util.zip.GZIPInputStream 6 | import java.io.{InputStream,OutputStream,InputStreamReader} 7 | import javax.xml.parsers.SAXParserFactory 8 | import scala.io.Source 9 | import util.control.Exception._ 10 | 11 | /** Request handler, contains request descriptor and a function to transform the result. */ 12 | case class Handler[T]( 13 | request: Request, 14 | block: Handler.F[T], 15 | listener: ExceptionListener 16 | ) { 17 | /** @return new Handler composing after with this Handler's block */ 18 | def ~> [R](after: T => R) = copy(block=(code, res, ent) => after(block(code,res,ent))) 19 | /** Set an exception listener */ 20 | def >!(listener: ExceptionListener) = this.copy(listener = listener) 21 | /** Create a new handler with block that receives all response parameters and 22 | this handler's block converted to parameterless function. */ 23 | def apply[R](next: (Int, HttpResponse, Option[HttpEntity], () => T) => R) = 24 | copy(block={(code, res, ent) => 25 | next(code, res, ent, () => block(code, res, ent)) 26 | }) 27 | } 28 | 29 | object Handler { 30 | type F[T] = (Int, HttpResponse, Option[HttpEntity]) => T 31 | /** Turns a simple entity handler in into a full response handler that fails if no entity */ 32 | def apply[T](req: Request, block: F[T]): Handler[T] = Handler( 33 | req, block, nothingCatcher) 34 | def apply[T](req: Request, block: HttpEntity => T): Handler[T] = 35 | Handler(req, { (code, res, ent) => ent match { 36 | case Some(ent) => block(ent) 37 | case None => sys.error(""" 38 | | Response has no HttpEntity: %s 39 | | If no response body is expected, use a handler such as 40 | | HandlerVerbs#>| that does not require one.""".stripMargin.format(res)) 41 | } } ) 42 | // retain factory to use with XML.load; its newInstance method is not thread-safe 43 | lazy val saxParserFactory = { 44 | val spf = SAXParserFactory.newInstance() 45 | spf.setNamespaceAware(false) 46 | spf 47 | } 48 | } 49 | 50 | trait ImplicitHandlerVerbs { 51 | implicit def toHandlerVerbs(req: Request) = new HandlerVerbs(req) 52 | implicit def stringToHandlerVerbs(str: String) = new HandlerVerbs(new Request(str)) 53 | } 54 | 55 | class HandlerVerbs(request: Request) { 56 | /** Handle InputStream in block, handle gzip if so encoded. Passes on any charset 57 | header value from response, otherwise the default charset. (See Request#>\) */ 58 | def >> [T] (block: (InputStream, String) => T) = Handler(request, { ent => 59 | val stm = (ent.getContent, ent.getContentEncoding) match { 60 | case (stm, null) => stm 61 | case (stm, enc) if enc.getValue == "gzip" => new GZIPInputStream(stm) 62 | case (stm, _) => stm 63 | } 64 | val charset = EntityUtils.getContentCharSet(ent) match { 65 | case null => request.defaultCharset 66 | case charset => charset 67 | } 68 | try { block(stm, charset) } 69 | finally { stm.close() } 70 | } ) 71 | /** Handle InputStream in block, handle gzip if so encoded. */ 72 | def >> [T] (block: InputStream => T): Handler[T] = >> { (stm, charset) => block(stm) } 73 | /** Handle response as a scala.io.Source, in a block. Note that Source may fail if the 74 | character set it receives (determined in >>) is incorrect. To process resources 75 | that have incorrect charset headers, use >> ((InputStream, String) => T). */ 76 | def >~ [T] (block: Source => T) = >> { (stm, charset) => 77 | block(Source.fromInputStream(stm, charset)) 78 | } 79 | /** Return response as a scala.io.Source. Charset note in >~ applies. */ 80 | def as_source = >~ { so => so } 81 | /** Handle some non-huge response body as a String, in a block. Charset note in >~ applies. */ 82 | def >- [T] (block: String => T) = >~ { so => block(so.mkString) } 83 | /** Return some non-huge response as a String. Charset note in >~ applies.*/ 84 | def as_str = >- { s => s } 85 | /** Handle response as a java.io.Reader */ 86 | def >>~ [T] (block: InputStreamReader => T) = >> { (stm, charset) => 87 | block(new InputStreamReader(stm, charset)) 88 | } 89 | /** Write to the given OutputStream. */ 90 | def >>> [OS <: OutputStream](out: OS) = Handler(request, { ent => ent.writeTo(out); out }) 91 | /** Process response as XML document in block */ 92 | def <> [T] (block: xml.Elem => T) = >>~ { reader => 93 | block(xml.XML.withSAXParser(Handler.saxParserFactory.newSAXParser).load(reader)) 94 | } 95 | /** Process header as Map in block. Map returns empty set for header 96 | * name misses. */ 97 | def >:> [T] (block: Map[String, Set[String]] => T) = { 98 | Handler(request, { (_, res, _) => 99 | val st = Map.empty[String, Set[String]].withDefaultValue(Set.empty) 100 | block((st /: res.getAllHeaders) { (m, h) => 101 | m + (h.getName -> (m(h.getName) + h.getValue)) 102 | }) 103 | }) 104 | } 105 | 106 | /** Process headers as a Map of strings to sequences of *lowercase* 107 | * strings, to facilitate case-insensetive header lookup. */ 108 | def headers_> [T] (block: Map[String, Seq[String]] => T) = { 109 | Handler(request, { (_, res, _) => 110 | val st = Map.empty[String, Seq[String]].withDefaultValue(Seq.empty) 111 | block((st /: res.getAllHeaders) { (m, h) => 112 | val key = h.getName.toLowerCase 113 | m + (key -> (m(key) :+ h.getValue)) 114 | }) 115 | }) 116 | } 117 | 118 | /** Combination header and request chaining verb. Headers are 119 | * converted to lowercase for case insensitive access. 120 | */ 121 | def >:+ [T] (block: (Map[String, Seq[String]], Request) => 122 | Handler[T]) = 123 | >+> { req => 124 | req headers_> { hs => block(hs, req) } 125 | } 126 | 127 | /** Ignore response body. */ 128 | def >| = Handler(request, (code, res, ent) => ()) 129 | 130 | /** Split into two request handlers, return results of each in tuple. */ 131 | def >+ [A, B] (block: Request => (Handler[A], Handler[B])) = { 132 | Handler(request, { (code, res, opt_ent) => 133 | val (a, b) = block(request) 134 | (a.block(code, res, opt_ent), b.block(code,res,opt_ent)) 135 | } ) 136 | } 137 | /** Chain two request handlers. First handler returns a second, which may use 138 | values obtained by the first. Both are run on the same request. */ 139 | def >+> [T] (block: Request => Handler[Handler[T]]) = { 140 | Handler( request, { (code, res, opt_ent) => 141 | (block(request)).block(code, res, opt_ent).block(code, res, opt_ent) 142 | } ) 143 | } 144 | } 145 | 146 | 147 | trait ImplicitXhtmlHandlerVerbs { 148 | implicit def toXhtmlHandlerVerbs(req: Request) = new XhtmlHandlerVerbs(req) 149 | implicit def stringToXhtmlHandlerVerbs(str: String) = new XhtmlHandlerVerbs(new Request(str)) 150 | } 151 | 152 | /** Import this object's methods to get the verb, or see the 153 | * jsoup and tagsoup modules. */ 154 | object XhtmlParsing extends ImplicitXhtmlHandlerVerbs 155 | 156 | class XhtmlHandlerVerbs(request: Request) { 157 | /** Process response as XHTML document in block, more lenient than <> 158 | * but still not great. See the jsoup and tagsoup modules for more 159 | * capable html parsers. */ 160 | def [T] (block: xml.NodeSeq => T) = request >~ { src => 161 | block(xml.parsing.XhtmlParser(src)) 162 | } 163 | } 164 | 165 | -------------------------------------------------------------------------------- /core/src/main/scala/logging.scala: -------------------------------------------------------------------------------- 1 | package dispatch.classic 2 | 3 | /** Mix in to Http if you want JDK logging */ 4 | trait JdkLogging extends HttpExecutor { 5 | override def make_logger = new dispatch.classic.Logger { 6 | val jdklog = java.util.logging.Logger.getLogger("dispatch") 7 | def info(msg: String, items: Any*) { 8 | jdklog.info(msg.format(items: _*)) 9 | } 10 | def warn(msg: String, items: Any*) { 11 | jdklog.warning(msg.format(items: _*)) 12 | } 13 | } 14 | } 15 | 16 | /** 17 | * Mix in to Http if you want no logging from Dispatch. 18 | * Note that HttpClient logs separately: 19 | * http://hc.apache.org/httpcomponents-client/logging.html 20 | */ 21 | trait NoLogging extends HttpExecutor { 22 | override def make_logger = new dispatch.classic.Logger { 23 | def info(msg: String, items: Any*) { } 24 | def warn(msg: String, items: Any*) { } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /core/src/main/scala/package.scala: -------------------------------------------------------------------------------- 1 | package dispatch 2 | package object classic { 3 | /** Exception listener is called in addition to an HttpExecutor's 4 | * regular exception handling (throwing/logging/ignoring it). */ 5 | type ExceptionListener = util.control.Exception.Catcher[Unit] 6 | } 7 | -------------------------------------------------------------------------------- /docs/00.markdown: -------------------------------------------------------------------------------- 1 | Dispatch 2 | ======== 3 | 4 | *Dispatch* is a library for conducting HTTP interaction. You tell it 5 | what to do in pure and uninhibited Scala, and Dispatch carries out your 6 | orders using its trusty [HttpClient][hc] backend. 7 | 8 | > [Version $version$ Release Notes]($version_link$) 9 | 10 | 11 | [hc]: http://hc.apache.org/httpcomponents-client/ 12 | 13 | 14 | -------------------------------------------------------------------------------- /docs/01.markdown: -------------------------------------------------------------------------------- 1 | Try Dispatch 2 | ------------ 3 | 4 | ### Getting a Console 5 | 6 | The simplest way to try Dispatch is to clone the Twine example 7 | application from github. 8 | 9 | git clone https://github.com/n8han/dispatch-twine.git 10 | 11 | > If you don't have git available, you can download and extract the 12 | project's current 13 | [zip archive](https://github.com/n8han/dispatch-twine/zipball/master). 14 | 15 | [zip]: https://github.com/n8han/dispatch-twine/zipball/master 16 | 17 | Twine is built with sbt 0.10. If you haven't yet [setup sbt][sbt], now 18 | is a good time to do that. Once you have `sbt` on your executable search 19 | path, you can enter its interactive console for the Twine project. 20 | 21 | [sbt]: https://github.com/harrah/xsbt/wiki/Setup 22 | 23 | cd dispatch-twine 24 | sbt 25 | 26 | In sbt's console, call the `console` task. 27 | 28 | console 29 | 30 | On the first run this task will download Dispatch dependencies and 31 | compile the Twine app. After that, you should see a message welcoming 32 | you to the console and a friendly `scala>` prompt. 33 | 34 | ### Your First Request 35 | 36 | First, import the main Dispatch classes and objects into scope. 37 | 38 | ```scala 39 | import dispatch._ 40 | ``` 41 | Then, we'll need an HTTP *executor* to carry out our requests. 42 | 43 | ```scala 44 | val h = new Http 45 | ``` 46 | Requests are described with `dispatch.Request` objects, and one way to 47 | construct them is with a URL. 48 | 49 | ```scala 50 | val req = url("http://www.scala-lang.org/") 51 | ``` 52 | This works because you've imported an object called `url` from the 53 | `dispatch` package, and the object is itself a function that creates 54 | request objects. 55 | 56 | Now, we have an executor and a request. Dispatch needs to know how to 57 | handle the request. 58 | 59 | ```scala 60 | val handler = req >>> System.out 61 | ``` 62 | With this complete request-response handler, Dispatch can execute the 63 | request. Assuming you have a network connection, that is. 64 | 65 | ```scala 66 | h(handler) 67 | ``` 68 | And that was the source of the Scala home page. Of course, we don't 69 | usually assign all these component parts to values unless we need to 70 | reuse them. Typically, the handler above would be written in one line. 71 | 72 | ```scala 73 | h(url("http://www.scala-lang.org/") >>> System.out) 74 | ``` 75 | 76 | If you can't wait to see more Dispatch *verbs* like `>>>`, here's a 77 | [colorful cheat sheet][table]. 78 | 79 | [gh]: https://github.com/n8han/dispatch-twine#readme 80 | [table]: http://www.flotsam.nl/dispatch-periodic-table.html 81 | -------------------------------------------------------------------------------- /docs/02.markdown: -------------------------------------------------------------------------------- 1 | Choose an Executor 2 | ------------------ 3 | 4 | Dispatch supports arbitrarily many types of HTTP *executors*--you could 5 | make your own, and you wouldn't be the first to do so. Executors carry 6 | out your request *handlers* and *callbacks*. 7 | 8 | Because executors are defined separately from HTTP interaction, 9 | you can model HTTP interaction without demanding a particular 10 | implementation. For example, the same interface to a web API can be 11 | used with an NIO executor and a Google App Engine executor. 12 | 13 | Dispatch includes several executors, defined in various modules and 14 | packages. These executors are all called "Http", so application code 15 | typically imports only the base `dispatch` package and refers to 16 | executors by their subpackage name, as seen below. 17 | 18 | > To follow along with the examples below, use a console of the Twine 19 | example app described in the preceding page. 20 | 21 | ### Current Thread Blocking (dispatch-http) 22 | 23 | The traditional and most common Dispatch executor is located in the 24 | base `dispatch` package. It performs its operations in the current 25 | program thread, allowing it to return whatever value the handler 26 | builds. If you request a string response body with `as_str`, the 27 | executor's return type is `String`. 28 | 29 | ```scala 30 | import dispatch._ 31 | val h = new Http 32 | h(url("http://www.scala-lang.org/") as_str) 33 | ``` 34 | You may notice in the Scala console that the last expression above 35 | reports a return type of `HttpPackage`. This wrapping type is required 36 | to generalize the executor model, but in this executor it is defined 37 | as the wrapped type itself. It can be used any place where the wrapped 38 | type is expected. 39 | 40 | ```scala 41 | val s: String = h(url("http://www.scala-lang.org/") as_str) 42 | ``` 43 | An important caveat of the executor defined above is that it is not 44 | thread-safe. You should not share references to it across 45 | threads. Instead, Dispatch applications commonly define a function in 46 | some convenient object that returns a newly constructed executor. This 47 | also allows you replace it with a customized executor, later. 48 | 49 | ```scala 50 | def http = new Http 51 | ``` 52 | Another option is to use a thread-safe executor, which maintains a 53 | shared connection pool and may (or may not) provide better 54 | performance. Dispatch provides a trait for this behavior: 55 | 56 | ```scala 57 | val http = new Http with thread.Safety 58 | ``` 59 | The `Http` singleton object is an executor constructed with this 60 | trait, so you can use it from any thread. 61 | 62 | ```scala 63 | Http(url("http://www.scala-lang.org/") as_str) 64 | ``` 65 | ### Background Thread Blocking (dispatch-http) 66 | 67 | Another executor that does not depart from the traditional model is 68 | the background thread executor. 69 | 70 | ```scala 71 | import dispatch._ 72 | val h = new thread.Http 73 | val f = h(url("http://www.scala-lang.org/") as_str) 74 | ``` 75 | This executor application evaluates to a *future* for the string value 76 | to be constructed by the handler. A future in Dispatch is a function. 77 | 78 | ```scala 79 | type Future[T] = Function0[T] { 80 | def isSet: Boolean 81 | } 82 | ``` 83 | When applied, the future waits until its underlying value is 84 | available, and `isSet` reports this availability. These are useful 85 | when handling a batch of requests, for example. 86 | 87 | ```scala 88 | val urls: Traversable[String] = < some urls I need to get > 89 | val futures = urls.map { u => h(url(u) as_str) } 90 | 91 | < do something else that's slow... > 92 | 93 | val (ready, working) = futures.partition { _.isSet } 94 | consume(ready.map { _() }) // mapped to Traversable[String] 95 | ``` 96 | ### Native I/O (dispatch-nio) 97 | 98 | This executor uses Java NIO and does not block a current or background 99 | thread while carrying out requests. You can have *n* active requests 100 | without tying up *n* threads or waiting for available threads from a 101 | pool. 102 | 103 | ```scala 104 | import dispatch._ 105 | val h = new nio.Http 106 | val f = h(url("http://www.scala-lang.org/") as_str) 107 | ``` 108 | Like the background thread executor, the NIO executor returns a 109 | Dispatch future; you can operate on collection as in the previous 110 | example. One caveat of this executor is that it must be shutdown. 111 | 112 | ```scala 113 | h.shutdown() 114 | ``` 115 | If you're using this executor in a console and forget to shut it down, 116 | the console may not close when asked. 117 | 118 | ### Google App Engine (dispatch-gae) 119 | 120 | This executor operates on the current thread, like `dispatch.Http`, 121 | but it is specially modified to work with Google App Engine which 122 | does not permit the direct use of sockets. 123 | 124 | ```scala 125 | import dispatch._ 126 | val h = new gae.Http 127 | val s = h(url("http://www.scala-lang.org/") as_str) 128 | ``` 129 | Note that this executor is not on Twine's classpath and should only be 130 | used with App Engine. 131 | 132 | ### Means of Execution 133 | 134 | The examples above all use `apply` which will execute the handler if 135 | the resulting HTTP statusCode is in the range 200 to 204. This range 136 | allows the standard handlers to fail-fast in situations where the response 137 | wouldn't be handled. If you need finer control please review the docs 138 | for the `when` and `x` methods on `trait HttpExecutor`. 139 | -------------------------------------------------------------------------------- /docs/03.markdown: -------------------------------------------------------------------------------- 1 | Project Setup 2 | ------------- 3 | 4 | ### Modules and Artifacts 5 | 6 | Dispatch is divided into a number of modules so that client 7 | applications need only depend on the parts of Dispatch they use. Some 8 | of the modules depend on other modules, and dependencies managers will 9 | add these transitive dependencies automatically. 10 | 11 | Each module is [cross-built][sbt] against several versions of Scala 12 | and [published to the scala-tools][st] repository with the 13 | organization-id "net.databinder". The modules have the Scala version 14 | they are built against appended. For Scala $scala$, the full artifact 15 | names are as follows: 16 | 17 | * dispatch-http_$scala$ 18 | * dispatch-nio_$scala$ 19 | * dispatch-core_$scala$ 20 | * dispatch-gae_$scala$ 21 | * dispatch-futures_$scala$ 22 | * dispatch-mime_$scala$ 23 | * dispatch-json_$scala$ 24 | * dispatch-http-json_$scala$ 25 | * dispatch-oauth_$scala$ 26 | 27 | [dn]: http://databinder.net/repo/ 28 | [st]: http://scala-tools.org/repo-releases/net/databinder/ 29 | [sbt]: http://code.google.com/p/simple-build-tool/wiki/CrossBuild 30 | 31 | ### Source Dependencies 32 | 33 | Because each web API and third party Scala library has its own release 34 | timeline, some modules are not part of the main Dispatch build and 35 | distribution. Instead, they are external modules hosted on github 36 | and tagged for release as needed. 37 | 38 | Since these modules are build from source, binary compatibility with 39 | Scala and Dispatch versions is not an issue. More importantly, they 40 | can be trivially initted, forked, and used by anyone. 41 | 42 | If a module is lacking an API method that you need right now, you can 43 | fork it, fix it, tag it, and push it without waiting on anyone 44 | else. At that point it's "released" under your name. You should also 45 | fork this documentation to add your integration module to the list: 46 | 47 | * [dispatch/dispatch-lift-json](https://github.com/dispatch/dispatch-lift-json) -- The lift-json parsing and serialization library 48 | * [n8han/dispatch-aws-s3](https://github.com/n8han/dispatch-aws-s3) -- Amazon S3 signing and bucket interaction 49 | * [n8han/dispatch-couch](https://github.com/n8han/dispatch-couch) -- Basic CouchDB integration module, rather outdated 50 | * [n8han/dispatch-google-clientlogin](https://github.com/n8han/dispatch-google-clientlogin) -- Google's ClientLogin 51 | * [n8han/dispatch-meetup](https://github.com/n8han/dispatch-meetup) -- Meetup Groups and Everywhere APIs 52 | * [n8han/dispatch-twitter](https://github.com/n8han/dispatch-twitter) -- Basic Twitter integration for Dispatch, pls fork! 53 | 54 | ### Build Tools 55 | 56 | #### Configuring sbt Projects 57 | 58 | When using sbt with Scala **binary dependencies**, it's best to have the 59 | Scala version [automatically appended][sbt] so it will always match 60 | your project's. In a sbt 0.11.x `build.sbt`: 61 | 62 | ```scala 63 | libraryDependencies ++= Seq( 64 | "net.databinder" %% "dispatch-http" % "$version$" 65 | ) 66 | ``` 67 | 68 | To use **source dependencies** with sbt, create a project build such 69 | as `project/build.scala`. This is an example using dispatch-lift-json: 70 | 71 | ```scala 72 | import sbt._ 73 | object MyApp extends Build 74 | { 75 | lazy val root = 76 | Project("root", file(".")) dependsOn(dispatchLiftJson) 77 | lazy val dispatchLiftJson = 78 | uri("git://github.com/dispatch/dispatch-lift-json#0.1.0") 79 | } 80 | ``` 81 | 82 | The [Twine example application][twine] uses both source and 83 | binary Dispatch dependencies in its sbt project. 84 | 85 | [twine]: /Try+Dispatch.html 86 | 87 | #### Maven 88 | 89 | With Maven, you can depend on Dispatch's binary modules by specifying 90 | the full artifact id with Scala version: 91 | 92 | ```xml 93 | 94 | net.databinder 95 | dispatch-http_$scala$ 96 | $version$ 97 | 98 | ``` 99 | 100 | To use source dependencies with Maven, your best bet is to check out 101 | the project as a submodule. 102 | -------------------------------------------------------------------------------- /docs/04.markdown: -------------------------------------------------------------------------------- 1 | Community 2 | --------- 3 | 4 | Dispatch (including this documentation) lives [on GitHub][gh], and 5 | many of its features and bug fixes originate in forks of the 6 | project there. Dispatch's documentation is generated via 7 | [Pamflet][pamflet]. We also have [a mailing list][list]. 8 | 9 | [gh]: https://github.com/dispatch/dispatch 10 | [pamflet]: http://pamflet.databinder.net/Pamflet.html 11 | [list]: https://groups.google.com/forum/?hl=en#!forum/dispatch-scala 12 | -------------------------------------------------------------------------------- /docs/05/00.markdown: -------------------------------------------------------------------------------- 1 | A Simple Request 2 | ---------------- 3 | 4 | This section explores basic request definition and response handling 5 | with Dispatch. You'll need a Scala [console with Dispatch][try] on the 6 | classpath to follow along. 7 | 8 | [try]: Try+Dispatch.html 9 | -------------------------------------------------------------------------------- /docs/05/a.markdown: -------------------------------------------------------------------------------- 1 | URLs and Paths 2 | -------------- 3 | 4 | Hosts are defined with the `:/` verb. It should remind you (a 5 | little) of the characters that come before hosts in URLs. 6 | 7 | ```scala 8 | import dispatch._ 9 | val sl = :/("www.scala-lang.org") 10 | ``` 11 | 12 | This value `sl` is a request to the root path of the Scala home 13 | domain. What if we want some sub-path of it? There's an obvious verb 14 | for that: `/` 15 | 16 | ```scala 17 | val learnScala = sl / "node" / "1305" 18 | ``` 19 | 20 | Now we have a reference to a page (the "Learning Scala" page 21 | actually), and `sl` is unchanged. 22 | 23 | Request definitions such as these are immutable; by appending a path 24 | to one you are creating a new request object, similar to string 25 | concatenation in Java. If we don't need to use the host for different 26 | requests, we would probably have defined this all at once: 27 | 28 | ```scala 29 | val learnScala2 = :/("www.scala-lang.org") / "node" / "1305" 30 | ``` 31 | 32 | But—can't you define this with its actual URL? Of course! 33 | 34 | ```scala 35 | val learnScala3 = url("http://www.scala-lang.org/node/1305") 36 | ``` 37 | 38 | The `url` verb is also imported from the `dispatch` package and it 39 | produces a request object like the others. It is most useful when 40 | dealing with URLs stored externally, or discovered at runtime. For 41 | fixed requests in your application code, building up from host and 42 | path components is usually preferred as it lends itself to reuse. 43 | -------------------------------------------------------------------------------- /docs/05/b.markdown: -------------------------------------------------------------------------------- 1 | Response Bodies 2 | --------------- 3 | 4 | Request definitions, by themselves, are not enough information for 5 | Dispatch to do its job. The response that the server returns may have 6 | no body at all—or it could contain too much data to hold in memory. 7 | 8 | Before Dispatch makes a request, then, you must tell it how to handle 9 | the server's response. For this we have *handlers*. Handlers are 10 | created from request definitions using handler verbs. Taking up the 11 | request defined on the last page, we could simply ignore the response. 12 | 13 | ```scala 14 | val ignore = learnScala >| 15 | ``` 16 | 17 | This would be very unusual for a GET request in particular, which 18 | shouldn't have any side effects. (It's also not speaking well of our 19 | commitment to learning Scala.) 20 | 21 | To use a handler, you pass it to a Dispatch [executor][executor]. 22 | 23 | [executor]: Choose+an+Executor.html 24 | 25 | ```scala 26 | Http(ignore) 27 | ``` 28 | 29 | Okay, but let's say you actually want to do something with the 30 | response. Assuming it's text of a reasonable size, you could retrieve 31 | it as a string: 32 | 33 | ```scala 34 | Http(learnScala >- { str => 35 | str.length 36 | }) 37 | ``` 38 | 39 | Like most handlers this one takes a function from the transformed 40 | response body to any type. Here the function is of type `String => 41 | Int`. It merely returns the string length of the response. 42 | 43 | How about trying something more interesting with the body, like 44 | extracting the page's title? 45 | 46 | ```scala 47 | import XhtmlParsing._ 48 | Http(learnScala { nodes => 49 | (nodes \\\\ "title").text 50 | }) 51 | ``` 52 | 53 | The `` handler processes the response as XHTML and passes a 54 | `scala.xml.NodeSeq` to the supplied function. The `\\\\` projection 55 | function finds the `` node, and `text` gives its contents. 56 | 57 | > Note: The parser imported here is very brittle. See the next section 58 | for alternatives that support real-world HTML. 59 | -------------------------------------------------------------------------------- /docs/06/00.markdown: -------------------------------------------------------------------------------- 1 | Parsing Responses 2 | ----------------- 3 | 4 | While you can easily wield Dispatch's stream and reader handlers to 5 | bind to any parser you like, several binding modules are available to 6 | make it even simpler. 7 | -------------------------------------------------------------------------------- /docs/06/a.markdown: -------------------------------------------------------------------------------- 1 | Lift JSON 2 | --------- 3 | 4 | Lift-JSON is developed and supported by the [Lift][lift] 5 | framework. These are some basic instructions for using it with 6 | Dispatch. 7 | 8 | [lift]: http://liftweb.net/ 9 | 10 | ### Example Usage 11 | 12 | Using Lift-JSON is very similar to using Dispatch's internal JSON 13 | representation. As always, start by importing the main Dispatch 14 | methods. 15 | 16 | ```scala 17 | import dispatch._ 18 | ``` 19 | But instead of importing the internal JSON methods, import Dispatch's 20 | interface to lift-json: 21 | 22 | ```scala 23 | import dispatch.liftjson.Js._ 24 | ``` 25 | 26 | We will now be able to use the `>#` operator in our handlers. For 27 | example, if we wanted to search for scala podcasts, we might use 28 | something like 29 | 30 | ```scala 31 | import net.liftweb.json.JsonAST._ 32 | 33 | val http = new Http() 34 | val u = url("http://gpodder.net/search.json") <<? Map("q" -> "scala") 35 | http(u ># { json => 36 | (json \ "title" children) flatMap( _ match { 37 | case JField("title", JString(d)) => Some(d) 38 | case JString(d) => Some(d) 39 | case _ => None 40 | }) 41 | }) 42 | ``` 43 | 44 | This script starts by importing lift's JSON values (for pattern 45 | matching) and then creates the http executor and the gpodder url. We 46 | then execute with a handler which accepts a lift JValue and returns a 47 | list of the podcast titles. Simple, no? 48 | -------------------------------------------------------------------------------- /docs/06/b.markdown: -------------------------------------------------------------------------------- 1 | TagSoup 2 | ------- 3 | 4 | [TagSoup][tagsoup] is a SAX-compliant parser, that parses HTML instead 5 | of well formed XML. TagSoup is very resilient, allowing you to load HTML 6 | as found in the wild, to be processed as a scala NodeSeq. 7 | 8 | [tagsoup]: http://ccil.org/~cowan/XML/tagsoup/ 9 | 10 | ### Example Usage 11 | 12 | To process the response with TagSoup you first have to make the handler 13 | verbs available. This can be done in several ways. 14 | 15 | ```scala 16 | import dispatch.tagsoup.TagSoupHttp._ 17 | ``` 18 | Now we can use the operators `</>`, `tagsouped` and `as_tagsouped` in 19 | our handlers. If we want to find the title of a HTML page it can look 20 | something like this. 21 | 22 | ```scala 23 | import dispatch.tagsoup.TagSoupHttp._ 24 | 25 | val title = Http(:/("example.org") </> { ns => 26 | (ns \\ "title").text 27 | }) 28 | ``` 29 | TagSoup let's you work with the HTML as a `scala.xml.NodeSeq` and as a 30 | convenience you can use `as_tagsouped` to retrieve it. 31 | 32 | ```scala 33 | val ns = Http(:/("example.com") as_tagsouped) 34 | ``` 35 | 36 | -------------------------------------------------------------------------------- /docs/06/c.markdown: -------------------------------------------------------------------------------- 1 | JSoup 2 | ----- 3 | 4 | [JSoup][jsoup] is a library for working with real-world HTML. It parses HTML 5 | and provides a convenient API for extracting and manipulating data. 6 | JSoup is similar to TagSoup as they both lets you work with real-world HTML, 7 | but JSoup provide more functionality for working with the extracted result. 8 | 9 | [jsoup]: http://jsoup.org/ 10 | 11 | ### Example Usage 12 | 13 | To use JSoup you have to make it handler verbs available. This is easiest done 14 | by importing them from JSoupHttp. 15 | 16 | ```scala 17 | import dispatch.jsoup.JSoupHttp._ 18 | ``` 19 | Now we can use the operators `</>`, `jsouped`, `as_jsouped` and `as_jsoupedNodeSeq` 20 | in our handlers. As a start we can extract the title from a HTML page. 21 | 22 | ```scala 23 | import dispatch.jsoup.JSoupHttp._ 24 | 25 | val title = Http(:/("example.org") </> { doc => 26 | doc.title 27 | }) 28 | ``` 29 | JSoup parse the HTML to a DOM like structure `org.jsoup.nodes.Document`. There 30 | is a rich set of find and search methods returning `org.jsoup.nodes.Elements`. 31 | Elements implements java List so they are very easy to use with scala 32 | collections, just make the usual `import scala.collection.JavaConversions._`. 33 | 34 | So, to extract all links from a page and put them in a list as absolute paths 35 | looks like this: 36 | 37 | ```scala 38 | import dispatch.jsoup.JSoupHttp._ 39 | import scala.collection.JavaConversions._ 40 | 41 | val request = :/("example.org") / "test.html" 42 | val list = Http(request </> { doc => 43 | doc.select("a[href]").map(_.attr("abs:href")) 44 | }) 45 | ``` 46 | 47 | JSoup is a great api for processing HTML, and scala makes it even better. 48 | To learn more of it's capabilities take a look in the [JSoup Cookbook][cookbook]. 49 | 50 | [cookbook]: http://jsoup.org/cookbook/ 51 | -------------------------------------------------------------------------------- /docs/07/00.markdown: -------------------------------------------------------------------------------- 1 | Putting It All Together 2 | ----------------------- 3 | 4 | Once you've mastered the basic request and response verbs, you can 5 | compose almost any kind of HTTP interaction. 6 | -------------------------------------------------------------------------------- /docs/07/a.markdown: -------------------------------------------------------------------------------- 1 | Two Handlers Are Better Than One 2 | -------------------------------- 3 | 4 | Dispatch has so many handy response handlers--but what if you want to 5 | use two with the same request? You *can't* do that with a stream of 6 | the response body, since this can only be consumed once, but for 7 | **headers** it's essential and straightforward. 8 | 9 | ### Header Chaining 10 | 11 | The recommended header-handling verb `>:+` provides a header `Map` and 12 | a request object to chain a second handler for the body. 13 | 14 | ```scala 15 | http(:/("dispatch.databinder.net") >:+ { (headers, req) => 16 | headers("content-type").filter { 17 | _.contains("text/html") 18 | }.headOption.map { _ => 19 | req </> { nodes => (nodes \\\\ "h1").text } 20 | }.getOrElse { 21 | req >> { _ => "unknown content type" } 22 | } 23 | }) 24 | ``` 25 | 26 | > To facilitate case-insensitive handling, the keys in the header 27 | > `Map` are uniformly lowercase. 28 | 29 | The function passed to `>:+` should produce a second handler for the 30 | body of the response. In this case, it produces different response 31 | handlers depending on the headers present. 32 | -------------------------------------------------------------------------------- /docs/08.markdown: -------------------------------------------------------------------------------- 1 | Dispatch in Android 2 | ------------------- 3 | 4 | Many people are writing Android applications in Scala and want to do 5 | their HTTP communications with Dispatch. Unfortunately, this is not as 6 | easy as it should be. 7 | 8 | ### Android's HttpClient 9 | 10 | The Android platform ships with the Apache HttpClient library. It is 11 | packaged in the same JAR as the "Java" standard library. From an application 12 | perspective it is just there, always. 13 | 14 | When packaging for Android, you should be sure not to include any 15 | HttpClient jars; the classloader will always find the stock client's 16 | classes first. This allows apps that require HTTP communication—nearly 17 | all apps—to save space. 18 | 19 | The downside of shipping libraries in the standard classpath is that 20 | the environment becomes rigidly coupled to them. If you want to take 21 | advantage of a new feature in a library, too bad. You'd have to wait 22 | until all of your users upgrade their operating system to the new 23 | version—if it's ever made available for their devices. 24 | 25 | But wait, it gets worse! Updates to the HttpClient libraries in 26 | Android stalled. There was a procedural issue with binary 27 | compatibility; see [this thread][thread] for the gory details. The 28 | short of it is, we can rule out just waiting for those parties to fix 29 | the situation. 30 | 31 | [thread]: http://old.nabble.com/HttpClient-in-Android-td27915358.html 32 | 33 | ### Dealing with It 34 | 35 | Versions of Dispatch through 0.7.x can work with Android's random old 36 | version of HttpClient just fine. Version 0.8.x however depends on newer 37 | major versions of the HttpComponents libraries and certain interfaces 38 | that are not compatible with the code shipped with Android. 39 | 40 | It is *possible* to include the newer HttpComponents artifacts in 41 | the application package and to trick the classloader into finding 42 | them. How? You can obfuscate the class and package names with 43 | ProGuard, eliminating the namespace collisions. 44 | 45 | But really: isn't programming hard enough without some bytecode 46 | analyzer changing the names of all your symbols prior to runtime? For 47 | most of us, yes. That's why it's recommended here that you **use 48 | Dispatch 0.7.x with Android**. That works without any tricks and it's 49 | still a lot nicer than having to code to the client directly. 50 | 51 | ### What's in Store 52 | 53 | This unfortunate problem underscores the need for Dispatch to be 54 | independent of any single backend. The primary advancement of 0.8 was 55 | to support HttpComponet's async NIO client in addition to its standard 56 | blocking client, but that (like the rest of 0.8) is unusable on 57 | Android without some serious hackery. 58 | 59 | Dispatch 0.9 will therefore need to support entirely different 60 | backends such as Netty or one of the higher level async clients built 61 | on Netty. And main reason to choose a backend like that for Andorid 62 | apps will be, ironically enough, its absence in the platform 63 | distribution. 64 | -------------------------------------------------------------------------------- /docs/09.markdown: -------------------------------------------------------------------------------- 1 | API Reference 2 | ------------- 3 | 4 | ### Dispatch Periodic Table 5 | 6 | [This clever and attractive table][table] lists Dispatch's request and 7 | response verbs all in one place. 8 | 9 | [table]: http://www.flotsam.nl/dispatch-periodic-table.html 10 | 11 | ### Scaladocs 12 | 13 | [Generated documentation][scaladoc] is available for the latest 14 | version of Dispatch. Since most Dispatch interaction is defined by 15 | *verbs* that are added implicitly to requests, the interesting 16 | classes are `RequestVerbs`, `CallbackVerbs`, and so on. 17 | 18 | [scaladoc]: http://databinder.net/dispatch-doc/ 19 | 20 | 21 | [sxr]: http://github.com/harrah/browse/tree/master 22 | [doc]: /dispatch-doc/ 23 | -------------------------------------------------------------------------------- /docs/99.markdown: -------------------------------------------------------------------------------- 1 | Who's Using Dispatch? 2 | --------------------- 3 | 4 | These are some projects and companies that have used Dispatch. Are 5 | *you* using Dispatch? 6 | [Fork this page](https://github.com/n8han/Databinder-Dispatch/blob/master/docs/99.markdown) 7 | on github. 8 | 9 | #### Novus 10 | 11 | Dispatch forms the foundation of an in-house SOAP client at [Novus](https://www.novus.com/) together with [scalaxb](http://scalaxb.org/). Thanks to these tools our standard response to "Can you do SOAP?" is "Bring it on!" instead of "Ew, what's this on my shoe?!" 12 | 13 | #### MLB.com 14 | 15 | In a past life, [maxaf](https://github.com/maxaf) wrote an internal baseball stats REST API client that was used to feed data into some very grueling PDF generation code. Dispatch was used to cleanly produce HTTP requests & parse responses without hurting engineer morale. 16 | 17 | #### conscript 18 | 19 | [conscript](https://github.com/n8han/conscript) uses Dispatch to fetch 20 | program descriptors from github. 21 | 22 | #### giter8 23 | 24 | [giter8](https://github.com/n8han/giter8) uses Dispatch to fetch 25 | templates from github. 26 | 27 | #### kiln2rally 28 | 29 | [kiln2rally](https://github.com/eed3si9n/kiln2rally) uses Dispatch along with [lift-json](https://github.com/lift/lift/tree/master/framework/lift-base/lift-json/) to hit Rally's awesome [web API](https://rally1.rallydev.com/slm/doc/webservice/index.jsp). 30 | 31 | #### posterous-sbt 32 | 33 | The [posterous-sbt](https://github.com/n8han/posterous-sbt) plugin 34 | uses Dispatch to publish project release notes. 35 | 36 | #### Unfiltered 37 | 38 | [Unfiltered](http://unfiltered.databinder.net/) uses Dispatch in its 39 | system tests. 40 | -------------------------------------------------------------------------------- /docs/dispatch.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family:"Helvetica Neue", Arial, Helvetica, sans-serif; 3 | } 4 | body p, h1, h2, h3, h4, h5, h6, blockquote, a, a.page, li { 5 | color: #444; 6 | } -------------------------------------------------------------------------------- /docs/template.properties: -------------------------------------------------------------------------------- 1 | version=0.8.10 2 | version_link=http://notes.implicit.ly/post/38089150571/dispatch-classic-0-8-9 3 | scala=2.10.2 4 | github=dispatch/dispatch 5 | -------------------------------------------------------------------------------- /futures/src/main/ls/0.8.6.json: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "organization":"net.databinder", 4 | "name":"dispatch-futures", 5 | "version":"0.8.6", 6 | "description":"Common interface to Java and Scala futures", 7 | "site":"http://dispatch.databinder.net/", 8 | "tags":[], 9 | "docs":"", 10 | "licenses": [], 11 | "resolvers": ["http://scala-tools.org/repo-releases"], 12 | "dependencies": [], 13 | "scalas": ["2.8.0","2.8.1","2.8.2","2.9.0","2.9.0-1","2.9.1"], 14 | "sbt": false 15 | } -------------------------------------------------------------------------------- /futures/src/main/ls/0.8.7.json: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "organization":"net.databinder", 4 | "name":"dispatch-futures", 5 | "version":"0.8.7", 6 | "description":"Common interface to Java and Scala futures", 7 | "site":"http://dispatch.databinder.net/", 8 | "tags":[], 9 | "docs":"", 10 | "licenses": [], 11 | "resolvers": ["http://scala-tools.org/repo-releases"], 12 | "dependencies": [], 13 | "scalas": ["2.8.0","2.8.1","2.8.2","2.9.0","2.9.0-1","2.9.1"], 14 | "sbt": false 15 | } -------------------------------------------------------------------------------- /futures/src/main/ls/0.8.8.json: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "organization":"net.databinder", 4 | "name":"dispatch-futures", 5 | "version":"0.8.8", 6 | "description":"Common interface to Java and Scala futures", 7 | "site":"http://dispatch.databinder.net/", 8 | "tags":[], 9 | "docs":"", 10 | "licenses": [{ 11 | "name": "LGPL v3", 12 | "url": "http://www.gnu.org/licenses/lgpl.txt" 13 | }], 14 | "resolvers": ["http://scala-tools.org/repo-releases"], 15 | "dependencies": [], 16 | "scalas": ["2.8.0","2.8.1","2.8.2","2.9.0","2.9.0-1","2.9.1"], 17 | "sbt": false 18 | } -------------------------------------------------------------------------------- /futures/src/main/ls/0.8.9.json: -------------------------------------------------------------------------------- 1 | { 2 | "organization" : "net.databinder", 3 | "name" : "dispatch-futures", 4 | "version" : "0.8.9", 5 | "description" : "Common interface to Java and Scala futures", 6 | "site" : "http://dispatch.databinder.net/", 7 | "tags" : [ ], 8 | "docs" : "", 9 | "resolvers" : [ "https://oss.sonatype.org/content/repositories/releases" ], 10 | "dependencies" : [ ], 11 | "scalas" : [ "2.8.0", "2.8.1", "2.8.2", "2.9.0", "2.9.0-1", "2.9.1", "2.9.1-1", "2.9.2" ], 12 | "licenses" : [ { 13 | "name" : "LGPL v3", 14 | "url" : "http://www.gnu.org/licenses/lgpl.txt" 15 | } ], 16 | "sbt" : false 17 | } -------------------------------------------------------------------------------- /futures/src/main/scala/Futures.scala: -------------------------------------------------------------------------------- 1 | package dispatch.classic.futures 2 | 3 | import java.util.concurrent.{Executors,Callable,ExecutorService} 4 | 5 | object Futures extends AvailableFutures { 6 | /** Structural type coinciding with scala.actors.Future */ 7 | type Future[T] = Function0[T] { 8 | def isSet: Boolean 9 | } 10 | } 11 | object DefaultFuture extends JucFuture { 12 | lazy val futureExecutor = Executors.newCachedThreadPool 13 | } 14 | trait AvailableFutures { 15 | /** @return values of futures that have completed their processing */ 16 | def available[T](fs: Iterable[Futures.Future[T]]) = fs filter { _.isSet } map { _() } toList 17 | } 18 | /** Interface to futures functionality */ 19 | trait Futures { 20 | def future[T](result: => T): Futures.Future[T] 21 | } 22 | /** A java.util.concurrent Future accessor, executor undefined */ 23 | trait JucFuture extends Futures { 24 | def future[T](result: => T) = new WrappedJucFuture( 25 | futureExecutor.submit(new Callable[T]{ 26 | def call = result 27 | }) 28 | ) 29 | /** Implement to customize the java.util.concurrent Executor, defaults to Executors.newCachedThreadPool */ 30 | val futureExecutor: ExecutorService 31 | } 32 | /** Wraps java.util.concurrent.Future */ 33 | class WrappedJucFuture[T](delegate: java.util.concurrent.Future[T]) extends (() => T) { 34 | def isSet = delegate.isDone 35 | def apply() = delegate.get() 36 | } 37 | 38 | /** Future accessor using a scala.actors future */ 39 | object ActorsFuture extends Futures { 40 | def future[T](result: => T) = scala.actors.Futures.future(result) 41 | } 42 | 43 | trait StoppableFuture[T] extends (() => T) { 44 | def isSet: Boolean 45 | def stop() 46 | } 47 | -------------------------------------------------------------------------------- /http+json/src/main/ls/0.8.6.json: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "organization":"net.databinder", 4 | "name":"dispatch-http-json", 5 | "version":"0.8.6", 6 | "description":"Adds JSON handler verbs to Dispatch", 7 | "site":"http://dispatch.databinder.net/", 8 | "tags":[], 9 | "docs":"", 10 | "licenses": [], 11 | "resolvers": ["http://scala-tools.org/repo-releases"], 12 | "dependencies": [{ 13 | "organization":"org.apache.httpcomponents", 14 | "name": "httpclient", 15 | "version": "4.1.2" 16 | }], 17 | "scalas": ["2.8.0","2.8.1","2.8.2","2.9.0","2.9.0-1","2.9.1"], 18 | "sbt": false 19 | } -------------------------------------------------------------------------------- /http+json/src/main/ls/0.8.7.json: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "organization":"net.databinder", 4 | "name":"dispatch-http-json", 5 | "version":"0.8.7", 6 | "description":"Adds JSON handler verbs to Dispatch", 7 | "site":"http://dispatch.databinder.net/", 8 | "tags":[], 9 | "docs":"", 10 | "licenses": [], 11 | "resolvers": ["http://scala-tools.org/repo-releases"], 12 | "dependencies": [{ 13 | "organization":"org.apache.httpcomponents", 14 | "name": "httpclient", 15 | "version": "4.1.2" 16 | }], 17 | "scalas": ["2.8.0","2.8.1","2.8.2","2.9.0","2.9.0-1","2.9.1"], 18 | "sbt": false 19 | } -------------------------------------------------------------------------------- /http+json/src/main/ls/0.8.8.json: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "organization":"net.databinder", 4 | "name":"dispatch-http-json", 5 | "version":"0.8.8", 6 | "description":"Adds JSON handler verbs to Dispatch", 7 | "site":"http://dispatch.databinder.net/", 8 | "tags":[], 9 | "docs":"", 10 | "licenses": [{ 11 | "name": "LGPL v3", 12 | "url": "http://www.gnu.org/licenses/lgpl.txt" 13 | }], 14 | "resolvers": ["http://scala-tools.org/repo-releases"], 15 | "dependencies": [{ 16 | "organization":"org.apache.httpcomponents", 17 | "name": "httpclient", 18 | "version": "4.1.3" 19 | }], 20 | "scalas": ["2.8.0","2.8.1","2.8.2","2.9.0","2.9.0-1","2.9.1"], 21 | "sbt": false 22 | } -------------------------------------------------------------------------------- /http+json/src/main/ls/0.8.9.json: -------------------------------------------------------------------------------- 1 | { 2 | "organization" : "net.databinder", 3 | "name" : "dispatch-http-json", 4 | "version" : "0.8.9", 5 | "description" : "Adds JSON handler verbs to Dispatch", 6 | "site" : "http://dispatch.databinder.net/", 7 | "tags" : [ ], 8 | "docs" : "", 9 | "resolvers" : [ "https://oss.sonatype.org/content/repositories/releases" ], 10 | "dependencies" : [ { 11 | "organization" : "org.apache.httpcomponents", 12 | "name" : "httpclient", 13 | "version" : "4.1.3" 14 | } ], 15 | "scalas" : [ "2.8.0", "2.8.1", "2.8.2", "2.9.0", "2.9.0-1", "2.9.1", "2.9.1-1", "2.9.2" ], 16 | "licenses" : [ { 17 | "name" : "LGPL v3", 18 | "url" : "http://www.gnu.org/licenses/lgpl.txt" 19 | } ], 20 | "sbt" : false 21 | } -------------------------------------------------------------------------------- /http+json/src/main/scala/JsHttp.scala: -------------------------------------------------------------------------------- 1 | package dispatch.classic.json 2 | import dispatch.classic._ 3 | 4 | trait ImplicitJsHandlers { 5 | /** Add JSON-processing method ># to dispatch.Request */ 6 | implicit def handlerToJsHandlers(r: HandlerVerbs) = new JsHandlers(r) 7 | implicit def requestToJsHandlers(r: Request) = new JsHandlers(r) 8 | implicit def stringToJsHandlers(r: String) = new JsHandlers(new Request(r)) 9 | } 10 | 11 | trait JsHttp extends ImplicitJsHandlers with Js 12 | 13 | object JsHttp extends JsHttp 14 | 15 | class JsHandlers(subject: HandlerVerbs) { 16 | /** Process response as JsValue in block */ 17 | def ># [T](block: json.Js.JsF[T]) = subject >> { (stm, charset) => 18 | block(json.Js(stm, charset)) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /http+json/src/test/scala/JsonSpec.scala: -------------------------------------------------------------------------------- 1 | import org.specs2.mutable.Specification 2 | 3 | object JsonSpec extends Specification { 4 | import dispatch.classic.json._ 5 | import JsHttp._ 6 | 7 | val js = Js(""" { "a": {"a": "a string", "b": {"pi": 3.14159265 } }, "b": [1,2,3] } """) 8 | val expected_map = Map( 9 | JsString('a) -> JsObject(Map( 10 | JsString('a) -> JsString("a string"), 11 | JsString('b) -> JsObject(Map( 12 | JsString('pi) -> JsNumber(BigDecimal("3.14159265"))) 13 | ) 14 | )), 15 | JsString('b) -> JsArray(List(JsNumber(1), JsNumber(2), JsNumber(3))) 16 | ) 17 | val js_list = Js("[1,2,3]") 18 | val expected_list = List(JsNumber(1), JsNumber(2), JsNumber(3)) 19 | 20 | /** mock ># of Http#Response */ 21 | object res { def ># [T](f: JsF[T]) = f(js) } 22 | 23 | "Parsed Json" should { 24 | "equal expected map" in { 25 | js.self must_== expected_map 26 | } 27 | "equal expected list" in { 28 | js_list.self must_== expected_list 29 | } 30 | "equal itself serilized and reparsed" in { 31 | js must_== JsValue.fromString(JsValue.toJson(js)) 32 | } 33 | } 34 | "Nested extractor object" should { 35 | object TestExtractor extends Js { 36 | val a = new Obj('a) { 37 | val a = ('a ? str) 38 | val b = new Obj('b) { 39 | val pi = 'pi ? num 40 | } 41 | } 42 | val b = 'b ? (list ! num) 43 | } 44 | "match against top level object" in { 45 | val TestExtractor.a(a) = js 46 | a must_== expected_map(JsString('a)) 47 | } 48 | "match against second level string" in { 49 | val TestExtractor.a.a(a) = js 50 | a must_== "a string" 51 | } 52 | "match against third level number" in { 53 | val TestExtractor.a.b.pi(p) = js 54 | p.toString must_== "3.14159265" 55 | } 56 | "match against a numeric list" in { 57 | val TestExtractor.b(b) = js 58 | b must_== List(1,2,3) 59 | } 60 | "replace second level string" in { 61 | res ># (TestExtractor.a.a << "land, ho") must_== (Js( 62 | """ { "a": {"a": "land, ho", "b": {"pi": 3.14159265 } }, "b": [1,2,3] } """ 63 | )) 64 | } 65 | } 66 | "Flat extractors" should { 67 | val a = 'a ? obj 68 | val aa = 'a ? str 69 | val b = 'b ? obj 70 | val pi = 'pi ? num 71 | val l = 'b ? list 72 | "extract a top level object" in { 73 | val a(a0) = js 74 | a0 must_== expected_map(JsString('a)) 75 | } 76 | "deeply extract a third level number" in { 77 | val a(b(pi(pi0))) = js 78 | pi0.toString must_== "3.14159265" 79 | } 80 | "match against an unextracted list" in { 81 | val l(l0) = js 82 | l0 must_== List(JsValue(1), JsValue(2), JsValue(3)) 83 | } 84 | val num_list = list ! num 85 | "match for an unenclosed Json list" in { 86 | val num_list(l0) = js_list 87 | l0 must_== List(1,2,3) 88 | } 89 | "pattern-match correct elements" in { 90 | (js match { 91 | case b(b0) => b0 92 | case a(a0) => a0 93 | }) must_== expected_map(JsString('a)) 94 | } 95 | "awkwardly replace second level string" in { 96 | val a(a0) = js 97 | res ># (a << (aa << "barnacles, ahoy")(a0)) must_== (Js( 98 | """ { "a": {"a": "barnacles, ahoy", "b": {"pi": 3.14159265 } }, "b": [1,2,3] } """ 99 | )) 100 | } 101 | } 102 | "Function extractor" should { 103 | "extract a top level object" in { 104 | res ># ('a ! obj) must_== expected_map(JsString('a)) 105 | } 106 | "extract a tuple of top level objects" in { 107 | res ># %('a ! obj, 'b ! list, 'b ! list) must_== 108 | (expected_map(JsString('a)), expected_list, expected_list) 109 | } 110 | "extract a second level string" in { 111 | res ># { ('a ! obj) andThen ('a ! str) } must_== "a string" 112 | } 113 | "extract a third level number" in { 114 | (res ># { ('a ! obj) andThen ('b ! obj) andThen ('pi ! num) }).toString must_== "3.14159265" 115 | } 116 | "work with map" in { 117 | List(js, js, js).map ('b ! (list ! num)) must_== (1 to 3).map{ _ => List(1,2,3) }.toList 118 | } 119 | def fun_l[T](ext: JsF[T]) = ext(js_list) 120 | "extract unenclosed Json list" in { 121 | fun_l(list ! num) must_== List(1,2,3) 122 | } 123 | } 124 | "assertion inserting" should { 125 | "replace second level string" in { 126 | res ># ('a << ('a << "barnacles, ahoy")) must_== (Js( 127 | """ { "a": {"a": "barnacles, ahoy", "b": {"pi": 3.14159265 } }, "b": [1,2,3] } """ 128 | )) 129 | } 130 | "replace a second level object with a string" in { 131 | res ># ('a << ('b << "bonzai!")) must_== (Js( 132 | """ { "a": {"a": "a string", "b": "bonzai!" } , "b": [1,2,3] } """ 133 | )) 134 | } 135 | } 136 | } 137 | 138 | -------------------------------------------------------------------------------- /http-gae/src/main/ls/0.8.6.json: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "organization":"net.databinder", 4 | "name":"dispatch-gae", 5 | "version":"0.8.6", 6 | "description":"Executor with a modified Apache HttpClient for Google App Engine", 7 | "site":"http://dispatch.databinder.net/", 8 | "tags":[], 9 | "docs":"", 10 | "licenses": [], 11 | "resolvers": ["http://scala-tools.org/repo-releases"], 12 | "dependencies": [{ 13 | "organization":"org.apache.httpcomponents", 14 | "name": "httpclient", 15 | "version": "4.1.2" 16 | },{ 17 | "organization":"com.google.appengine", 18 | "name": "appengine-api-1.0-sdk", 19 | "version": "1.5.5" 20 | }], 21 | "scalas": ["2.8.0","2.8.1","2.8.2","2.9.0","2.9.0-1","2.9.1"], 22 | "sbt": false 23 | } -------------------------------------------------------------------------------- /http-gae/src/main/ls/0.8.7.json: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "organization":"net.databinder", 4 | "name":"dispatch-gae", 5 | "version":"0.8.7", 6 | "description":"Executor with a modified Apache HttpClient for Google App Engine", 7 | "site":"http://dispatch.databinder.net/", 8 | "tags":[], 9 | "docs":"", 10 | "licenses": [], 11 | "resolvers": ["http://scala-tools.org/repo-releases"], 12 | "dependencies": [{ 13 | "organization":"org.apache.httpcomponents", 14 | "name": "httpclient", 15 | "version": "4.1.2" 16 | },{ 17 | "organization":"com.google.appengine", 18 | "name": "appengine-api-1.0-sdk", 19 | "version": "1.5.5" 20 | }], 21 | "scalas": ["2.8.0","2.8.1","2.8.2","2.9.0","2.9.0-1","2.9.1"], 22 | "sbt": false 23 | } -------------------------------------------------------------------------------- /http-gae/src/main/ls/0.8.8.json: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "organization":"net.databinder", 4 | "name":"dispatch-gae", 5 | "version":"0.8.8", 6 | "description":"Executor with a modified Apache HttpClient for Google App Engine", 7 | "site":"http://dispatch.databinder.net/", 8 | "tags":[], 9 | "docs":"", 10 | "licenses": [{ 11 | "name": "LGPL v3", 12 | "url": "http://www.gnu.org/licenses/lgpl.txt" 13 | }], 14 | "resolvers": ["http://scala-tools.org/repo-releases"], 15 | "dependencies": [{ 16 | "organization":"org.apache.httpcomponents", 17 | "name": "httpclient", 18 | "version": "4.1.3" 19 | },{ 20 | "organization":"com.google.appengine", 21 | "name": "appengine-api-1.0-sdk", 22 | "version": "1.5.5" 23 | }], 24 | "scalas": ["2.8.0","2.8.1","2.8.2","2.9.0","2.9.0-1","2.9.1"], 25 | "sbt": false 26 | } -------------------------------------------------------------------------------- /http-gae/src/main/ls/0.8.9.json: -------------------------------------------------------------------------------- 1 | { 2 | "organization" : "net.databinder", 3 | "name" : "dispatch-gae", 4 | "version" : "0.8.9", 5 | "description" : "Executor with a modified Apache HttpClient for Google App Engine", 6 | "site" : "http://dispatch.databinder.net/", 7 | "tags" : [ ], 8 | "docs" : "", 9 | "resolvers" : [ "https://oss.sonatype.org/content/repositories/releases" ], 10 | "dependencies" : [ { 11 | "organization" : "org.apache.httpcomponents", 12 | "name" : "httpclient", 13 | "version" : "4.1.3" 14 | }, { 15 | "organization" : "com.google.appengine", 16 | "name" : "appengine-api-1.0-sdk", 17 | "version" : "1.5.5" 18 | } ], 19 | "scalas" : [ "2.8.0", "2.8.1", "2.8.2", "2.9.0", "2.9.0-1", "2.9.1", "2.9.1-1", "2.9.2" ], 20 | "licenses" : [ { 21 | "name" : "LGPL v3", 22 | "url" : "http://www.gnu.org/licenses/lgpl.txt" 23 | } ], 24 | "sbt" : false 25 | } -------------------------------------------------------------------------------- /http-gae/src/main/scala/AppEngineHttp.scala: -------------------------------------------------------------------------------- 1 | package dispatch.classic.gae 2 | 3 | import org.apache.http.params.HttpParams 4 | import org.apache.http.conn.ClientConnectionManager 5 | 6 | class AppEngineConfiguredClient( 7 | credentials: dispatch.classic.Http.CurrentCredentials 8 | ) extends dispatch.classic.ConfiguredHttpClient(credentials) { 9 | /** @return GAEConnectionManager for non-socket based connections */ 10 | override def createClientConnectionManager = GAEConnectionManager 11 | /** No need for proxy support on app engine. */ 12 | override protected def configureProxy(params: HttpParams) = params 13 | } 14 | 15 | object Http extends Http 16 | 17 | class Http extends dispatch.classic.Http { 18 | override def make_client = new AppEngineConfiguredClient(credentials) 19 | } 20 | -------------------------------------------------------------------------------- /http-gae/src/main/scala/gae.scala: -------------------------------------------------------------------------------- 1 | package dispatch.classic.gae 2 | 3 | import java.net._ 4 | import java.util.concurrent.TimeUnit 5 | import org.apache.http.conn._ 6 | import org.apache.http.params._ 7 | import org.apache.http.conn.routing.HttpRoute 8 | import org.apache.http.conn.scheme._ 9 | 10 | object GAEConnectionManager extends GAEConnectionManager 11 | 12 | // adapted from this Java impl: http://esxx.blogspot.com/2009/06/using-apaches-httpclient-on-google-app.html 13 | 14 | class GAEConnectionManager extends ClientConnectionManager { 15 | val no_socket_factory = new SchemeSocketFactory { 16 | def connectSocket(sock: Socket, remote:InetSocketAddress, local: InetSocketAddress, params: HttpParams): Socket = null 17 | def createSocket(params: HttpParams): Socket = null 18 | def isSecure(s: Socket): Boolean = false 19 | } 20 | 21 | protected val schemeRegistry = new SchemeRegistry 22 | schemeRegistry.register(new Scheme("http", 80, no_socket_factory)) 23 | schemeRegistry.register(new Scheme("https", 443, no_socket_factory)) 24 | 25 | override def getSchemeRegistry: SchemeRegistry = schemeRegistry 26 | 27 | override def requestConnection(route: HttpRoute, state: AnyRef): ClientConnectionRequest = { 28 | val mgr = this 29 | new ClientConnectionRequest { 30 | def abortRequest = {} 31 | def getConnection(timeout: Long, tunit: TimeUnit): ManagedClientConnection = { 32 | new GAEClientConnection(mgr, route, state) 33 | } 34 | } 35 | } 36 | 37 | override def releaseConnection(conn: ManagedClientConnection, validDuration: Long, timeUnit: TimeUnit) = {} 38 | override def closeIdleConnections(idletime: Long, tunit: TimeUnit) = {} 39 | override def closeExpiredConnections = {} 40 | override def shutdown = {} 41 | } 42 | 43 | import java.io._ 44 | import java.net._ 45 | import java.util.concurrent.TimeUnit 46 | import org.apache.http._ 47 | import org.apache.http.conn._ 48 | import org.apache.http.conn.routing.HttpRoute 49 | import org.apache.http.entity.ByteArrayEntity 50 | import org.apache.http.message.BasicHttpResponse 51 | import org.apache.http.params._ 52 | import org.apache.http.protocol._ 53 | 54 | import com.google.appengine.api.urlfetch._ 55 | 56 | object GAEClientConnection { 57 | private var urlFS: URLFetchService = URLFetchServiceFactory.getURLFetchService 58 | } 59 | 60 | class GAEClientConnection(var cm: ClientConnectionManager, var route: HttpRoute, var state: AnyRef, var closed: Boolean) extends ManagedClientConnection { 61 | import GAEClientConnection._ 62 | 63 | def this(cm: ClientConnectionManager, route: HttpRoute, state: AnyRef) = 64 | this(cm, route, state, true) 65 | 66 | private var request: HTTPRequest = _ 67 | private var response: HTTPResponse = _ 68 | private var reusable: Boolean = _ 69 | 70 | override def isSecure: Boolean = route.isSecure 71 | override def getRoute: HttpRoute = route 72 | override def getSSLSession: javax.net.ssl.SSLSession = null 73 | 74 | override def open(route: HttpRoute, context: HttpContext, params: HttpParams) = { 75 | close 76 | this.route = route 77 | } 78 | 79 | override def tunnelTarget(secure: Boolean, params: HttpParams) = throw new IOException("tunnelTarget not supported") 80 | override def tunnelProxy(next: HttpHost, secure: Boolean, params: HttpParams) = throw new IOException("tunnelProxy not supported") 81 | override def layerProtocol(context: HttpContext, params: HttpParams) = throw new IOException("layerProtocol not supported") 82 | override def markReusable = reusable = true 83 | override def unmarkReusable = reusable = false 84 | override def isMarkedReusable: Boolean = reusable 85 | override def setState(state: AnyRef) = this.state = state 86 | override def getState: AnyRef = state 87 | override def setIdleDuration(duration: Long, unit: TimeUnit) = {} 88 | override def isResponseAvailable(timeout: Int): Boolean = response != null 89 | 90 | override def sendRequestHeader(request: HttpRequest) = { 91 | try { 92 | val host = route.getTargetHost 93 | val port = if (host.getPort() == -1) "" else ":" + host.getPort() 94 | val uri = new URI(host.getSchemeName 95 | + "://" 96 | + host.getHostName 97 | + port 98 | + request.getRequestLine.getUri) 99 | 100 | this.request = new HTTPRequest(uri.toURL, 101 | HTTPMethod.valueOf(request.getRequestLine.getMethod), 102 | FetchOptions.Builder.disallowTruncate.doNotFollowRedirects) 103 | } 104 | catch { 105 | case ex: URISyntaxException => 106 | throw new IOException("Malformed request URI: " + ex.getMessage) 107 | case ex: IllegalArgumentException => 108 | throw new IOException("Unsupported HTTP method: " + ex.getMessage) 109 | } 110 | 111 | for (h <- request.getAllHeaders) 112 | this.request.addHeader(new HTTPHeader(h.getName, h.getValue)) 113 | } 114 | 115 | override def sendRequestEntity(request: HttpEntityEnclosingRequest ) = { 116 | val baos = new ByteArrayOutputStream 117 | if (request.getEntity != null) 118 | request.getEntity.writeTo(baos) 119 | this.request.setPayload(baos.toByteArray) 120 | } 121 | 122 | override def receiveResponseHeader: HttpResponse = { 123 | if (this.response == null) { 124 | flush 125 | } 126 | 127 | val response: HttpResponse = new BasicHttpResponse(new ProtocolVersion("HTTP", 1, 1), 128 | this.response.getResponseCode, 129 | null) 130 | 131 | val i = this.response.getHeaders.iterator 132 | while (i.hasNext) { 133 | val h = i.next 134 | response.addHeader(h.getName, h.getValue) 135 | } 136 | 137 | response 138 | } 139 | 140 | override def receiveResponseEntity(response: HttpResponse) = { 141 | if (this.response == null) 142 | throw new IOException("receiveResponseEntity called on closed connection") 143 | 144 | val bae = new ByteArrayEntity(this.response.getContent) 145 | bae.setContentType(response.getFirstHeader("Content-Type")) 146 | response.setEntity(bae) 147 | 148 | //response = null 149 | } 150 | 151 | override def flush = { 152 | if (request != null) { 153 | response = urlFS.fetch(request) 154 | request = null 155 | } 156 | else { 157 | response = null 158 | } 159 | } 160 | 161 | override def close { 162 | request = null 163 | response = null 164 | closed = true 165 | } 166 | 167 | override def isOpen: Boolean = (request != null) || (response != null) 168 | override def isStale: Boolean = !isOpen && !closed 169 | 170 | override def setSocketTimeout(timeout: Int) = {} 171 | override def getSocketTimeout: Int = -1 172 | 173 | override def shutdown = close 174 | override def getMetrics: HttpConnectionMetrics = null 175 | 176 | override def getLocalAddress: InetAddress = null 177 | override def getLocalPort: Int = 0 178 | override def getRemoteAddress: InetAddress = null 179 | override def getRemotePort: Int = { 180 | val host: HttpHost = route.getTargetHost 181 | cm.getSchemeRegistry.getScheme(host).resolvePort(host.getPort) 182 | } 183 | 184 | override def releaseConnection = cm.releaseConnection(this, java.lang.Long.MAX_VALUE, TimeUnit.MILLISECONDS) 185 | 186 | override def abortConnection = { 187 | unmarkReusable 188 | shutdown 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /http/src/main/ls/0.8.6.json: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "organization":"net.databinder", 4 | "name":"dispatch-http", 5 | "version":"0.8.6", 6 | "description":"Standard HTTP executor, uses Apache DefaultHttpClient", 7 | "site":"http://dispatch.databinder.net/", 8 | "tags":[], 9 | "docs":"", 10 | "licenses": [], 11 | "resolvers": ["http://scala-tools.org/repo-releases"], 12 | "dependencies": [{ 13 | "organization":"org.apache.httpcomponents", 14 | "name": "httpclient", 15 | "version": "4.1.2" 16 | }], 17 | "scalas": ["2.8.0","2.8.1","2.8.2","2.9.0","2.9.0-1","2.9.1"], 18 | "sbt": false 19 | } -------------------------------------------------------------------------------- /http/src/main/ls/0.8.7.json: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "organization":"net.databinder", 4 | "name":"dispatch-http", 5 | "version":"0.8.7", 6 | "description":"Standard HTTP executor, uses Apache DefaultHttpClient", 7 | "site":"http://dispatch.databinder.net/", 8 | "tags":[], 9 | "docs":"", 10 | "licenses": [], 11 | "resolvers": ["http://scala-tools.org/repo-releases"], 12 | "dependencies": [{ 13 | "organization":"org.apache.httpcomponents", 14 | "name": "httpclient", 15 | "version": "4.1.2" 16 | }], 17 | "scalas": ["2.8.0","2.8.1","2.8.2","2.9.0","2.9.0-1","2.9.1"], 18 | "sbt": false 19 | } -------------------------------------------------------------------------------- /http/src/main/ls/0.8.8.json: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "organization":"net.databinder", 4 | "name":"dispatch-http", 5 | "version":"0.8.8", 6 | "description":"Standard HTTP executor, uses Apache DefaultHttpClient", 7 | "site":"http://dispatch.databinder.net/", 8 | "tags":[], 9 | "docs":"", 10 | "licenses": [{ 11 | "name": "LGPL v3", 12 | "url": "http://www.gnu.org/licenses/lgpl.txt" 13 | }], 14 | "resolvers": ["http://scala-tools.org/repo-releases"], 15 | "dependencies": [{ 16 | "organization":"org.apache.httpcomponents", 17 | "name": "httpclient", 18 | "version": "4.1.3" 19 | }], 20 | "scalas": ["2.8.0","2.8.1","2.8.2","2.9.0","2.9.0-1","2.9.1"], 21 | "sbt": false 22 | } -------------------------------------------------------------------------------- /http/src/main/ls/0.8.9.json: -------------------------------------------------------------------------------- 1 | { 2 | "organization" : "net.databinder", 3 | "name" : "dispatch-http", 4 | "version" : "0.8.9", 5 | "description" : "Standard HTTP executor, uses Apache DefaultHttpClient", 6 | "site" : "http://dispatch.databinder.net/", 7 | "tags" : [ ], 8 | "docs" : "", 9 | "resolvers" : [ "https://oss.sonatype.org/content/repositories/releases" ], 10 | "dependencies" : [ { 11 | "organization" : "org.apache.httpcomponents", 12 | "name" : "httpclient", 13 | "version" : "4.1.3" 14 | } ], 15 | "scalas" : [ "2.8.0", "2.8.1", "2.8.2", "2.9.0", "2.9.0-1", "2.9.1", "2.9.1-1", "2.9.2" ], 16 | "licenses" : [ { 17 | "name" : "LGPL v3", 18 | "url" : "http://www.gnu.org/licenses/lgpl.txt" 19 | } ], 20 | "sbt" : false 21 | } -------------------------------------------------------------------------------- /http/src/main/scala/ConfiguredHttpClient.scala: -------------------------------------------------------------------------------- 1 | package dispatch.classic 2 | 3 | import org.apache.http.{HttpHost,HttpVersion,HttpRequest,HttpResponse} 4 | import org.apache.http.auth.{AuthScope, NTCredentials, UsernamePasswordCredentials} 5 | import org.apache.http.impl.client.{DefaultHttpClient, BasicCredentialsProvider} 6 | import org.apache.http.conn.params.ConnRouteParams 7 | import org.apache.http.params.{HttpProtocolParams, BasicHttpParams, HttpParams} 8 | import org.apache.http.client.params.AuthPolicy 9 | 10 | /** Basic extension of DefaultHttpClient defaulting to Http 1.1, UTF8, 11 | * and no Expect-Continue. Scopes authorization credentials to particular 12 | * requests thorugh a DynamicVariable. */ 13 | class ConfiguredHttpClient( 14 | credentials: Http.CurrentCredentials 15 | ) extends DefaultHttpClient { 16 | protected var proxyScope: Option[AuthScope] = None 17 | protected var proxyBasicCredentials: Option[UsernamePasswordCredentials] = None 18 | protected var proxyNTCredentials: Option[NTCredentials] = None 19 | protected def configureProxy(params: HttpParams) = { 20 | val sys = System.getProperties() 21 | val host = sys.getProperty("https.proxyHost", 22 | sys.getProperty("http.proxyHost")) 23 | val port = sys.getProperty("https.proxyPort", 24 | sys.getProperty("http.proxyPort")) 25 | val user = sys.getProperty("https.proxyUser", 26 | sys.getProperty("http.proxyUser")) 27 | val password = sys.getProperty("https.proxyPassword", 28 | sys.getProperty("http.proxyPassword")) 29 | val domain = sys.getProperty("https.auth.ntlm.domain", 30 | sys.getProperty("http.auth.ntlm.domain")) 31 | if (host != null && !host.trim.isEmpty && port != null && !port.trim.isEmpty) { 32 | ConnRouteParams.setDefaultProxy(params, 33 | new HttpHost(host, port.toInt)) 34 | proxyScope = Some(new AuthScope(host, port.toInt)) 35 | } 36 | if (user != null && !user.trim.isEmpty && password != null && !password.trim.isEmpty) { 37 | proxyBasicCredentials = 38 | Some(new UsernamePasswordCredentials(user, password)) 39 | // We should pass our hostname, actually 40 | // Also, we ought to support "domain/user" syntax 41 | proxyNTCredentials = Some(new NTCredentials( 42 | user, password, "", Option(domain) getOrElse "")) 43 | } 44 | params 45 | } 46 | 47 | override def createHttpParams = { 48 | val params = new BasicHttpParams 49 | HttpProtocolParams.setVersion(params, HttpVersion.HTTP_1_1) 50 | HttpProtocolParams.setContentCharset(params, Request.factoryCharset) 51 | HttpProtocolParams.setUseExpectContinue(params, false) 52 | configureProxy(params) 53 | } 54 | /** Follow response redirect regardless of request method */ 55 | setRedirectStrategy(new org.apache.http.impl.client.DefaultRedirectStrategy { 56 | import org.apache.http.protocol.HttpContext 57 | import org.apache.http.HttpStatus._ 58 | override def isRedirected(req: HttpRequest, res: HttpResponse, ctx: HttpContext) = 59 | (SC_MOVED_TEMPORARILY :: SC_MOVED_PERMANENTLY :: SC_TEMPORARY_REDIRECT :: 60 | SC_SEE_OTHER :: Nil) contains res.getStatusLine.getStatusCode 61 | }) 62 | setCredentialsProvider(new BasicCredentialsProvider { 63 | override def getCredentials(scope: AuthScope) = credentials.value flatMap { 64 | case (auth_scope, Credentials(n, p)) if scope.`match`(auth_scope) >= 0 => 65 | Some(new UsernamePasswordCredentials(n, p)) 66 | case _ => None 67 | } orElse { 68 | // This test probably returns true even if only the port matches 69 | if (proxyScope exists (scope.`match`(_) >= 0)) { 70 | if (scope.getScheme() == AuthPolicy.NTLM) proxyNTCredentials 71 | else proxyBasicCredentials 72 | } else None 73 | } orNull 74 | }) 75 | } 76 | 77 | -------------------------------------------------------------------------------- /http/src/main/scala/Http.scala: -------------------------------------------------------------------------------- 1 | package dispatch.classic 2 | 3 | import collection.Map 4 | import collection.immutable.{Map => IMap} 5 | import java.net.URI 6 | 7 | import org.apache.http.{HttpHost,HttpRequest,HttpResponse,HttpEntity} 8 | import org.apache.http.client.HttpClient 9 | import org.apache.http.client.entity.UrlEncodedFormEntity 10 | import org.apache.http.client.methods.HttpRequestBase 11 | import org.apache.http.client.utils.URLEncodedUtils 12 | import org.apache.http.auth.AuthScope 13 | import org.apache.http.params.HttpProtocolParams 14 | 15 | import org.apache.http.entity.{StringEntity,FileEntity} 16 | import org.apache.http.message.BasicNameValuePair 17 | import org.apache.http.conn.params.ConnRouteParams 18 | import org.apache.http.conn.ClientConnectionManager 19 | 20 | /** Http access point. Standard instances to be used by a single thread. */ 21 | class Http extends BlockingHttp { 22 | /** Unadorned handler return type */ 23 | type HttpPackage[T] = T 24 | /** Synchronously access and return plain result value */ 25 | def pack[T](req: { def abort() }, result: => T) = result 26 | } 27 | 28 | /** May be used directly from any thread. */ 29 | object Http extends Http with thread.Safety { 30 | type CurrentCredentials = util.DynamicVariable[Option[(AuthScope, Credentials)]] 31 | } 32 | 33 | trait BlockingHttp extends HttpExecutor with BlockingCallback { 34 | import util.control.{Exception => Exc} 35 | /** This reference's underlying value is updated for the current thread when a 36 | * request specifies credentials. Typically passed to ConfiguredHttpClient. */ 37 | val credentials = new Http.CurrentCredentials(None) 38 | /** Reference to the underlying client. Override make_client define your own. */ 39 | final val client = make_client 40 | /** Defaults to dispatch.ConfiguredHttpClient(credentials), override to customize. */ 41 | def make_client: HttpClient = new ConfiguredHttpClient(credentials) 42 | 43 | /** Execute method for the given host. */ 44 | private def execute(host: HttpHost, req: HttpRequestBase): HttpResponse = { 45 | client.execute(host, req) 46 | } 47 | /** Execute for given optional parameters, with logging. Creates local scope for credentials. */ 48 | def execute[T](host: HttpHost, 49 | credsopt: Option[Credentials], 50 | req: HttpRequestBase, 51 | block: HttpResponse => T, 52 | listener: ExceptionListener) = 53 | pack(req, try { 54 | block(credsopt.map { creds => 55 | credentials.withValue(Some(( 56 | new AuthScope(host.getHostName, host.getPort), creds) 57 | ))(execute(host, req)) 58 | } getOrElse { execute(host, req) }) 59 | } catch { 60 | case e => listener.lift(e); throw e 61 | }) 62 | /** Make sure the entity is consumed. */ 63 | def consumeContent(entity: Option[HttpEntity]) { 64 | entity.foreach(org.apache.http.util.EntityUtils.consume) 65 | } 66 | /** Potentially wraps payload, e.g. in a Future */ 67 | def pack[T](req: { def abort() }, result: => T): HttpPackage[T] 68 | 69 | /** Shutdown connection manager, threads. */ 70 | def shutdownClient() = client.getConnectionManager.shutdown() 71 | } 72 | -------------------------------------------------------------------------------- /http/src/main/scala/https.scala: -------------------------------------------------------------------------------- 1 | package dispatch.classic 2 | 3 | /** Trust all TLS certs */ 4 | trait HttpsLeniency { self: BlockingHttp => 5 | import java.net.Socket 6 | import javax.net.ssl.{X509TrustManager, SSLContext} 7 | import java.security.KeyStore 8 | import java.security.cert.X509Certificate 9 | import org.apache.http.conn.scheme.Scheme 10 | import org.apache.http.conn.ssl.SSLSocketFactory 11 | 12 | private def socket_factory = { 13 | val truststore = KeyStore.getInstance(KeyStore.getDefaultType()) 14 | truststore.load(null, null) 15 | val context = SSLContext.getInstance(SSLSocketFactory.TLS) 16 | val manager = new X509TrustManager { 17 | def checkClientTrusted(xcs: Array[X509Certificate], string: String) {} 18 | def checkServerTrusted(xcs: Array[X509Certificate], string: String) {} 19 | def getAcceptedIssuers = null 20 | } 21 | context.init(null, Array(manager), null) 22 | new SSLSocketFactory(context, 23 | SSLSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER) 24 | } 25 | 26 | client.getConnectionManager.getSchemeRegistry.register( 27 | new Scheme("https", 443, socket_factory) 28 | ) 29 | } 30 | -------------------------------------------------------------------------------- /http/src/main/scala/thread/thread.scala: -------------------------------------------------------------------------------- 1 | package dispatch.classic.thread 2 | 3 | import dispatch.classic._ 4 | import dispatch.classic.futures._ 5 | import org.apache.http.client.HttpClient 6 | 7 | /** Http with a thread-safe client */ 8 | trait Safety { self: BlockingHttp => 9 | /** Maximum number of connections in pool, default is 50 */ 10 | def maxConnections = 50 11 | /** Maximum number of connections one one route, default is maxConnections */ 12 | def maxConnectionsPerRoute = maxConnections 13 | /** Shutdown connection manager if no longer in use. */ 14 | 15 | override def make_client: HttpClient = new ThreadSafeHttpClient( 16 | credentials, maxConnections, maxConnectionsPerRoute) 17 | } 18 | 19 | /** Wraps each call in a (threaded) future. */ 20 | trait Future extends Safety { self: BlockingHttp => 21 | type HttpPackage[T] = StoppableFuture[T] 22 | def pack[T](request: { def abort() }, result: => T) = new StoppableFuture[T] { 23 | val delegate = DefaultFuture.future(result) 24 | def apply() = delegate.apply() 25 | def isSet = delegate.isSet 26 | def stop() = request.abort() 27 | } 28 | } 29 | 30 | class Http extends BlockingHttp with Future 31 | 32 | /** Client with a ThreadSafeClientConnManager */ 33 | class ThreadSafeHttpClient( 34 | credentials: Http.CurrentCredentials, 35 | maxConnections: Int, 36 | maxConnectionsPerRoute: Int 37 | ) extends ConfiguredHttpClient(credentials) { 38 | import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager 39 | override def createClientConnectionManager = { 40 | val cm = new ThreadSafeClientConnManager() 41 | cm.setMaxTotal(maxConnections) 42 | cm.setDefaultMaxPerRoute(maxConnectionsPerRoute) 43 | cm 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /http/src/test/scala/HttpSpec.scala: -------------------------------------------------------------------------------- 1 | import org.apache.http.client.methods.HttpUriRequest 2 | import org.apache.http.client.{ResponseHandler, HttpClient} 3 | import org.apache.http.protocol.HttpContext 4 | import org.apache.http.{HttpRequest, HttpHost} 5 | import org.specs2.mutable.Specification 6 | 7 | object HttpSpec extends Specification { 8 | import dispatch.classic._ 9 | 10 | import org.apache.http.protocol.HTTP.CONTENT_ENCODING 11 | 12 | val jane = "It is a truth universally acknowledged, that a single man in possession of a good fortune, must be in want of a wife.\n" 13 | 14 | "Singleton Http test get" should { 15 | val req = new Request("http://technically.us/test.text") 16 | "not throw exception if credentials are specified without explicit host" in { 17 | Http (req as ("user", "pass") as_str) must_== (jane) 18 | } 19 | get_specs(req) 20 | } 21 | "Bound host get" should { 22 | val req = :/("technically.us") / "test.text" 23 | "not throw exception if credentials are specified with explicit host" in { 24 | Http (req as ("user", "pass") as_str) must_== (jane) 25 | } 26 | get_specs(req) 27 | } 28 | "Combined request get" should { 29 | get_specs(:/("technically.us") <& /("test.text")) 30 | } 31 | "Backwards combined request get" should { 32 | get_specs(/("test.text") <& :/("technically.us")) 33 | } 34 | 35 | "Http" should { 36 | class SimpleDelegatingHttpClient(realClient: HttpClient) extends HttpClient { 37 | def getParams = realClient.getParams 38 | def getConnectionManager = realClient.getConnectionManager 39 | def execute(request: HttpUriRequest) = realClient.execute(request) 40 | def execute(request: HttpUriRequest, context: HttpContext) = realClient.execute(request, context) 41 | def execute(target: HttpHost, request: HttpRequest) = realClient.execute(target, request) 42 | def execute(target: HttpHost, request: HttpRequest, context: HttpContext) = realClient.execute(target, request, context) 43 | def execute[T](request: HttpUriRequest, responseHandler: ResponseHandler[_ <: T]) = realClient.execute(request, responseHandler) 44 | def execute[T](request: HttpUriRequest, responseHandler: ResponseHandler[_ <: T], context: HttpContext) = realClient.execute(request, responseHandler, context) 45 | def execute[T](target: HttpHost, request: HttpRequest, responseHandler: ResponseHandler[_ <: T]) = realClient.execute(target, request, responseHandler) 46 | def execute[T](target: HttpHost, request: HttpRequest, responseHandler: ResponseHandler[_ <: T], context: HttpContext) = realClient.execute(target, request, responseHandler, context) 47 | } 48 | 49 | "allow override" in { 50 | val http = new Http with thread.Safety { 51 | override def make_client: HttpClient = new SimpleDelegatingHttpClient(super.make_client) 52 | } 53 | http must not beNull; // i.e. this code should compile 54 | http.shutdown() 55 | success 56 | } 57 | } 58 | 59 | val http = new Http 60 | val httpfuture = new thread.Http 61 | def get_specs(test: Request) = { 62 | // start some connections as futures 63 | val stream = httpfuture(test >> { stm => 64 | // the nested scenario here contrived fails with actors.Futures 65 | httpfuture((test >> { stm => 66 | scala.io.Source.fromInputStream(stm).mkString 67 | }) ~> { string => 68 | string // identity function 69 | }) 70 | }) 71 | val string = httpfuture(test as_str) 72 | val bytes = httpfuture(test >>> new java.io.ByteArrayOutputStream) 73 | // test a few other things 74 | "throw status code exception when applied to non-existent resource" in { 75 | http (test / "do_not_want" as_str) must throwA[StatusCode] 76 | } 77 | "allow any status code with x" in { 78 | (http x (test / "do_not_want" as_str) { 79 | case (404, _, _, out) => out() 80 | case _ => "success is failure" 81 | }) must contain ("404 Not Found") 82 | } 83 | "serve a gzip header" in { 84 | http(test.gzip >:> { _(CONTENT_ENCODING) }) must_== (Set("gzip")) 85 | } 86 | // check back on the futures 87 | "equal expected string" in { 88 | string() must_== jane 89 | } 90 | "stream to expected sting" in { 91 | stream()() must_== jane 92 | } 93 | "write to expected sting bytes" in { 94 | bytes().toByteArray.toList must_== jane.getBytes.toList 95 | } 96 | 97 | "equal expected string with gzip encoding, using future" in { 98 | httpfuture(test.gzip >+ { r => (r as_str, r >:> { _(CONTENT_ENCODING) }) } )() must_== (jane, Set("gzip")) 99 | } 100 | val h = new Http// single threaded Http instance 101 | "equal expected string with a gzip defaulter" in { 102 | val my_defaults = /\.gzip 103 | h(my_defaults <& test >+ { r => (r as_str, r >:> { _(CONTENT_ENCODING) }) } ) must_== (jane, Set("gzip")) 104 | } 105 | 106 | "process html page" in { 107 | import XhtmlParsing._ 108 | h(url("http://technically.us/") </> { xml => 109 | (xml \\ "title").text 110 | }) must_== "technically.us" 111 | } 112 | 113 | "process xml response" in { 114 | h(url("http://technically.us/test.xml") <> { xml => 115 | (xml \ "quote").text.trim 116 | }) must_== jane.trim 117 | } 118 | 119 | "equal expected string without gzip encoding, with handler chaining" in { 120 | h(test >+> { r => r >:> { headers => 121 | r >- { (_, headers(CONTENT_ENCODING)) } 122 | } }) must_== (jane, Set()) 123 | } 124 | "equal expected string with gzip encoding, with >:+" in { 125 | h(test.gzip >:+ { (headers, r) => 126 | r >- { (_, headers(CONTENT_ENCODING.toLowerCase)) } 127 | }) must_== (jane, Seq("gzip")) 128 | } 129 | } 130 | "Path building responses" should { 131 | // using singleton Http, will need to shut down after all tests 132 | val test2 = "and they were both ever sensible of the warmest gratitude\n" 133 | "work with chaining" in { 134 | Http( :/("technically.us") / "test" / "test.text" as_str ) must_== test2 135 | } 136 | "work with factories" in { 137 | Http( :/("technically.us") <& /("test") <& /("test.text") as_str ) must_== test2 138 | } 139 | } 140 | 141 | } 142 | -------------------------------------------------------------------------------- /json/src/main/ls/0.8.6.json: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "organization":"net.databinder", 4 | "name":"dispatch-json", 5 | "version":"0.8.6", 6 | "description":"A JSON parser", 7 | "site":"http://dispatch.databinder.net/", 8 | "tags":[], 9 | "docs":"", 10 | "licenses": [], 11 | "resolvers": ["http://scala-tools.org/repo-releases"], 12 | "dependencies": [], 13 | "scalas": ["2.8.0","2.8.1","2.8.2","2.9.0","2.9.0-1","2.9.1"], 14 | "sbt": false 15 | } -------------------------------------------------------------------------------- /json/src/main/ls/0.8.7.json: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "organization":"net.databinder", 4 | "name":"dispatch-json", 5 | "version":"0.8.7", 6 | "description":"A JSON parser", 7 | "site":"http://dispatch.databinder.net/", 8 | "tags":[], 9 | "docs":"", 10 | "licenses": [], 11 | "resolvers": ["http://scala-tools.org/repo-releases"], 12 | "dependencies": [], 13 | "scalas": ["2.8.0","2.8.1","2.8.2","2.9.0","2.9.0-1","2.9.1"], 14 | "sbt": false 15 | } -------------------------------------------------------------------------------- /json/src/main/ls/0.8.8.json: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "organization":"net.databinder", 4 | "name":"dispatch-json", 5 | "version":"0.8.8", 6 | "description":"A JSON parser", 7 | "site":"http://dispatch.databinder.net/", 8 | "tags":[], 9 | "docs":"", 10 | "licenses": [{ 11 | "name": "LGPL v3", 12 | "url": "http://www.gnu.org/licenses/lgpl.txt" 13 | }], 14 | "resolvers": ["http://scala-tools.org/repo-releases"], 15 | "dependencies": [], 16 | "scalas": ["2.8.0","2.8.1","2.8.2","2.9.0","2.9.0-1","2.9.1"], 17 | "sbt": false 18 | } -------------------------------------------------------------------------------- /json/src/main/ls/0.8.9.json: -------------------------------------------------------------------------------- 1 | { 2 | "organization" : "net.databinder", 3 | "name" : "dispatch-json", 4 | "version" : "0.8.9", 5 | "description" : "A JSON parser", 6 | "site" : "http://dispatch.databinder.net/", 7 | "tags" : [ ], 8 | "docs" : "", 9 | "resolvers" : [ "https://oss.sonatype.org/content/repositories/releases" ], 10 | "dependencies" : [ ], 11 | "scalas" : [ "2.8.0", "2.8.1", "2.8.2", "2.9.0", "2.9.0-1", "2.9.1", "2.9.1-1", "2.9.2" ], 12 | "licenses" : [ { 13 | "name" : "LGPL v3", 14 | "url" : "http://www.gnu.org/licenses/lgpl.txt" 15 | } ], 16 | "sbt" : false 17 | } -------------------------------------------------------------------------------- /json/src/main/scala/Json.scala: -------------------------------------------------------------------------------- 1 | package dispatch.classic.json 2 | 3 | // http://paste.pocoo.org/raw/106688/ 4 | 5 | import scala.util.parsing.combinator._ 6 | import scala.util.parsing.combinator.syntactical._ 7 | import scala.util.parsing.combinator.lexical._ 8 | import scala.util.parsing.input.{CharSequenceReader,CharArrayReader} 9 | import java.io.{InputStream, InputStreamReader} 10 | 11 | class JsonParser extends StdTokenParsers with ImplicitConversions { 12 | type Tokens = scala.util.parsing.json.Lexer 13 | val lexical = new Tokens 14 | 15 | lexical.reserved ++= List("true", "false", "null") 16 | lexical.delimiters ++= List("{", "}", "[", "]", ":", ",") 17 | 18 | def jsonObj = "{" ~> repsep(objPair, ",") <~ "}" ^^ (JsObject.apply _) 19 | def jsonArr = "[" ~> repsep(jsonVal, ",") <~ "]" ^^ (JsArray.apply _) 20 | def objPair = jsonStr ~ (":" ~> jsonVal) ^^ { case x ~ y => (x, y) } 21 | def jsonVal: Parser[JsValue] = 22 | (jsonObj | jsonArr | jsonStr | jsonNum | "true" ^^^ JsTrue | "false" ^^^ JsFalse | "null" ^^^ JsNull) 23 | def jsonStr = accept("string", { case lexical.StringLit(n) => JsString(n)}) 24 | def jsonNum = accept("number", { case lexical.NumericLit(n) => JsNumber(n) }) 25 | 26 | def apply(input: CharSequenceReader): JsValue = 27 | phrase(jsonVal)(new lexical.Scanner(input)) match { 28 | case Success(result, _) => result 29 | case _ => throw new Exception("Illegal JSON format") 30 | } 31 | } 32 | 33 | object JsonParser { 34 | def apply(input: CharSequenceReader): JsValue = { 35 | val parse = new JsonParser 36 | parse(input) 37 | } 38 | } 39 | 40 | sealed trait JsValue { 41 | type T 42 | def self: T 43 | override def toString = JsValue.toJson(this) 44 | } 45 | 46 | case class JsString(override val self: String) extends JsValue { 47 | type T = String 48 | } 49 | 50 | /** 51 | * This can also be implemented with as a Double, even though BigDecimal is 52 | * more loyal to the json spec. 53 | * NOTE: Subtle bugs can arise, i.e. 54 | * BigDecimal(3.14) != BigDecimal("3.14") 55 | * such are the perils of floating point arithmetic. 56 | */ 57 | case class JsNumber(override val self: BigDecimal) extends JsValue { 58 | type T = BigDecimal 59 | } 60 | 61 | // This can extend scala.collection.MapProxy to implement Map interface 62 | case class JsObject(override val self: Map[JsString, JsValue]) extends JsValue { 63 | type T = Map[JsString, JsValue] 64 | } 65 | 66 | // This can extend scala.SeqProxy to implement Seq interface 67 | case class JsArray(override val self: List[JsValue]) extends JsValue { 68 | type T = List[JsValue] 69 | } 70 | 71 | sealed abstract case class JsBoolean(b: Boolean) extends JsValue { 72 | type T = Boolean 73 | val self = b 74 | } 75 | 76 | object JsTrue extends JsBoolean(true) 77 | object JsFalse extends JsBoolean(false) 78 | case object JsNull extends JsValue { 79 | type T = Null 80 | val self = null 81 | } 82 | 83 | object JsObject { 84 | def apply() = new JsObject(Map()) 85 | def apply(xs: Seq[(JsString, JsValue)]) = new JsObject(Map() ++ xs) 86 | } 87 | 88 | object JsNumber { 89 | def apply(n: Int) = new JsNumber(BigDecimal(n)) 90 | def apply(n: Long) = new JsNumber(BigDecimal(n)) 91 | def apply(n: Float) = new JsNumber(BigDecimal(n)) 92 | def apply(n: Double) = new JsNumber(BigDecimal(n)) 93 | def apply(n: BigInt) = new JsNumber(BigDecimal(n)) 94 | def apply(n: String) = new JsNumber(BigDecimal(n)) 95 | } 96 | 97 | object JsString { 98 | def apply(x: Any): JsString = x match { 99 | case s: Symbol => new JsString(s.name) 100 | case s: String => new JsString(s) 101 | // This is a hack needed for JsObject. 102 | // The other option is to throw an exception here. 103 | case _ => new JsString(x.toString) 104 | } 105 | } 106 | 107 | object JsValue { 108 | def apply(x: Any): JsValue = x match { 109 | case null => JsNull 110 | case j: JsValue => j 111 | case true => JsTrue 112 | case false => JsFalse 113 | case s @ (_: String | _: Symbol) => JsString(s) 114 | case n: Int => JsNumber(n) 115 | case n: Long => JsNumber(n) 116 | case n: Float => JsNumber(n) 117 | case n: Double => JsNumber(n) 118 | case n: BigInt => JsNumber(n) 119 | case n: BigDecimal => JsNumber(n) 120 | case xs: List[_] => JsArray(xs.map(JsValue.apply)) 121 | case m: scala.collection.Map[_, _] => JsObject( 122 | Map.empty ++ (for ((key, value) <- m) yield (JsString(key), JsValue(value))) 123 | ) 124 | case xs: Seq[_] => JsArray(xs.map(JsValue.apply).toList) 125 | } 126 | 127 | def fromString(s: String) = JsonParser(new CharArrayReader(s.toCharArray)) 128 | def fromStream(s: InputStream, charset: String = "utf-8") = 129 | JsonParser(new CharArrayReader(io.Source.fromInputStream(s, charset).mkString.toCharArray)) 130 | 131 | def toJson(x: JsValue): String = { 132 | val w = new java.lang.StringBuilder 133 | writeJson(x, w) 134 | w.toString 135 | } 136 | 137 | def writeJson(x: JsValue, w: Appendable) { 138 | x match { 139 | case JsNull => w.append("null") 140 | case JsBoolean(b) => w.append(b.toString) 141 | case JsNumber(n) => w.append(n.toString) 142 | case JsString(s) => { 143 | w.append("\"") 144 | s foreach { 145 | case '\\' => w.append("\\\\") 146 | case '\n' => w.append("\\n") 147 | case '\r' => w.append("\\r") 148 | case '\t' => w.append("\\t") 149 | case '"' => w.append("\\\"") 150 | case c if c <= 0x1F => w.append("\\u%04x".format(c.toInt)) 151 | case c => w.append(c) 152 | } 153 | w.append("\"") 154 | } 155 | case JsArray(ys) => { 156 | w.append("[") 157 | var first = true 158 | for (y <- ys) { 159 | if (first) { 160 | first = false 161 | } else { 162 | w.append(", ") 163 | } 164 | writeJson(y, w) 165 | } 166 | w.append("]") 167 | } 168 | case JsObject(m) => { 169 | w.append("{") 170 | var first = true 171 | for ((k,v) <- m) { 172 | if (first) { 173 | first = false 174 | } else { 175 | w.append(", ") 176 | } 177 | writeJson(k, w) 178 | w.append(": ") 179 | writeJson(v, w) 180 | } 181 | w.append("}") 182 | } 183 | } 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /json/src/main/scala/JsonExtractor.scala: -------------------------------------------------------------------------------- 1 | package dispatch.classic.json 2 | 3 | /** Json Extractor, extracts a value of type T from the given JsValue. */ 4 | trait Extract[T] { 5 | def unapply(js: JsValue): Option[T] 6 | override def toString = getClass.getName 7 | } 8 | /** Json Inserter, adds or replaces properties in the given JsValue 9 | (error if not a JsObject) */ 10 | trait Insert[T] extends Extract[T] { 11 | /** Insert value into JsValue given after */ 12 | def << (t: T)(js: JsValue): JsObject 13 | /** Insert value into empty JsValue */ 14 | def <<| (t: T) = <<(t)(Js()) 15 | } 16 | /** Json Property Extractor, extracts a value of type T assigned to 17 | the property sym in a given JsObject (checks any JsValue). Additionally, 18 | can replace the property sym with a new value T in the << opertion. */ 19 | case class Property[T](sym: Symbol, ext: Extract[T]) extends Extract[T] with Insert[T] { 20 | def unapply(js: JsValue) = js match { 21 | case js: JsObject => js.self.get(JsString(sym)) flatMap ext.unapply 22 | case _ => None 23 | } 24 | /** Adds or replaces the property sym in the given JsValue (JsObject) */ 25 | def << (t: T)(js: JsValue) = js match { 26 | case JsObject(m) => JsObject(m + (JsString(sym) -> JsValue(t))) 27 | case js => sys.error("Unable to replace property in " + js) 28 | } 29 | override def toString = "%s ! %s" format (sym, ext) 30 | } 31 | /** Extractor that resolves first by its parent extractor, if present. */ 32 | case class Child[T, E <: Insert[T]](parent: Option[Obj], self: E) extends Extract[T] with Insert[T] { 33 | def unapply(js: JsValue) = parent map { parent => js match { 34 | case parent(self(t)) => Some(t) 35 | } } getOrElse { js match { 36 | case self(t) => Some(t) 37 | case _ => None 38 | } 39 | } 40 | /** Inserts the value t in self and replaces self in parent, if any. */ 41 | def << (t: T)(js: JsValue) = parent map { parent => js match { 42 | case parent(my_js) => (parent << (self << t)(my_js))(js) 43 | } } getOrElse (self << t)(js) 44 | } 45 | /** Obj extractor, respects current parent context and sets a new context to itself. */ 46 | class Obj(sym: Symbol)(implicit parent: Option[Obj]) 47 | extends Child[JsObject, Property[JsObject]](parent, Property(sym, Js.obj)) { 48 | implicit val ctx = Some(this) 49 | } 50 | /** Namespace and context for Json extraction. Client extraction 51 | objects, e.g. dispatch.twitter.Search, may extend this trait to 52 | receive all implicit functions and values. */ 53 | trait Js { 54 | object str extends Extract[String] { 55 | def unapply(js: JsValue) = js match { 56 | case JsString(v) => Some(v) 57 | case _ => None 58 | } 59 | } 60 | object num extends Extract[BigDecimal] { 61 | def unapply(js: JsValue) = js match { 62 | case JsNumber(v) => Some(v) 63 | case _ => None 64 | } 65 | } 66 | object bool extends Extract[Boolean] { 67 | def unapply(js: JsValue) = js match { 68 | case JsBoolean(v) => Some(v) 69 | case _ => None 70 | } 71 | } 72 | // keep in wrapped type to allow nested extractors 73 | object obj extends Extract[JsObject] { 74 | def unapply(js: JsValue) = js match { 75 | case JsObject(v) => Some(JsObject(v)) 76 | case _ => None 77 | } 78 | } 79 | object list extends Extract[List[JsValue]] { 80 | def unapply(js: JsValue) = js match { 81 | case JsArray(v) => Some(v) 82 | case _ => None 83 | } 84 | def ! [T](ext: Extract[T]) = new Extract[List[T]] { 85 | def unapply(js: JsValue) = js match { 86 | case list(l) => Some(l map { _ match { case ext(v) => v } } ) 87 | case _ => None 88 | } 89 | } 90 | } 91 | 92 | /** The parent Obj context, defaults to None for top level definitions. */ 93 | implicit val ctx: Option[Obj] = None 94 | 95 | /** Assertion extracting function, error if expected Js type is not present. */ 96 | type JsF[T] = JsValue => T 97 | 98 | /** Assertion inserting function, puts any type value into the given JsValue. */ 99 | type JsIF = JsF[JsObject] 100 | 101 | /** Add operators to Symbol. */ 102 | implicit def sym_add_operators[T](sym: Symbol) = new SymOp(sym) 103 | /** For ! , ? , and << operators on Symbol. */ 104 | case class SymOp(sym: Symbol) { 105 | /** @return an extractor, respects current Obj context */ 106 | def ? [T](cst: Extract[T])(implicit parent: Option[Obj]) = 107 | new Child[T, Property[T]](parent, Property(sym, cst)) 108 | 109 | /** @return an assertion extracting function (JsF) */ 110 | def ! [T](cst: Extract[T]): JsF[T] = Js.ext2fun(new Property(sym, cst)) 111 | 112 | /** @return a optional extracting function */ 113 | def ?? [T](cst: Extract[T]): JsValue => Option[T] = jsv => new Property(sym,cst).unapply(jsv) 114 | 115 | /** @return new JsObject with the given sym property replaced by t */ 116 | def << [T](t: T): JsIF = _ match { 117 | case JsObject(m) => JsObject(m + (JsString(sym) -> JsValue(t))) 118 | case js => sys.error("Unable to replace property in " + js) 119 | } 120 | /** Chain this to another assertion inserting function to replace deep properties */ 121 | def << (f: JsIF): JsIF = js => (this << f((this ! obj)(js)))(js) 122 | /** Insert value into empty JsValue */ 123 | def <<| [T] (t: T) = <<(t)(Js()) 124 | } 125 | /** Combines assertion extracting functions into a single function returning a tuple, when curried. */ 126 | def %[A,B](a: JsF[A], b: JsF[B])(js: JsValue) = (a(js), b(js)) 127 | /** Combines assertion extracting functions into a single function returning a tuple, when curried. */ 128 | def %[A,B,C](a: JsF[A], b: JsF[B], c: JsF[C])(js: JsValue) = (a(js), b(js), c(js)) 129 | /** Combines assertion extracting functions into a single function returning a tuple, when curried. */ 130 | def %[A,B,C,D](a: JsF[A], b: JsF[B], c: JsF[C], d: JsF[D])(js: JsValue) = (a(js), b(js), c(js), d(js)) 131 | /** Converts a Json extrator to an assertion extracting function (JsF). */ 132 | implicit def ext2fun[T](ext: Extract[T]): JsF[T] = jsv => ext.unapply(jsv).getOrElse { 133 | sys.error("Extractor %s does not match JSON: %s" format (ext, jsv)) 134 | } 135 | } 136 | 137 | /** Factory for JsValues as well as a global access point for 138 | implicit functions and values. This object extends the Js 139 | trait so that `import Js._` brings implicits into scope. */ 140 | object Js extends Js { 141 | def apply(): JsValue = JsObject() 142 | def apply(stream: java.io.InputStream): JsValue = JsValue.fromStream(stream) 143 | def apply(stream: java.io.InputStream, charset: String) = 144 | JsValue.fromStream(stream, charset) 145 | def apply(string: String): JsValue = JsValue.fromString(string) 146 | } 147 | -------------------------------------------------------------------------------- /json/src/test/resources/test.json: -------------------------------------------------------------------------------- 1 | {"completed_in":0.113,"max_id":107131941547016193,"max_id_str":"107131941547016193","next_page":"?page=2&max_id=107131941547016193&q=dispatch","page":1,"query":"dispatch","refresh_url":"?since_id=107131941547016193&q=dispatch","results":[{"created_at":"Fri, 26 Aug 2011 16:46:59 +0000","from_user":"brgiddens","from_user_id":212561564,"from_user_id_str":"212561564","geo":null,"id":107131941547016193,"id_str":"107131941547016193","iso_language_code":"en","metadata":{"result_type":"recent"},"profile_image_url":"http://a2.twimg.com/profile_images/1199478603/20100808-mthood_bombbardier_normal.jpg","source":"<a href="http://twitter.com/tweetbutton" rel="nofollow">Tweet Button</a>","text":"Pointers And Clicks: HCSC Photo Workshop Dispatch | SnowboarderMag.com: http://t.co/xypNWYn via @AddThis","to_user_id":null,"to_user_id_str":null},{"created_at":"Fri, 26 Aug 2011 16:46:56 +0000","from_user":"0Solace0","from_user_id":76246707,"from_user_id_str":"76246707","geo":null,"id":107131928448208897,"id_str":"107131928448208897","iso_language_code":"en","metadata":{"result_type":"recent"},"profile_image_url":"http://a1.twimg.com/profile_images/800077064/twitterProfilePhoto_normal.jpg","source":"<a href="http://twitter.com/">web</a>","text":"RT @WSJ: 150k to evacuate from Ocean City, Maryland. Continuing hurricane coverage: http://t.co/vJGgFkr","to_user_id":null,"to_user_id_str":null},{"created_at":"Fri, 26 Aug 2011 16:46:45 +0000","from_user":"MarieLongchamps","from_user_id":7348058,"from_user_id_str":"7348058","geo":null,"id":107131879995617281,"id_str":"107131879995617281","iso_language_code":"en","metadata":{"result_type":"recent"},"profile_image_url":"http://a3.twimg.com/profile_images/1367339809/Sans_titre_normal.png","source":"<a href="http://www.hootsuite.com" rel="nofollow">HootSuite</a>","text":"RT @GlobalPost: The Search For #Canada's War Criminals. http://ow.ly/6dJrl","to_user_id":null,"to_user_id_str":null},{"created_at":"Fri, 26 Aug 2011 16:46:20 +0000","from_user":"regulaHoli","from_user_id":95202500,"from_user_id_str":"95202500","geo":null,"id":107131776249507840,"id_str":"107131776249507840","iso_language_code":"en","metadata":{"result_type":"recent"},"profile_image_url":"http://a3.twimg.com/profile_images/1487978368/regulaHoli_normal.jpg","source":"<a href="http://twitter.com/">web</a>","text":"RT @WSJ: 150k to evacuate from Ocean City, Maryland. Continuing hurricane coverage: http://t.co/vJGgFkr","to_user_id":null,"to_user_id_str":null},{"created_at":"Fri, 26 Aug 2011 16:46:07 +0000","from_user":"elina_smith","from_user_id":81761364,"from_user_id_str":"81761364","geo":null,"id":107131722709209088,"id_str":"107131722709209088","iso_language_code":"da","metadata":{"result_type":"recent"},"profile_image_url":"http://a2.twimg.com/sticky/default_profile_images/default_profile_3_normal.png","source":"<a href="http://www.facebook.com/twitter" rel="nofollow">Facebook</a>","text":"TANF and Drug Testing http://t.co/I2Ts3hB","to_user_id":null,"to_user_id_str":null},{"created_at":"Fri, 26 Aug 2011 16:46:07 +0000","from_user":"aiodts","from_user_id":813085,"from_user_id_str":"813085","geo":null,"id":107131721304121344,"id_str":"107131721304121344","iso_language_code":"da","metadata":{"result_type":"recent"},"profile_image_url":"http://a1.twimg.com/profile_images/1493283103/access_normal.jpg","source":"<a href="http://www.merchantcircle.com/" rel="nofollow">MerchantCircle</a>","text":"TANF and Drug Testing http://t.co/OIRHukv","to_user_id":null,"to_user_id_str":null},{"created_at":"Fri, 26 Aug 2011 16:45:53 +0000","from_user":"colourmusic","from_user_id":12998154,"from_user_id_str":"12998154","geo":null,"id":107131662030221312,"id_str":"107131662030221312","iso_language_code":"en","metadata":{"result_type":"recent"},"profile_image_url":"http://a2.twimg.com/profile_images/1449592439/image_normal.jpg","source":"<a href="http://twitter.com/">web</a>","text":"RT @colourrevolt: DENVER - youre the first stop on this tour, lets make it count. Sept 14 All Ages, no excuses. Get tickets, tell yr pals http://t.co/QLhptJa","to_user_id":null,"to_user_id_str":null},{"created_at":"Fri, 26 Aug 2011 16:45:39 +0000","from_user":"arsyfajriar","from_user_id":140570122,"from_user_id_str":"140570122","geo":null,"id":107131606073999360,"id_str":"107131606073999360","iso_language_code":"en","metadata":{"result_type":"recent"},"profile_image_url":"http://a3.twimg.com/profile_images/1511613883/Photo_on_2011-08-25_at_00.34__4_normal.jpg","source":"<a href="http://blackberry.com/twitter" rel="nofollow">Twitter for BlackBerry\u00AE</a>","text":"Wow ! RT @WSJ: 150k to evacuate from Ocean City, Maryland. Continuing hurricane coverage: http://t.co/0cb1QkF","to_user_id":null,"to_user_id_str":null},{"created_at":"Fri, 26 Aug 2011 16:45:30 +0000","from_user":"meghanfrazer","from_user_id":221578256,"from_user_id_str":"221578256","geo":null,"id":107131567050199040,"id_str":"107131567050199040","iso_language_code":"en","metadata":{"result_type":"recent"},"profile_image_url":"http://a2.twimg.com/profile_images/1246622571/2657_58724352636_615132636_1563869_2721974_n_2__normal.jpg","source":"<a href="http://twitter.com/tweetbutton" rel="nofollow">Tweet Button</a>","text":"Bob Hunter commentary: Crew Stadium issue must be addressed | The Columbus Dispatch http://t.co/7jigYCg #fb","to_user_id":null,"to_user_id_str":null},{"created_at":"Fri, 26 Aug 2011 16:45:30 +0000","from_user":"edwinbeall","from_user_id":151559257,"from_user_id_str":"151559257","geo":null,"id":107131564894326785,"id_str":"107131564894326785","iso_language_code":"en","metadata":{"result_type":"recent"},"profile_image_url":"http://a1.twimg.com/profile_images/1094751511/Edwin_Beall_normal.jpg","source":"<a href="http://twitterfeed.com" rel="nofollow">twitterfeed</a>","text":"RT @WSJ: 150k to evacuate from Ocean City, Maryland. Continuing hurricane coverage: http://t.co/DeGD36M","to_user_id":null,"to_user_id_str":null},{"created_at":"Fri, 26 Aug 2011 16:45:29 +0000","from_user":"audiarau","from_user_id":241246580,"from_user_id_str":"241246580","geo":null,"id":107131561996066816,"id_str":"107131561996066816","iso_language_code":"en","metadata":{"result_type":"recent"},"profile_image_url":"http://a2.twimg.com/profile_images/1361359483/image_normal.jpg","source":"<a href="http://twitter.com/">web</a>","text":"RT @WSJ: 150k to evacuate from Ocean City, Maryland. Continuing hurricane coverage: http://t.co/vJGgFkr","to_user_id":null,"to_user_id_str":null},{"created_at":"Fri, 26 Aug 2011 16:45:05 +0000","from_user":"GloryDaysGNV","from_user_id":7823654,"from_user_id_str":"7823654","geo":null,"id":107131460988833792,"id_str":"107131460988833792","iso_language_code":"en","metadata":{"result_type":"recent"},"profile_image_url":"http://a3.twimg.com/profile_images/118112819/Glory_Days_Logo_-_Lo_Res_normal.jpg","source":"<a href="http://twitter.com/">web</a>","text":"REMINDER! Tix on sale NOW for Nov 9 ALL TIME LOW @ FL Theater of Gville! Ppl already scrambling for em! Get yers today! http://t.co/XuGCuZy","to_user_id":null,"to_user_id_str":null},{"created_at":"Fri, 26 Aug 2011 16:45:03 +0000","from_user":"dnjuniorg","from_user_id":81005422,"from_user_id_str":"81005422","geo":null,"id":107131451853651969,"id_str":"107131451853651969","iso_language_code":"en","metadata":{"result_type":"recent"},"profile_image_url":"http://a2.twimg.com/profile_images/1507511422/image_normal.jpg","source":"<a href="http://twitter.com/">web</a>","text":"RT @WSJ: 150k to evacuate from Ocean City, Maryland. Continuing hurricane coverage: http://t.co/vJGgFkr","to_user_id":null,"to_user_id_str":null},{"created_at":"Fri, 26 Aug 2011 16:44:42 +0000","from_user":"thorneultimatum","from_user_id":152400457,"from_user_id_str":"152400457","geo":null,"id":107131364482101248,"id_str":"107131364482101248","iso_language_code":"en","metadata":{"result_type":"recent"},"profile_image_url":"http://a2.twimg.com/profile_images/1325450112/twitter_profile_normal.JPG","source":"<a href="http://ubersocial.com" rel="nofollow">\u00DCberSocial for BlackBerry</a>","text":"My sale goodies are here! Massive kudos to @modelsown for speedy dispatch and beautiful packaging. http://t.co/VJCdwiD","to_user_id":null,"to_user_id_str":null},{"created_at":"Fri, 26 Aug 2011 16:44:41 +0000","from_user":"KitRoupe","from_user_id":137679406,"from_user_id_str":"137679406","geo":null,"id":107131361424457728,"id_str":"107131361424457728","iso_language_code":"en","metadata":{"result_type":"recent"},"profile_image_url":"http://a0.twimg.com/profile_images/1082541445/roupe_37_web_normal.jpg","source":"<a href="http://www.hootsuite.com" rel="nofollow">HootSuite</a>","text":"RT @wsj: 150k to evacuate from Ocean City, Maryland. Continuing hurricane coverage: http://t.co/Y6AX2ia","to_user_id":null,"to_user_id_str":null}],"results_per_page":15,"since_id":0,"since_id_str":"0"} -------------------------------------------------------------------------------- /json/src/test/scala/JsonSpec.scala: -------------------------------------------------------------------------------- 1 | import org.specs2.mutable.Specification 2 | 3 | object JsValueSpec extends Specification { 4 | import dispatch.classic.json._ 5 | import java.io.ByteArrayInputStream 6 | 7 | val testFile = getClass.getClassLoader.getResourceAsStream("test.json") 8 | val json = scala.io.Source.fromInputStream(testFile, "utf-8").mkString 9 | 10 | def time[T](block: => T): (T, Long) = { 11 | val start = System.currentTimeMillis 12 | (block, System.currentTimeMillis - start) 13 | } 14 | 15 | def reparse(x: Any) = Js(JsValue(x).toString) 16 | 17 | "JS parsing" should { 18 | "parse null" in { 19 | Js("null") must be equalTo(JsNull) 20 | } 21 | "parse true" in { 22 | Js("true") must be equalTo(JsTrue) 23 | } 24 | "parse false" in { 25 | Js("false") must be equalTo(JsFalse) 26 | } 27 | "parse number" in { 28 | Js("2394.3") must be equalTo(JsNumber(BigDecimal(2394.3))) 29 | } 30 | "parse string" in { 31 | Js("\"foobie bletch\"") must be equalTo(JsString("foobie bletch")) 32 | } 33 | "parse escaped string" in { 34 | Js("\"hello\\u203D\"") must be equalTo(JsString("hello\u203d")) 35 | } 36 | "parse array" in { 37 | Js("[\"foo\", \"bar\"]") must be equalTo(JsArray(List(JsString("foo"), JsString("bar")))) 38 | } 39 | "parse object" in { 40 | Js("{\"foo\": true}") must be equalTo(JsObject(Map(JsString("foo") -> JsTrue))) 41 | } 42 | } 43 | 44 | "JS round-tripping" should { 45 | "round-trip null" in { 46 | reparse(null) must be equalTo(JsNull) 47 | } 48 | "round-trip string" in { 49 | reparse("HACKEM MUCHE") must be equalTo(JsString("HACKEM MUCHE")) 50 | } 51 | "round-trip array" in { 52 | reparse(List("foo", false)) must be equalTo(JsArray(List(JsString("foo"), JsFalse))) 53 | } 54 | "round-trip object" in { 55 | reparse(Map("scrolls" -> List("identify"), "name" -> "foo")) must be equalTo( 56 | JsObject(Map(JsString("scrolls") -> JsArray(List(JsString("identify"))), 57 | JsString("name") -> JsString("foo")))) 58 | } 59 | "round-trip Unicode string" in { 60 | reparse("control \u0008") must be equalTo(JsString("control \u0008")) 61 | } 62 | } 63 | 64 | "JsValue.fromString and JsValue.fromStream" should { 65 | val maxTime = 1000L 66 | "not become slow under alternate calling" in { 67 | val (_, t1) = time { JsValue.fromString(json) } 68 | t1 must be_<=(maxTime) // normally finish within 1s 69 | val (_, t2) = time { JsValue.fromStream(new ByteArrayInputStream(json.getBytes("utf-8"))) } 70 | t2 must be_<=(maxTime) 71 | } 72 | 73 | "work properly under multi-thread calling" in { 74 | import java.util.concurrent._ 75 | val numOfThreads = 20 76 | val executor = Executors.newFixedThreadPool(numOfThreads) 77 | (1 to numOfThreads).map{i => 78 | val f = new FutureTask(new Callable[(JsValue, Long)]{ 79 | def call = 80 | if (i % 2 == 0) time { JsValue.fromString(json) } 81 | else time { JsValue.fromStream(new ByteArrayInputStream(json.getBytes("utf-8"))) } 82 | }) 83 | executor.execute(f) 84 | f 85 | }.map{_.get._2}.toList must not contain{(_: Long) > maxTime} 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /jsoup/src/main/ls/0.8.7.json: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "organization":"net.databinder", 4 | "name":"dispatch-jsoup", 5 | "version":"0.8.7", 6 | "description":"Adds JSoup handler verbs to Dispatch", 7 | "site":"http://dispatch.databinder.net/", 8 | "tags":[], 9 | "docs":"", 10 | "licenses": [], 11 | "resolvers": ["http://scala-tools.org/repo-releases"], 12 | "dependencies": [{ 13 | "organization":"org.apache.httpcomponents", 14 | "name": "httpclient", 15 | "version": "4.1.2" 16 | },{ 17 | "organization":"org.jsoup", 18 | "name": "jsoup", 19 | "version": "1.6.1" 20 | }], 21 | "scalas": ["2.8.0","2.8.1","2.8.2","2.9.0","2.9.0-1","2.9.1"], 22 | "sbt": false 23 | } -------------------------------------------------------------------------------- /jsoup/src/main/ls/0.8.8.json: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "organization":"net.databinder", 4 | "name":"dispatch-jsoup", 5 | "version":"0.8.8", 6 | "description":"Adds JSoup handler verbs to Dispatch", 7 | "site":"http://dispatch.databinder.net/", 8 | "tags":[], 9 | "docs":"", 10 | "licenses": [{ 11 | "name": "LGPL v3", 12 | "url": "http://www.gnu.org/licenses/lgpl.txt" 13 | }], 14 | "resolvers": ["http://scala-tools.org/repo-releases"], 15 | "dependencies": [{ 16 | "organization":"org.apache.httpcomponents", 17 | "name": "httpclient", 18 | "version": "4.1.3" 19 | },{ 20 | "organization":"org.jsoup", 21 | "name": "jsoup", 22 | "version": "1.6.1" 23 | }], 24 | "scalas": ["2.8.0","2.8.1","2.8.2","2.9.0","2.9.0-1","2.9.1"], 25 | "sbt": false 26 | } -------------------------------------------------------------------------------- /jsoup/src/main/ls/0.8.9.json: -------------------------------------------------------------------------------- 1 | { 2 | "organization" : "net.databinder", 3 | "name" : "dispatch-jsoup", 4 | "version" : "0.8.9", 5 | "description" : "Adds JSoup handler verbs to Dispatch", 6 | "site" : "http://dispatch.databinder.net/", 7 | "tags" : [ ], 8 | "docs" : "", 9 | "resolvers" : [ "https://oss.sonatype.org/content/repositories/releases" ], 10 | "dependencies" : [ { 11 | "organization" : "org.apache.httpcomponents", 12 | "name" : "httpclient", 13 | "version" : "4.1.3" 14 | }, { 15 | "organization" : "org.jsoup", 16 | "name" : "jsoup", 17 | "version" : "1.6.1" 18 | } ], 19 | "scalas" : [ "2.8.0", "2.8.1", "2.8.2", "2.9.0", "2.9.0-1", "2.9.1", "2.9.1-1", "2.9.2" ], 20 | "licenses" : [ { 21 | "name" : "LGPL v3", 22 | "url" : "http://www.gnu.org/licenses/lgpl.txt" 23 | } ], 24 | "sbt" : false 25 | } -------------------------------------------------------------------------------- /jsoup/src/main/scala/JSoupHttp.scala: -------------------------------------------------------------------------------- 1 | package dispatch.classic.jsoup 2 | 3 | import dispatch.classic._ 4 | import org.jsoup.nodes.Document 5 | import org.jsoup.Jsoup 6 | import io.Source 7 | import xml.NodeSeq 8 | 9 | trait ImplicitJSoupHandlers { 10 | implicit def requestToJSoupHandlers(req: Request) = new JSoupHandlers(req); 11 | implicit def stringToJSoupHandlers(str: String) = new JSoupHandlers(new Request(str)); 12 | } 13 | 14 | object JSoupHttp extends ImplicitJSoupHandlers 15 | 16 | class JSoupHandlers(request: Request) { 17 | /** Process response with JSoup html processor in block */ 18 | def jsouped [T] (block: (Document) => T) = request >> { (stm, charset) => 19 | block(Jsoup.parse(stm, charset, request.to_uri.toString)) 20 | } 21 | /** Alias for verb jsouped */ 22 | def </> [T] (block: (Document) => T) = jsouped(block) 23 | /** Conveniences handler for retrieving a org.jsoup.nodes.Document */ 24 | def as_jsouped: Handler[Document] = jsouped { dom => dom } 25 | /** Conveniences handler for retrieving a NodeSeq */ 26 | def as_jsoupedNodeSeq: Handler[NodeSeq] = jsouped { dom: Document => { 27 | xml.parsing.XhtmlParser(Source.fromString(dom.html)) 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /jsoup/src/test/resources/Human.html: -------------------------------------------------------------------------------- 1 | <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" 2 | "http://www.w3.org/TR/html4/loose.dtd"> 3 | <html> 4 | <head> 5 | <title>Human 6 | 7 | 8 |

To err is human, to forgive divine

9 | 10 |

The verb "err" means to do something wrong; to make a mistake is "to err". 11 | "To err is human" because all people ("humans") make mistakes. 12 |

"To err is human, to forgive divine" says we should try hard to forgive others because all 13 | people are human and make mistakes. 14 |

Example: "I am still angry about what my manager did yesterday!" Answer: "It is best to just 15 | let it go; to err is human, to forgive divine." The world of people is "human" 16 | and the world of God is "divine". 17 |

God's special power to forgive people for their mistakes is called "divine" mercy. 18 | When we forgive other people we are acting "divine". "To err is human, to forgive 19 | divine" says that we are all human and we all make mistakes so we should all try hard to forgive other 20 | people when they make mistakes. 21 |

Example: "I will never forgive my mother for what she has done!" 22 |

Answer:

23 |
  • "Don't be angry at her. To err is human, to forgive divine." (Because we use the 24 | word "is" in the first phrase "to err is human" we do not use "is" again in the second phrase: 25 | "to forgive divine".) We are all people who make mistakes so to forgive others when they make 26 | mistakes is the right thing to do; "to err is human, to forgive divine." 27 | 28 | 29 | -------------------------------------------------------------------------------- /jsoup/src/test/resources/test.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | ApA 6 | 7 | 8 |

    Ape

    9 |

    Apes are Old World anthropoid mammals, more specifically a clade of tailless catarrhine primates, 10 | belonging to the biological superfamily Hominoidea. The apes are native to Africa and South-east Asia, 11 | although in relatively recent times humans have spread all over the world. 12 | Apes are the world's largest primates; the orangutan, an ape, is the world's largest living arboreal animal. 13 | Hominoids are traditionally forest dwellers, although chimpanzees may range into savanna, and the extinct 14 | australopithecines are famous for being savanna inhabitants, inferred from their morphology. Humans inhabit 15 | almost every terrestrial habitat.

    16 |

    Hominoidea contains two families of living (extant) species:

    17 | 23 | 24 | -------------------------------------------------------------------------------- /jsoup/src/test/scala/JSoupSpec.scala: -------------------------------------------------------------------------------- 1 | import org.eclipse.jetty.server.handler.{DefaultHandler, HandlerList, ResourceHandler} 2 | import org.eclipse.jetty.server.nio.SelectChannelConnector 3 | import org.eclipse.jetty.server.Server 4 | import org.specs2.mutable.Specification 5 | import dispatch.classic._ 6 | 7 | object JSoupSpec extends Specification with ServedByJetty { 8 | val port = 9990 9 | val resourceBase = "./jsoup/src/test/resources" 10 | 11 | object JSoupHtml extends Request(:/("localhost", port)) 12 | 13 | "JSoup html parser" should { 14 | import dispatch.classic.jsoup.JSoupHttp._ 15 | "successfully parse good html" in { 16 | withResourceServer { _ => 17 | val request = JSoupHtml / "test.html" 18 | Http(request as_jsouped) must not (throwA[Exception]) 19 | } 20 | } 21 | "successfully parse bad html" in { 22 | withResourceServer { _ => 23 | val request = JSoupHtml / "Human.html" 24 | Http(request as_jsouped) must not (throwA[Exception]) 25 | } 26 | } 27 | } 28 | 29 | "Using JSoup Document" should { 30 | import dispatch.classic.jsoup.JSoupHttp._ 31 | "find Elements by tag" in { 32 | withResourceServer { _ => 33 | val request = JSoupHtml / "test.html" 34 | val elements = Http(request jsouped { doc => 35 | doc.getElementsByTag("title"); 36 | }) 37 | 38 | elements.size must be_==(1) 39 | elements.get(0).text must be_==("ApA") 40 | } 41 | } 42 | 43 | "make Elements processable as scala collections" in { 44 | withResourceServer { _ => 45 | val request = JSoupHtml / "Human.html" 46 | val elements = Http(request jsouped { doc => 47 | doc.getElementsByTag("h1"); 48 | }) 49 | 50 | import scala.collection.JavaConversions._ 51 | val hOnes = elements.iterator.toList.map(e => e.text.reverse) 52 | hOnes.head must be_==("enivid evigrof ot ,namuh si rre oT") 53 | } 54 | } 55 | 56 | "make resolved links absolute" in { 57 | withResourceServer { _ => 58 | import scala.collection.JavaConversions._ 59 | val request = JSoupHtml / "test.html" 60 | val links = Http(request jsouped { doc => 61 | doc.select("a[href]").toList; 62 | }) 63 | 64 | links.head.attr("href") must be_==("/Human.html") 65 | links.head.attr("abs:href") must be_==("http://localhost:9990/Human.html") 66 | } 67 | } 68 | } 69 | 70 | """Using the verb """ should { 71 | import dispatch.classic.jsoup.JSoupHttp._ 72 | "do the same thing as the verb jsouped" in { 73 | withResourceServer { _ => 74 | val request = JSoupHtml / "test.html" 75 | 76 | val title1 = Http(request {doc => 77 | doc.title 78 | }) 79 | val title2 = Http(request jsouped {doc => 80 | doc.title 81 | }) 82 | 83 | title1 must be_==(title2) 84 | } 85 | } 86 | } 87 | 88 | "Using the verb as_jsoupedNodeSeq" should { 89 | import dispatch.classic.jsoup.JSoupHttp._ 90 | "use JSoup to retrieve a NodeSeq" in { 91 | withResourceServer { _ => 92 | val request = JSoupHtml / "Human.html" 93 | 94 | val ns = Http(request as_jsoupedNodeSeq) 95 | 96 | (ns \\ "title").text must be_==("Human") 97 | } 98 | } 99 | } 100 | } 101 | 102 | trait ServedByJetty { 103 | val port: Int 104 | val resourceBase: String 105 | 106 | def withResourceServer[A](op: Unit => A): A = { 107 | // Configure Jetty server 108 | val connector = new SelectChannelConnector 109 | connector.setHost("localhost") 110 | connector.setPort(port) 111 | 112 | val handler = new ResourceHandler 113 | handler.setDirectoriesListed(true) 114 | handler.setResourceBase(resourceBase) 115 | val handlers = new HandlerList 116 | handlers.setHandlers(Array(handler, new DefaultHandler)) 117 | 118 | val server = new Server 119 | server.addConnector(connector) 120 | server.setHandler(handlers) 121 | 122 | // Run server for test and then stop 123 | try { 124 | server.start 125 | op(()) 126 | } finally { 127 | server.stop 128 | } 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /mime/src/main/ls/0.8.6.json: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "organization":"net.databinder", 4 | "name":"dispatch-mime", 5 | "version":"0.8.6", 6 | "description":"Support for multipart MIME POSTs", 7 | "site":"http://dispatch.databinder.net/", 8 | "tags":[], 9 | "docs":"", 10 | "licenses": [], 11 | "resolvers": ["http://scala-tools.org/repo-releases"], 12 | "dependencies": [{ 13 | "organization":"org.apache.httpcomponents", 14 | "name": "httpclient", 15 | "version": "4.1.2" 16 | },{ 17 | "organization":"org.apache.httpcomponents", 18 | "name": "httpmime", 19 | "version": "4.1.2" 20 | },{ 21 | "organization":"commons-logging", 22 | "name": "commons-logging", 23 | "version": "1.1.1" 24 | },{ 25 | "organization":"org.apache.james", 26 | "name": "apache-mime4j", 27 | "version": "0.7.1" 28 | }], 29 | "scalas": ["2.8.0","2.8.1","2.8.2","2.9.0","2.9.0-1","2.9.1"], 30 | "sbt": false 31 | } -------------------------------------------------------------------------------- /mime/src/main/ls/0.8.7.json: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "organization":"net.databinder", 4 | "name":"dispatch-mime", 5 | "version":"0.8.7", 6 | "description":"Support for multipart MIME POSTs", 7 | "site":"http://dispatch.databinder.net/", 8 | "tags":[], 9 | "docs":"", 10 | "licenses": [], 11 | "resolvers": ["http://scala-tools.org/repo-releases"], 12 | "dependencies": [{ 13 | "organization":"org.apache.httpcomponents", 14 | "name": "httpclient", 15 | "version": "4.1.2" 16 | },{ 17 | "organization":"org.apache.httpcomponents", 18 | "name": "httpmime", 19 | "version": "4.1.2" 20 | },{ 21 | "organization":"commons-logging", 22 | "name": "commons-logging", 23 | "version": "1.1.1" 24 | },{ 25 | "organization":"org.apache.james", 26 | "name": "apache-mime4j", 27 | "version": "0.7.1" 28 | }], 29 | "scalas": ["2.8.0","2.8.1","2.8.2","2.9.0","2.9.0-1","2.9.1"], 30 | "sbt": false 31 | } -------------------------------------------------------------------------------- /mime/src/main/ls/0.8.8.json: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "organization":"net.databinder", 4 | "name":"dispatch-mime", 5 | "version":"0.8.8", 6 | "description":"Support for multipart MIME POSTs", 7 | "site":"http://dispatch.databinder.net/", 8 | "tags":[], 9 | "docs":"", 10 | "licenses": [{ 11 | "name": "LGPL v3", 12 | "url": "http://www.gnu.org/licenses/lgpl.txt" 13 | }], 14 | "resolvers": ["http://scala-tools.org/repo-releases"], 15 | "dependencies": [{ 16 | "organization":"org.apache.httpcomponents", 17 | "name": "httpclient", 18 | "version": "4.1.3" 19 | },{ 20 | "organization":"org.apache.httpcomponents", 21 | "name": "httpmime", 22 | "version": "4.1.2" 23 | },{ 24 | "organization":"commons-logging", 25 | "name": "commons-logging", 26 | "version": "1.1.1" 27 | },{ 28 | "organization":"org.apache.james", 29 | "name": "apache-mime4j-core", 30 | "version": "0.7.2" 31 | }], 32 | "scalas": ["2.8.0","2.8.1","2.8.2","2.9.0","2.9.0-1","2.9.1"], 33 | "sbt": false 34 | } -------------------------------------------------------------------------------- /mime/src/main/ls/0.8.9.json: -------------------------------------------------------------------------------- 1 | { 2 | "organization" : "net.databinder", 3 | "name" : "dispatch-mime", 4 | "version" : "0.8.9", 5 | "description" : "Support for multipart MIME POSTs", 6 | "site" : "http://dispatch.databinder.net/", 7 | "tags" : [ ], 8 | "docs" : "", 9 | "resolvers" : [ "https://oss.sonatype.org/content/repositories/releases" ], 10 | "dependencies" : [ { 11 | "organization" : "org.apache.httpcomponents", 12 | "name" : "httpclient", 13 | "version" : "4.1.3" 14 | }, { 15 | "organization" : "org.apache.httpcomponents", 16 | "name" : "httpmime", 17 | "version" : "4.1.2" 18 | }, { 19 | "organization" : "commons-logging", 20 | "name" : "commons-logging", 21 | "version" : "1.1.1" 22 | }, { 23 | "organization" : "org.apache.james", 24 | "name" : "apache-mime4j-core", 25 | "version" : "0.7.2" 26 | } ], 27 | "scalas" : [ "2.8.0", "2.8.1", "2.8.2", "2.9.0", "2.9.0-1", "2.9.1", "2.9.1-1", "2.9.2" ], 28 | "licenses" : [ { 29 | "name" : "LGPL v3", 30 | "url" : "http://www.gnu.org/licenses/lgpl.txt" 31 | } ], 32 | "sbt" : false 33 | } -------------------------------------------------------------------------------- /mime/src/main/scala/Mime.scala: -------------------------------------------------------------------------------- 1 | package dispatch.classic.mime 2 | import dispatch.classic._ 3 | import java.io.{FilterOutputStream, OutputStream} 4 | import org.apache.http.HttpEntity 5 | import org.apache.http.entity.HttpEntityWrapper 6 | import org.apache.http.entity.mime.{FormBodyPart, MultipartEntity} 7 | 8 | import java.io.{File, InputStream} 9 | import java.nio.charset.Charset 10 | import org.apache.http.entity.mime.content._ 11 | 12 | /** Mime module for multipart form posting. Note that when using an InputStream generator, 13 | chuncked encoding will be used with no Content-Length header and the stream will be closed 14 | after posting. It is therefore highly recommended that your generator always return a new 15 | stream instance, or a Request descriptor referencing it will fail after its first use. */ 16 | object Mime { 17 | /** Adds multipart operators to Request */ 18 | implicit def Request2ExtendedRequest(r: Request) = new MimeRequestTerms(r) 19 | 20 | type Headers = Map[String, List[String]] 21 | type MultipartBlock[T] = (Headers, InputStream) => T 22 | 23 | /** Request derivative with multipart operators */ 24 | class MimeRequestTerms(r: Request) { 25 | /** Process parts of a multipart response in a block. The block is called once for each part 26 | with a Map[String,List[String]] of its headers and an InputStream of the body. */ 27 | def >--> [T] (multipart_block: MultipartBlock[T]) = r >+> { r2 => 28 | r2 >:> { headers => 29 | r2 >> headers("Content-Type").find { h => true }.map(mime_stream_parser(multipart_block)).get 30 | } 31 | } 32 | 33 | /** Add file to multipart post, will convert other post methods to multipart */ 34 | def <<* (name: String, file: File) = 35 | add(name, new FileBody(file)) 36 | /** Add file with content-type to multipart post, will convert other post methods to multipart */ 37 | def <<* (name: String, file: File, content_type: String) = 38 | add(name, new FileBody(file, content_type)) 39 | 40 | /** Add stream generator to multipart post, will convert other post methods to multipart. */ 41 | def <<* (name: String, file_name: String, stream: () => InputStream, content_type: String) = 42 | add(name, new InputStreamBody(stream(), content_type, file_name)) 43 | 44 | /** Add stream generator with content-type to multipart post, will convert other post methods to multipart */ 45 | def <<* (name: String, file_name: String, stream: () => InputStream) = 46 | add(name, new InputStreamBody(stream(), file_name)) 47 | 48 | /** Add byte array to multipart post, will convert other post methods to multipart */ 49 | def <<* (name: String, file_name: String, bytes: Array[Byte]) = 50 | add(name, new ByteArrayBody(bytes, file_name)) 51 | 52 | /** Add byte array with content-type to multipart post, will convert other post methods to multipart */ 53 | def <<* (name: String, file_name: String, bytes: Array[Byte], content_type: String) = 54 | add(name, new ByteArrayBody(bytes, content_type, file_name)) 55 | 56 | private def mime_ent: Mime.Entity = { 57 | def newent = new MultipartEntity with Mime.Entity { 58 | val charset = Charset.forName(r.defaultCharset) 59 | } 60 | r.body.map { 61 | case ent: Mime.Entity => ent 62 | case orig: FormEntity => newent.add(orig.oauth_params) 63 | case ent => sys.error("trying to add multipart content to entity: " + ent) 64 | } getOrElse newent 65 | } 66 | def add(name: String, content: => ContentBody) = { 67 | val ent = mime_ent 68 | ent.addPart(name, content) 69 | r.POST.copy(body=Some(ent)) 70 | } 71 | /** Add a listener function to be called as bytes are uploaded */ 72 | def >?> (listener_f: ListenerF) = r.copy( 73 | body=Some(new CountingMultipartEntity(mime_ent, listener_f)) 74 | ) 75 | } 76 | /** Post listener function. Called once with the total bytes; the function returned is 77 | called with the bytes uploaded at each kilobyte boundary, and when complete. */ 78 | type ListenerF = Long => Long => Unit 79 | trait Entity extends HttpEntity with FormEntity { 80 | def addPart(name: String, body: ContentBody) 81 | def add(values: Traversable[(String, String)]) = { 82 | for ((name,value) <- values) 83 | addPart(name, new StringBody(value, charset)) 84 | this 85 | } 86 | def oauth_params = Nil 87 | def charset: Charset 88 | } 89 | 90 | def mime_stream_parser[T](multipart_block: MultipartBlock[T])(content_type: String)(stm: InputStream) = { 91 | import org.apache.james.mime4j.stream.{MimeTokenStream, EntityState} 92 | val m = new MimeTokenStream() 93 | m.parseHeadless(stm, content_type) 94 | val empty_headers = Map.empty[String, List[String]] 95 | def walk(state: EntityState, 96 | headers: Headers, 97 | outs: List[T]): List[T] = { 98 | import EntityState._ 99 | state match { 100 | case T_END_OF_STREAM => outs 101 | case T_FIELD => 102 | val added = headers + ((m.getField.getName, 103 | m.getField.getBody :: headers.getOrElse(m.getField.getName, Nil))) 104 | walk(m.next(), added, outs) 105 | case T_BODY => 106 | val output = multipart_block(headers, m.getInputStream) 107 | walk(m.next(), empty_headers, output :: outs) 108 | case state => 109 | walk(m.next(), headers, outs) 110 | } 111 | } 112 | walk(m.getState, empty_headers, Nil).reverse 113 | } 114 | } 115 | 116 | /** Byte-counting entity writer used when a listener function is passed in to the MimeRequest. */ 117 | class CountingMultipartEntity(delegate: Mime.Entity, 118 | listener_f: Mime.ListenerF) extends HttpEntityWrapper(delegate) with Mime.Entity { 119 | def addPart(name: String, body: ContentBody) { delegate.addPart(name, body) } 120 | def charset = delegate.charset 121 | override def writeTo(out: OutputStream) { 122 | import scala.actors.Actor._ 123 | super.writeTo(new FilterOutputStream(out) { 124 | var transferred = 0L 125 | val total = delegate.getContentLength 126 | val sent = listener_f(total) 127 | val listener = actor { loop { react { 128 | case l: Long => { 129 | sent(l) 130 | if (l == total) exit() 131 | } 132 | } } } 133 | override def write(b: Int) { 134 | super.write(b) 135 | transferred += 1 136 | if (transferred % 1024 == 0 || transferred == total) 137 | listener ! transferred 138 | } 139 | }) 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /nio/src/main/ls/0.8.6.json: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "organization":"net.databinder", 4 | "name":"dispatch-nio", 5 | "version":"0.8.6", 6 | "description":"NIO HTTP executor, uses Apache DefaultHttpAsyncClient", 7 | "site":"http://dispatch.databinder.net/", 8 | "tags":[], 9 | "docs":"", 10 | "licenses": [], 11 | "resolvers": ["http://scala-tools.org/repo-releases"], 12 | "dependencies": [{ 13 | "organization":"org.apache.httpcomponents", 14 | "name": "httpclient", 15 | "version": "4.1.2" 16 | },{ 17 | "organization":"org.apache.httpcomponents", 18 | "name": "httpasyncclient", 19 | "version": "4.0-alpha1" 20 | }], 21 | "scalas": ["2.8.0","2.8.1","2.8.2","2.9.0","2.9.0-1","2.9.1"], 22 | "sbt": false 23 | } -------------------------------------------------------------------------------- /nio/src/main/ls/0.8.7.json: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "organization":"net.databinder", 4 | "name":"dispatch-nio", 5 | "version":"0.8.7", 6 | "description":"NIO HTTP executor, uses Apache DefaultHttpAsyncClient", 7 | "site":"http://dispatch.databinder.net/", 8 | "tags":[], 9 | "docs":"", 10 | "licenses": [], 11 | "resolvers": ["http://scala-tools.org/repo-releases"], 12 | "dependencies": [{ 13 | "organization":"org.apache.httpcomponents", 14 | "name": "httpclient", 15 | "version": "4.1.2" 16 | },{ 17 | "organization":"org.apache.httpcomponents", 18 | "name": "httpasyncclient", 19 | "version": "4.0-alpha1" 20 | }], 21 | "scalas": ["2.8.0","2.8.1","2.8.2","2.9.0","2.9.0-1","2.9.1"], 22 | "sbt": false 23 | } -------------------------------------------------------------------------------- /nio/src/main/ls/0.8.8.json: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "organization":"net.databinder", 4 | "name":"dispatch-nio", 5 | "version":"0.8.8", 6 | "description":"NIO HTTP executor, uses Apache DefaultHttpAsyncClient", 7 | "site":"http://dispatch.databinder.net/", 8 | "tags":[], 9 | "docs":"", 10 | "licenses": [{ 11 | "name": "LGPL v3", 12 | "url": "http://www.gnu.org/licenses/lgpl.txt" 13 | }], 14 | "resolvers": ["http://scala-tools.org/repo-releases"], 15 | "dependencies": [{ 16 | "organization":"org.apache.httpcomponents", 17 | "name": "httpclient", 18 | "version": "4.1.3" 19 | },{ 20 | "organization":"org.apache.httpcomponents", 21 | "name": "httpasyncclient", 22 | "version": "4.0-alpha1" 23 | }], 24 | "scalas": ["2.8.0","2.8.1","2.8.2","2.9.0","2.9.0-1","2.9.1"], 25 | "sbt": false 26 | } -------------------------------------------------------------------------------- /nio/src/main/ls/0.8.9.json: -------------------------------------------------------------------------------- 1 | { 2 | "organization" : "net.databinder", 3 | "name" : "dispatch-nio", 4 | "version" : "0.8.9", 5 | "description" : "NIO HTTP executor, uses Apache DefaultHttpAsyncClient", 6 | "site" : "http://dispatch.databinder.net/", 7 | "tags" : [ ], 8 | "docs" : "", 9 | "resolvers" : [ "https://oss.sonatype.org/content/repositories/releases" ], 10 | "dependencies" : [ { 11 | "organization" : "org.apache.httpcomponents", 12 | "name" : "httpclient", 13 | "version" : "4.1.3" 14 | }, { 15 | "organization" : "org.apache.httpcomponents", 16 | "name" : "httpasyncclient", 17 | "version" : "4.0-alpha1" 18 | } ], 19 | "scalas" : [ "2.8.0", "2.8.1", "2.8.2", "2.9.0", "2.9.0-1", "2.9.1", "2.9.1-1", "2.9.2" ], 20 | "licenses" : [ { 21 | "name" : "LGPL v3", 22 | "url" : "http://www.gnu.org/licenses/lgpl.txt" 23 | } ], 24 | "sbt" : false 25 | } -------------------------------------------------------------------------------- /nio/src/main/scala/nio.scala: -------------------------------------------------------------------------------- 1 | package dispatch.classic.nio 2 | 3 | import dispatch.classic.{Callback,Request,ExceptionListener} 4 | import org.apache.http.{HttpHost,HttpRequest,HttpResponse,HttpEntity,HttpException} 5 | import org.apache.http.message.BasicHttpEntityEnclosingRequest 6 | import org.apache.http.protocol._ 7 | import org.apache.http.impl.nio.client.{DefaultHttpAsyncClient,BasicHttpAsyncRequestProducer=>Producer} 8 | import org.apache.http.client.methods._ 9 | import org.apache.http.params.CoreConnectionPNames 10 | import org.apache.http.params.CoreProtocolPNames 11 | import org.apache.http.nio.{ContentDecoder,IOControl,NHttpConnection} 12 | import org.apache.http.nio.entity.{ConsumingNHttpEntity,BufferingNHttpEntity} 13 | import org.apache.http.nio.client.HttpAsyncResponseConsumer 14 | import org.apache.http.nio.concurrent.FutureCallback 15 | import org.apache.http.nio.util.HeapByteBufferAllocator 16 | import java.util.concurrent.Future 17 | import java.io.IOException 18 | 19 | object Http { 20 | val socket_buffer_size = 8 * 1024 21 | } 22 | 23 | class Http extends dispatch.classic.HttpExecutor { 24 | lazy val client = { 25 | val cl = make_client 26 | cl.start() 27 | cl 28 | } 29 | def make_client = new DefaultHttpAsyncClient 30 | 31 | type HttpPackage[T] = dispatch.classic.futures.StoppableFuture[T] 32 | 33 | abstract class StoppableConsumer[T]( 34 | listener: ExceptionListener 35 | ) extends HttpAsyncResponseConsumer[T] { 36 | @volatile var stopping = false 37 | @volatile var exception: Option[Exception] = None 38 | private def setException(e: Exception) { 39 | exception = Some(e) 40 | listener.lift(e) 41 | } 42 | final override def consumeContent(decoder: ContentDecoder, ioctrl: IOControl) { 43 | try { 44 | if (stopping || exception.isDefined) { 45 | ioctrl.shutdown() 46 | cancel() 47 | } 48 | else consume(decoder, ioctrl) 49 | } catch { 50 | case e: Exception => 51 | stopping = true 52 | setException(e) 53 | } 54 | } 55 | @volatile var response: Option[HttpResponse] = None 56 | def responseReceived(res: HttpResponse) { 57 | response = Some(res) 58 | } 59 | def consume(decoder: ContentDecoder, ioctrl: IOControl) 60 | def stop() { stopping = true } 61 | @volatile var result: Option[T] = None 62 | def responseCompleted() { 63 | try { 64 | result = Some(completeResult(response.getOrElse { 65 | sys.error("responseCompleted called but response unset") 66 | })) 67 | } catch { 68 | case e: Exception => setException(e) 69 | } 70 | } 71 | def completeResult(response: HttpResponse): T 72 | // asynchttpclient would a lot rather we return null here than throw an exception 73 | def getResult: T = result.getOrElse(null.asInstanceOf[T]) 74 | def failed(ex: Exception) { 75 | setException(ex) 76 | } 77 | } 78 | class EmptyCallback[T] extends FutureCallback[T] { 79 | def cancelled() { } 80 | def completed(res: T) { } 81 | def failed(ex: Exception) { } 82 | } 83 | class ConsumerFuture[T]( 84 | underlying: Future[T], 85 | consumer: StoppableConsumer[T] 86 | ) extends dispatch.classic.futures.StoppableFuture[T] { 87 | def apply() = { 88 | val res = underlying.get() 89 | consumer.exception.foreach { throw _ } 90 | res 91 | } 92 | def isSet = consumer.exception.isDefined || underlying.isDone 93 | def stop() { 94 | consumer.stop() 95 | underlying.cancel(true) 96 | } 97 | } 98 | /* substitute future used for blocking consumers */ 99 | trait SubstituteFuture[T] extends dispatch.classic.futures.StoppableFuture[T] { 100 | def isSet = true 101 | def stop() { } 102 | } 103 | class ExceptionFuture[T](e: Throwable) extends SubstituteFuture[T] { 104 | def apply() = throw e 105 | } 106 | 107 | def execute[T](host: HttpHost, 108 | credsopt: Option[dispatch.classic.Credentials], 109 | req: HttpRequestBase, 110 | block: HttpResponse => T, 111 | listener: ExceptionListener) = { 112 | credsopt.map { creds => 113 | sys.error("Not yet implemented, but you can force basic auth with as_!") 114 | } getOrElse { 115 | try { 116 | val consumer = new StoppableConsumer[T](listener) { 117 | @volatile var entity: Option[ConsumingNHttpEntity] = None 118 | def consume(decoder: ContentDecoder, ioctrl: IOControl) { synchronized { 119 | entity = entity.orElse { 120 | for { 121 | res <- response 122 | ent <- Option(res.getEntity) 123 | } yield (new BufferingNHttpEntity(ent, new HeapByteBufferAllocator)) 124 | } 125 | entity.map { _.consumeContent(decoder, ioctrl) } 126 | } } 127 | def completeResult(res: HttpResponse) = { 128 | for (ent <- entity) { 129 | res.setEntity(ent) 130 | ent.finish() 131 | } 132 | block(res) 133 | } 134 | def cancel() { 135 | entity.map { _.finish() } 136 | } 137 | } 138 | new ConsumerFuture( 139 | client.execute(new Producer(host, req), consumer, new EmptyCallback[T]), 140 | consumer 141 | ) 142 | } catch { 143 | case e => 144 | listener.lift(e) 145 | new ExceptionFuture(e) 146 | } 147 | } 148 | } 149 | 150 | def executeWithCallback[T](host: HttpHost, credsopt: Option[dispatch.classic.Credentials], 151 | req: HttpRequestBase, callback: Callback[T]) = { 152 | credsopt.map { creds => 153 | sys.error("Not yet implemented, but you can force basic auth with as_!") 154 | } getOrElse { 155 | val ioc = DecodingCallback(callback) 156 | val consumer = new StoppableConsumer[T](callback.listener) { 157 | override def responseReceived(res: HttpResponse) { 158 | response = Some(res) 159 | } 160 | def consume(decoder: ContentDecoder, ioctrl: IOControl) { 161 | ioc.with_decoder(response.get, decoder) 162 | } 163 | def completeResult(response: HttpResponse) = 164 | callback.finish(response) 165 | def cancel() { } 166 | } 167 | new ConsumerFuture( 168 | client.execute(new Producer(host, req), consumer, new EmptyCallback[T]), 169 | consumer 170 | ) 171 | } 172 | } 173 | /** Does nothing, NIO executor always consumes entities it creates */ 174 | def consumeContent(entity: Option[HttpEntity]) { } 175 | def shutdownClient() { 176 | client.shutdown() 177 | } 178 | } 179 | 180 | case class DecodingCallback[T](callback: dispatch.classic.Callback[T]) { 181 | def with_decoder(response: HttpResponse, decoder: ContentDecoder) { 182 | val buffer = java.nio.ByteBuffer.allocate(Http.socket_buffer_size) 183 | val length = decoder.read(buffer) 184 | if (length > 0) 185 | callback.function(response, buffer.array(), length) 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /notes/0.7.0.markdown: -------------------------------------------------------------------------------- 1 | The big news is built-in support for asynchronous HTTP interaction. Although asynchronous execution can be implemented fairly directly in Scala, the new interface guarantees that a thread-safe instance of HttpClient is in use and simplifies exception handling. 2 | 3 | The foundation of this support is in a new module `dispatch-futures`, which has no dependencies and may be used from other libraries. It defines a structural type `dispatch.futures.Futures.Future` that corresponds with `scala.actors.Future`, though its current default implementation is a `java.util.concurrent.Future`. The `dispatch-http` module now depends on `dispatch-futures` and includes a `dispatch.Threads` mix-in for the base `dispatch.Http` class that enables asynchronous interaction. It can be used as follows: 4 | 5 | import dispatch._ 6 | val http = new Http with Threads 7 | val fut_str = http.future(:/("example.com") as_str) 8 | // returns immediately. If we later need that string... 9 | fut_str() // blocks until it is available 10 | 11 | If you *won't* ever access the results from the main thread, you may want to process any exception it throws: 12 | 13 | http on_error { 14 | case e => println(e.getMessage) 15 | } future (:/("example.com") >- { str => 16 | // do something with this string as soon as it's available 17 | }) 18 | 19 | If you want to describe future-interaction with a fully defined request `Handler` like the one returned by `dispatch.meetup.Auth.access_token()`, you can extend it with the new `~>` operator defined on `Handler`: 20 | 21 | http.future(Auth.access_token(consumer, request_token, verifier) ~> { access_token => 22 | // I'm in the vault! 23 | })// <- returns immediately 24 | 25 | And [by request][forum], the `<<` operator on Request is now overloaded to support plain string POSTs. 26 | 27 | [forum]: http://n2.nabble.com/How-to-POST-XML-td4457822.html -------------------------------------------------------------------------------- /notes/0.7.1.markdown: -------------------------------------------------------------------------------- 1 | * No code changes from last release; republishing with [sbt 0.7.1][sbt] for a corrected pom.xml structure. 2 | 3 | [sbt]: http://implicit.ly/simple-build-tool-071 -------------------------------------------------------------------------------- /notes/0.7.2.markdown: -------------------------------------------------------------------------------- 1 | * Use `defaultCharset` charset established by `>\` for POST `<<` and PUT `<<<`, [requested on the forum][charset] 2 | * Handler chaining with the `>+>` operator, to use more than one handler against the same request with overlapping scopes 3 | * Multipart response handling with the [`>-->` operator][mime] in `dispatch-mime`, to support [Riak link-walking and map-reduce][riak] 4 | 5 | [charset]: http://n2.nabble.com/Charset-issue-td4664456.html#a4664456 6 | [riak]: http://blog.basho.com/2010/02/24/link-walking-by-example/ 7 | [mime]: http://databinder.net/dispatch-doc/dispatch/mime/Mime$object.MimeRequest.html -------------------------------------------------------------------------------- /notes/0.7.3.markdown: -------------------------------------------------------------------------------- 1 | * Proxy support contributed by [Max Afonov][maxaf], uses [standard system properties][jp]. 2 | * lift-json dependency updated to [2.0-M4][m4] 3 | * Cross-published for [Scala 2.8.0.RC1][rc1] 4 | 5 | [maxaf]: http://github.com/maxaf 6 | [jp]: http://java.sun.com/javase/6/docs/technotes/guides/net/proxies.html 7 | [m4]: http://implicit.ly/ann-lift-20-m4-release 8 | [rc1]: http://www.scala-lang.org/node/5982 -------------------------------------------------------------------------------- /notes/0.7.4.markdown: -------------------------------------------------------------------------------- 1 | * Deprecate `Http#also` since `Http#x` with `Handler#apply` can do the same thing, but better. 2 | * Deprecate <<< (a: Any) in favor of <<< (s: String) 3 | * Improve error message for [missing entity error][noentity] 4 | * [Encode asterisks][aster] according to the OAuth spec 5 | * Depend on lift-json M5 6 | * Cross-publish for Scala 2.8.0.RC3 7 | 8 | [noentity]: http://databinder.3617998.n2.nabble.com/No-Entity-Error-td5036081.html 9 | [aster]: http://github.com/n8han/Databinder-Dispatch/commit/24b4d7542cc59d582a2a3f06de5b08147ba9e7ce -------------------------------------------------------------------------------- /notes/0.7.5.markdown: -------------------------------------------------------------------------------- 1 | * [Meetup Everywhere API][everywhere] wrapper in [Everywhere.scala][source] 2 | * Cross-built for Scala 2.7.5, 2.7.6, 2.7.7, *and* 2.8.0 3 | 4 | [everywhere]: http://www.meetup.com/meetup_api/everywhere 5 | [source]: http://sourced.implicit.ly/net.databinder/dispatch-meetup/0.7.5/dispatch/Everywhere.scala.html -------------------------------------------------------------------------------- /notes/0.7.6.markdown: -------------------------------------------------------------------------------- 1 | * Enabled redirect handling for all request methods 2 | * Dropped compilation for Scala 2.7.5, 2.7.6 3 | * dispatch-lift-json depends lift-json 2.1-M1 4 | * Support for Google [ClientLogin][ClientLogin] authorization by [Chris Lewis][Chris Lewis] 5 | * Optional Json extractor `??` contributed by [musk][musk] 6 | * Added the optional "oauth_version" parameter to the OAuth Authorization header, because stream.twitter.com [requires it][stream]. 7 | 8 | [ClientLogin]: http://code.google.com/apis/accounts/docs/AuthForInstalledApps.html 9 | [Chris Lewis]: http://github.com/chrislewis 10 | [musk]: http://github.com/musk 11 | [stream]: http://databinder.3617998.n2.nabble.com/OAuth-fails-for-stream-twitter-com-td5448011.html#a5448011 12 | -------------------------------------------------------------------------------- /notes/0.7.7.markdown: -------------------------------------------------------------------------------- 1 | This release introduces an **Amazon S3** interface. 2 | 3 | The `aws-s3` module provides basic support for interacting with 4 | Amazon's S3 service. It supplies Dispatch handlers that sign requests 5 | in accordance with the [S3 authentication specifications][1] and a 6 | convenience class for interacting with S3 Buckets. See the 7 | [module README][readme] for more information. 8 | 9 | [1]: http://docs.amazonwebservices.com/AmazonS3/index.html?RESTAuthentication.html 10 | [readme]: http://github.com/n8han/Databinder-Dispatch/tree/master/aws-s3/#readme 11 | -------------------------------------------------------------------------------- /notes/0.7.8.markdown: -------------------------------------------------------------------------------- 1 | Http 2 | ---- 3 | 4 | Uses `Source.fromInputStream` for [>~][stm] instead of loading stream into 5 | a String first (it's a long story). 6 | 7 | [stm]: http://sourced.implicit.ly/net.databinder/dispatch-http/0.7.8/dispatch/Http.scala.html#20006 8 | 9 | OAuth 10 | ----- 11 | 12 | Transfers oauth_callback in header (with all other OAuth parameters) 13 | rather than post method body. The consumer-only signing application of 14 | the [<@][sign] operator is now deprecated. ([softprops][softprops]) 15 | 16 | [sign]: http://sourced.implicit.ly/net.databinder/dispatch-oauth/0.7.8/dispatch/OAuth.scala.html#21274 17 | [softprops]: https://github.com/softprops/ 18 | 19 | Service Interfaces 20 | ------------------ 21 | 22 | * Meetup: Added support for the Open Events method. 23 | * Twitter: Added a [UserStream][us] interface (sans Json schema for now), 24 | and using api.twitter.com endpoint. 25 | 26 | [me]: http://sourced.implicit.ly/net.databinder/dispatch-meetup/0.7.8/dispatch/Meetup.scala.html#9449 27 | [us]: http://sourced.implicit.ly/net.databinder/dispatch-twitter/0.7.8/dispatch/Streaming.scala.html#9344 28 | 29 | Twine 30 | ----- 31 | 32 | Updated to work from its [own repository][twine], and also to use UserStream. 33 | 34 | [twine]: https://github.com/n8han/dispatch-twine 35 | 36 | Other 37 | ----- 38 | 39 | Source JARs now published to scala-tools. (Nadav Wiener) 40 | -------------------------------------------------------------------------------- /notes/0.8.0.Beta1.markdown: -------------------------------------------------------------------------------- 1 | Welcome to the Dispatch 0.8.0 track! This line depends on HttpComponents 4.1, currently in alpha2. To keep things simple we're just doing beta incrementals until they release a final. 2 | 3 | Unless you need an 0.8 feature *now*, you can save yourself some trouble by staying with the 0.7 line, where all bug fixes as well as all new features that don't require HttpComponents 4.1 will land until the final release of 0.8.0. 4 | 5 | * New `http-gae` module for Google App Engine support, contributed by [maxaf][maxaf] 6 | * Configurable max pooled connections in [dispatch.Threads][threads] 7 | 8 | [maxaf]: http://github.com/maxaf 9 | [threads]: http://sourced.implicit.ly/net.databinder/dispatch-http/0.8.0.Beta1/Threads.scala.html#6804 10 | -------------------------------------------------------------------------------- /notes/0.8.0.Beta3.markdown: -------------------------------------------------------------------------------- 1 | This beta release updates Dispatch's HttpClient dependency to 4.1 2 | final, and adds support for the NIO [HttpAsyncClient][async]. Support 3 | for Scala 2.7.x is dropped in this line. 4 | 5 | [async]: http://hc.apache.org/httpcomponents-asyncclient-dev/index.html 6 | 7 | There are now four standard varieties of http executors available in 8 | different modules: 9 | 10 | dispatch.Http 11 | dispatch.nio.Http 12 | dispatch.thread.Http 13 | dispatch.gae.Http 14 | 15 | Typical usage would be: 16 | 17 | import dispatch._ 18 | val http = new nio.Http 19 | 20 | [Request][Request] itself has been substantially refactored. It is now only a 21 | class with properties and a copy method. All of the request-building 22 | operations ("verbs") are [added via implicit conversions][requests], 23 | putting the core module on the same plane as internal and external 24 | extension modules. 25 | 26 | [Request]: http://sourced.implicit.ly/net.databinder/dispatch-core/0.8.0.Beta3/dispatch/requests.scala.html#9128 27 | [requests]: http://sourced.implicit.ly/net.databinder/dispatch-core/0.8.0.Beta3/dispatch/requests.scala.html#9131 28 | 29 | ### Callbacks and futures 30 | 31 | The `thread` and `nio` executors return [stoppable futures][stoppable] 32 | from their `apply` and other request executions, allowing persistent 33 | connections to be easily closed. A new alternative to request handlers 34 | is [dispatch.Callback][callback], which defines a callback function to 35 | receive request data as it arrives. For example, the fundamental 36 | response callback accepting bytes: 37 | 38 | [stoppable]: http://sourced.implicit.ly/net.databinder/dispatch-futures/0.8.0.Beta3/Futures.scala.html#9092 39 | [callback]: http://sourced.implicit.ly/net.databinder/dispatch-core/0.8.0.Beta3/dispatch/callbacks.scala.html 40 | 41 | h(:/("example.com") ^ (response, bytes, length) => { ... } ) 42 | 43 | There are more convenient callbacks that convert the bytes to strings, 44 | and buffer them into single lines: 45 | 46 | h(:/("example.com") ^-- println) 47 | 48 | You can also return a value from the stoppable future produced, by 49 | defining a finishing function: 50 | 51 | h(:/("example.com") ^-- println ^> { res => "all done!" }) 52 | 53 | But if what you want is a finished response object, it's much easier 54 | to just use the standard response handlers. These work with the NIO 55 | interface by buffering the response in memory. So this works with any 56 | of the executors: 57 | 58 | import dispatch.liftjson.Js._ 59 | h(:/("example.com") ># { js => ... }) 60 | 61 | With the `thread` and `nio` executors, the current thread continues 62 | while the request is performed in a background or IO thread. The 63 | handler function is called with the completed request object (in this 64 | case a JValue) as soon as it is available. 65 | 66 | The [080 branch][twine] of the twine example application uses the new 67 | stoppable future / callback interface with an NIO executor to connect 68 | to Twitter [User Streams](http://dev.twitter.com/pages/user_streams). 69 | 70 | [twine]: https://github.com/n8han/dispatch-twine/blob/080/src/main/scala/twine/Twine.scala#L68 71 | 72 | ### Breaking changes 73 | 74 | The `Http` object is no longer accessible from all modules, so the 75 | implicit conversions that were found there are now in `Request`. 76 | 77 | One change that is likely to break some client code (sorry!) is 78 | the `<<` and `<!` for handlers and `^!` for callbacks 6 | * [HttpsLeniency][hl] trait to trust all SSL certs (men-in-the-middle rejoice) 7 | 8 | [post]: http://sourced.implicit.ly/net.databinder/dispatch-core/0.8.0.Beta4/dispatch/requests.scala.html#26247 9 | [hl]: http://sourced.implicit.ly/net.databinder/dispatch-http/0.8.0.Beta4/dispatch/https.scala.html#9369 10 | -------------------------------------------------------------------------------- /notes/0.8.0.Beta5.markdown: -------------------------------------------------------------------------------- 1 | OAuth signature in prior betas did not account for POST parameters, 2 | [reported][report] in the forum and fixed with this release. Published 3 | for Scala 2.8.0, 2.8.1, and 2.9.0.RC1. 4 | 5 | [report]: http://databinder.3617998.n2.nabble.com/Can-t-post-a-message-from-Dispatch-Twine-branch-080-td6209559.html 6 | -------------------------------------------------------------------------------- /notes/0.8.0.markdown: -------------------------------------------------------------------------------- 1 | Overview 2 | -------- 3 | 4 | This is a significant upgrade to Dispatch, adding preliminary support 5 | for the NIO HttpAsyncClient as a backend. Dispatch's internals have 6 | been refactored to provide the needed flexibility, with some breaking 7 | changes. And lastly, Scala 2.7.x is not supported in Dispatch 0.8.x. 8 | 9 | For these reasons, the Dispatch 0.7 line will continue as long as 10 | needed by client applications. Bug fixes will be released in new 0.7 11 | versions (by request) and 0.7 binaries will be back-published for use 12 | with new versions of Scala. 13 | 14 | Core Module and NIO Support 15 | --------------------------- 16 | 17 | In previous versions of Dispatch the lowest level HTTP module was 18 | `dispatch-http`, which depends on HttpComponents's HttpClient 19 | library. Since 0.8.0 adds support for the HttpComponents's nio-based 20 | [HttpAsyncClient][async], there is a new core module that further abstracts 21 | request definitions and handlers/callbacks from request executors. 22 | 23 | [async]: http://hc.apache.org/httpcomponents-asyncclient-dev/index.html 24 | 25 | * dispatch-core - requests, handlers, callbacks, and abstract 26 | executors 27 | * dispatch-http - Traditional Dispatch client interface, depends on 28 | core, uses threads for futures and callbacks 29 | * dispatch-nio - New Dispatch client interface, depends on core, uses 30 | NIO for futures and callbacks 31 | 32 | The dispatch-nio module is relatively unproven compared to 33 | dispatch-http, and its underlying HttpAsyncClient library is a 34 | prerelease version. Just, fyi. 35 | 36 | API Refactorings 37 | ---------------- 38 | 39 | The module reorganization motivated a number of refactorings that 40 | would have been a good idea any, but some of them will require changes 41 | to client applications. 42 | 43 | ### import Request._ 44 | 45 | If you used to import `Http._` you will probably need to change that 46 | to `Request._`. 47 | 48 | This will enable all the useful implicit conversions that used to be 49 | in dispatch.Http (which is not in the core module). Because these are 50 | in the Request module, most implicit conversions will happen even if 51 | you don't import them. But for migrations the simplest thing is to 52 | change your import; there are no implicits on Http so there is no 53 | reason to leave that import in code. 54 | 55 | ### Parameters are Traversable[(String, String)] 56 | 57 | Sorry, this one's going to sting a little. Parameters used to be 58 | Map[String, Any]. Traversable is more general, no problem there, but 59 | anywhere that your code is passing in a parameter value (e.g. to 60 | `<<` or `<` to handle imperfect inputs that the 4 | xml handler `<>` would choke on. 5 | * [dispatch.Logger](http://sourced.implicit.ly/net.databinder/dispatch-core/0.8.2/executor.scala.html#9104) 6 | trait declares a `warn` method. Any custom 7 | implementations will need to define it. 8 | * The base `HttpExecutor` defines a [shutdown()](http://sourced.implicit.ly/net.databinder/dispatch-core/0.8.2/executor.scala.html#18266) 9 | method which should be 10 | called when finished with any instance. 11 | * `HttpExecutor` defines a `finalize()` method that shuts down the 12 | instance if needed, and issues a warning. 13 | * Removed dynamic call to Configgy logging, which fails on Scala 2.9 14 | if its incompatible classes are on the classpath. 15 | 16 | #### Shutdown Notes 17 | 18 | Although Dispatch 0.7 did not require single-connection client 19 | instances to be shutdown after use, changes in the underlying Apache 20 | HttpComponents library introduced in Dispatch 0.8 effectively require 21 | shutdown for any client instance. 22 | 23 | If an application creates many instances (a few hundred) of a client 24 | and does not shut them down, resources held by the underlying library 25 | will be exhausted and calls to new clients will block indefinitely. To 26 | mitigate the problem Dispatch invokes `shutdown()` from its 27 | `finalize()` method, but an application should not depend on the 28 | garbage collector to keep up with a fluctuating pool of executors. 29 | 30 | If you are using a single-connection executor, Dispatch recommends 31 | that you retain it for a thread of execution (instead of creating new 32 | instances for each call) and shut it down before the thread 33 | terminates. Alternatively, consider sharing a thread-safe client 34 | across your application, which can be easily mixed in to blocking 35 | executors with the [dispatch.thread.Safety](http://sourced.implicit.ly/net.databinder/dispatch-http/0.8.2/thread/thread.scala.html#9391) 36 | trait. 37 | -------------------------------------------------------------------------------- /notes/0.8.3.markdown: -------------------------------------------------------------------------------- 1 | * Fixed regression in NIO handler futures introduced in 0.8.2. An 2 | exception was thrown after otherwise successful buffered stream 3 | request handling. 4 | * Fixed bug in NIO handler that would in some cases allow `apply()` to 5 | return null after exceptions were recorded. Applying the NIO future 6 | will now throw any recorded exceptions, as intended. 7 | -------------------------------------------------------------------------------- /notes/0.8.4.markdown: -------------------------------------------------------------------------------- 1 | Botched release, skip to 0.8.5. 2 | -------------------------------------------------------------------------------- /notes/0.8.5.markdown: -------------------------------------------------------------------------------- 1 | * Converted the build to sbt 0.10, [issue 35](https://github.com/n8han/Databinder-Dispatch/issues/35) 2 | * Extracted API integration modules into external git-dependencies: see updated [project setup documentation](http://dispatch.databinder.net/Project+Setup.html) 3 | * Fixed typing conflict that prevented JSON extension verbs from working with a split request handler, [issue 40](https://github.com/n8han/Databinder-Dispatch/issues/40) 4 | 5 | ### API Module Migration 6 | 7 | To minimize the disruption of this point release, the popular 8 | `dispatch.twitter.Auth` object has been kept in the main distribution, 9 | specifically in the dispatch-oauth module. If you're using only this 10 | object, you won't need to set up a git-dependency on the external 11 | Twitter module. 12 | -------------------------------------------------------------------------------- /notes/0.8.6.markdown: -------------------------------------------------------------------------------- 1 | ### Features 2 | 3 | * Support for proxy authentication, contributed by [dcsobral](https://github.com/dispatch/dispatch/commit/826c80298067d5a4f93e76dc20d58154cf088ff7) 4 | * Easier to use [header handler >:+](http://dispatch.databinder.net/Two+Handlers+Are+Better+Than+One.html) 5 | 6 | ### Fixes 7 | 8 | * Explicit return type for `make_client` to fix override problem, 9 | contributed by [ostewart](https://github.com/dispatch/dispatch/commit/af44119013455a6bcc4eff293fe42a3b7530f933) 10 | * Avoid slow stream reader usage in json parsing, contributed by [pomu0325](https://github.com/dispatch/dispatch/pull/49) 11 | 12 | ### Dependencies 13 | 14 | * dispatch-gae updated to 1.5.5 of appengine-api 15 | * dispatch-http updated to 4.1.2 of Apache httpclient 16 | * dispatch-lift-json is now an [external source dependency](https://github.com/dispatch/dispatch-lift-json). See the [project setup docs](http://dispatch.databinder.net/Project+Setup.html) for usage information. 17 | 18 | ### News 19 | 20 | The Dispatch repository has moved: [https://github.com/dispatch/dispatch](https://github.com/dispatch/dispatch) 21 | -------------------------------------------------------------------------------- /notes/0.8.7.markdown: -------------------------------------------------------------------------------- 1 | ### Breaking Changes 2 | 3 | If using the `` verb to parse XML, you must now bring it into scope 4 | explicitly: 5 | 6 | import dispatch.XhtmlParsing._ 7 | 8 | There's a good reason for this! (See below.) 9 | 10 | ### JSoup and TagSoup modules 11 | 12 | Thanks to a [big contribution from daros][soup], Dispatch now 13 | integrates with parsers that can handle real-world HTML. The handler 14 | verb `` is used across all HTML parsers, resolved by its imported 15 | implicit conversions. For usage instructions and examples, see the 16 | Dispatch documentation for [TagSoup][tagsoup] and [JSoup][jsoup]. 17 | 18 | [soup]: https://github.com/dispatch/dispatch/pull/62 19 | [tagsoup]: http://dispatch.databinder.net/TagSoup.html 20 | [jsoup]: http://dispatch.databinder.net/JSoup.html 21 | 22 | ### Support byte arrays for POST 23 | 24 | This [contribution by dyross][ary] (no relation?) adds 25 | 26 | def << (contents: Array[Byte]) 27 | 28 | to the standard set of request verbs. 29 | 30 | [ary]: https://github.com/dispatch/dispatch/pull/63 31 | -------------------------------------------------------------------------------- /notes/0.8.8.markdown: -------------------------------------------------------------------------------- 1 | * JsonParser improvements (pull requests [66][66], 2 | [68][68]) { [elehack][elehack] } 3 | * Improved system-property handling for proxies (pull request [71][71]) { 4 | [iron9light][iron9light] } 5 | * Correction to mime4j dependency, which was failing to resolve for 6 | Maven builds; now points directly to the apache-mime4j-core. 7 | * Updated the httpclient dependency to version 4.1.3. 8 | 9 | 10 | [elehack]: https://github.com/elehack 11 | [iron9light]: https://github.com/iron9light 12 | [68]: https://github.com/dispatch/dispatch/pull/68 13 | [66]: https://github.com/dispatch/dispatch/pull/66 14 | [71]: https://github.com/dispatch/dispatch/pull/71 15 | -------------------------------------------------------------------------------- /notes/0.8.9.markdown: -------------------------------------------------------------------------------- 1 | ### Forwards-compatibility with Dispatch 0.9+ (reboot) 2 | 3 | This release **changes the base package** of the 0.8 line from 4 | `dispatch` to `dispatch.classic`, so that Dispatch 0.8.9 and Dispatch 5 | 0.9.x may be used on the same classpath. 6 | 7 | This will require code changes in any project using Dispatch 0.8. 8 | Typically, it will require changes to imports and any fully qualified 9 | classname; e.g. `dispatch.Http` becomes `dispatch.classic.Http`. 10 | 11 | ### Enhancements 12 | 13 | * Integration with the Foursquare API [contributed by adamdecaf][fsq] 14 | * Ability to use a byte-array to define a multipart-post body, 15 | [contributed by steppenwells][byte] 16 | * Support for OPTIONS requests, [contributed by stephenjudkins][opt] 17 | * Ability to specify content-type with `<<<`, 18 | [contributed by telmomenezes][typ] 19 | 20 | [fsq]: https://github.com/dispatch/dispatch/pull/75 21 | [byte]: https://github.com/dispatch/dispatch/pull/74 22 | [opt]: https://github.com/dispatch/dispatch/pull/73 23 | [typ]: https://github.com/dispatch/dispatch/pull/72 24 | -------------------------------------------------------------------------------- /notes/about.markdown: -------------------------------------------------------------------------------- 1 | [Dispatch][1] is a library for HTTP interaction, from asynchronous GETs to multi-part OAuth-enticated POSTs. 2 | 3 | [1]: http://dispatch-classic.databinder.net/ 4 | -------------------------------------------------------------------------------- /oauth/src/main/ls/0.8.6.json: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "organization":"net.databinder", 4 | "name":"dispatch-oauth", 5 | "version":"0.8.6", 6 | "description":"OAuth 1.0a signing for Dispatch requests", 7 | "site":"http://dispatch.databinder.net/", 8 | "tags":[], 9 | "docs":"", 10 | "licenses": [], 11 | "resolvers": ["http://scala-tools.org/repo-releases"], 12 | "dependencies": [{ 13 | "organization":"org.apache.httpcomponents", 14 | "name": "httpclient", 15 | "version": "4.1.2" 16 | }], 17 | "scalas": ["2.8.0","2.8.1","2.8.2","2.9.0","2.9.0-1","2.9.1"], 18 | "sbt": false 19 | } -------------------------------------------------------------------------------- /oauth/src/main/ls/0.8.7.json: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "organization":"net.databinder", 4 | "name":"dispatch-oauth", 5 | "version":"0.8.7", 6 | "description":"OAuth 1.0a signing for Dispatch requests", 7 | "site":"http://dispatch.databinder.net/", 8 | "tags":[], 9 | "docs":"", 10 | "licenses": [], 11 | "resolvers": ["http://scala-tools.org/repo-releases"], 12 | "dependencies": [{ 13 | "organization":"org.apache.httpcomponents", 14 | "name": "httpclient", 15 | "version": "4.1.2" 16 | }], 17 | "scalas": ["2.8.0","2.8.1","2.8.2","2.9.0","2.9.0-1","2.9.1"], 18 | "sbt": false 19 | } -------------------------------------------------------------------------------- /oauth/src/main/ls/0.8.8.json: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "organization":"net.databinder", 4 | "name":"dispatch-oauth", 5 | "version":"0.8.8", 6 | "description":"OAuth 1.0a signing for Dispatch requests", 7 | "site":"http://dispatch.databinder.net/", 8 | "tags":[], 9 | "docs":"", 10 | "licenses": [{ 11 | "name": "LGPL v3", 12 | "url": "http://www.gnu.org/licenses/lgpl.txt" 13 | }], 14 | "resolvers": ["http://scala-tools.org/repo-releases"], 15 | "dependencies": [{ 16 | "organization":"org.apache.httpcomponents", 17 | "name": "httpclient", 18 | "version": "4.1.3" 19 | }], 20 | "scalas": ["2.8.0","2.8.1","2.8.2","2.9.0","2.9.0-1","2.9.1"], 21 | "sbt": false 22 | } -------------------------------------------------------------------------------- /oauth/src/main/ls/0.8.9.json: -------------------------------------------------------------------------------- 1 | { 2 | "organization" : "net.databinder", 3 | "name" : "dispatch-oauth", 4 | "version" : "0.8.9", 5 | "description" : "OAuth 1.0a signing for Dispatch requests", 6 | "site" : "http://dispatch.databinder.net/", 7 | "tags" : [ ], 8 | "docs" : "", 9 | "resolvers" : [ "https://oss.sonatype.org/content/repositories/releases" ], 10 | "dependencies" : [ { 11 | "organization" : "org.apache.httpcomponents", 12 | "name" : "httpclient", 13 | "version" : "4.1.3" 14 | } ], 15 | "scalas" : [ "2.8.0", "2.8.1", "2.8.2", "2.9.0", "2.9.0-1", "2.9.1", "2.9.1-1", "2.9.2" ], 16 | "licenses" : [ { 17 | "name" : "LGPL v3", 18 | "url" : "http://www.gnu.org/licenses/lgpl.txt" 19 | } ], 20 | "sbt" : false 21 | } -------------------------------------------------------------------------------- /oauth/src/main/scala/OAuth.scala: -------------------------------------------------------------------------------- 1 | package dispatch.classic.oauth 2 | import dispatch.classic._ 3 | import Request.{encode_%, decode_%} 4 | 5 | import collection.Map 6 | import collection.immutable.{TreeMap, Map=>IMap} 7 | 8 | import javax.crypto 9 | import java.net.URI 10 | 11 | import org.apache.http.protocol.HTTP.UTF_8 12 | 13 | case class Consumer(key: String, secret: String) 14 | case class Token(value: String, secret: String) 15 | object Token { 16 | def apply[T <: Any](m: Map[String, T]): Option[Token] = List("oauth_token", "oauth_token_secret").flatMap(m.get) match { 17 | case value :: secret :: Nil => Some(Token(value.toString, secret.toString)) 18 | case _ => None 19 | } 20 | } 21 | 22 | /** Import this object's methods to add signing operators to dispatch.Request */ 23 | object OAuth { 24 | /** @return oauth parameter map including signature */ 25 | def sign(method: String, url: String, user_params: Map[String, Any], consumer: Consumer, 26 | token: Option[Token], verifier: Option[String], callback: Option[String]) = { 27 | val oauth_params = IMap( 28 | "oauth_consumer_key" -> consumer.key, 29 | "oauth_signature_method" -> "HMAC-SHA1", 30 | "oauth_timestamp" -> (System.currentTimeMillis / 1000).toString, 31 | "oauth_nonce" -> System.nanoTime.toString, 32 | "oauth_version" -> "1.0" 33 | ) ++ token.map { "oauth_token" -> _.value } ++ 34 | verifier.map { "oauth_verifier" -> _ } ++ 35 | callback.map { "oauth_callback" -> _ } 36 | 37 | val encoded_ordered_params = ( 38 | new TreeMap[String, String] ++ (user_params ++ oauth_params map %%) 39 | ) map { case (k, v) => k + "=" + v } mkString "&" 40 | 41 | val message = 42 | %%(method.toUpperCase :: url :: encoded_ordered_params :: Nil) 43 | 44 | val SHA1 = "HmacSHA1" 45 | val key_str = %%(consumer.secret :: (token map { _.secret } getOrElse "") :: Nil) 46 | val key = new crypto.spec.SecretKeySpec(bytes(key_str), SHA1) 47 | val sig = { 48 | val mac = crypto.Mac.getInstance(SHA1) 49 | mac.init(key) 50 | new String(Request.encode_base64(mac.doFinal(bytes(message)))) 51 | } 52 | oauth_params + ("oauth_signature" -> sig) 53 | } 54 | 55 | /** Out-of-band callback code */ 56 | val oob = "oob" 57 | 58 | /** Map with oauth_callback set to the given url */ 59 | def callback(url: String) = IMap("oauth_callback" -> url) 60 | 61 | //normalize to OAuth percent encoding 62 | private def %% (str: String): String = { 63 | val remaps = ("+", "%20") :: ("%7E", "~") :: ("*", "%2A") :: Nil 64 | (encode_%(str) /: remaps) { case (str, (a, b)) => str.replace(a,b) } 65 | } 66 | private def %% (s: Seq[String]): String = s map %% mkString "&" 67 | private def %% (t: (String, Any)): (String, String) = (%%(t._1), %%(t._2.toString)) 68 | 69 | private def bytes(str: String) = str.getBytes(UTF_8) 70 | 71 | /** Add OAuth operators to dispatch.Request */ 72 | implicit def Request2RequestSigner(r: Request) = new RequestSigner(r) 73 | /** Add String conversion since Http#str2req implicit will not chain. */ 74 | implicit def Request2RequestSigner(r: String) = new RequestSigner(new Request(r)) 75 | 76 | class RequestSigner(r: Request) { 77 | @deprecated("use <@ (consumer, callback) to pass the callback in the header for a request-token request", "0.7.8") 78 | def <@ (consumer: Consumer): Request = sign(consumer, None, None, None) 79 | /** sign a request with a callback, e.g. a request-token request */ 80 | def <@ (consumer: Consumer, callback: String): Request = sign(consumer, None, None, Some(callback)) 81 | /** sign a request with a consumer, token, and verifier, e.g. access-token request */ 82 | def <@ (consumer: Consumer, token: Token, verifier: String): Request = 83 | sign(consumer, Some(token), Some(verifier), None) 84 | /** sign a request with a consumer and a token, e.g. an OAuth-signed API request */ 85 | def <@ (consumer: Consumer, token: Token): Request = sign(consumer, Some(token), None, None) 86 | 87 | /** add token value as a query string parameter, for user authorization redirects */ 88 | def with_token (token: Token) = // implicit use of < token.value) :: Nil) 90 | 91 | /** Sign request by reading Post (<<) and query string parameters */ 92 | private def sign(consumer: Consumer, token: Option[Token], verifier: Option[String], callback: Option[String]) = { 93 | val oauth_url = r.to_uri.toString.split('?')(0) 94 | val query_params = split_decode(r.to_uri.getRawQuery) 95 | val form_params = r.body.toList.flatMap { 96 | case ent: FormEntity => ent.oauth_params 97 | case _ => Nil 98 | } 99 | val oauth_params = OAuth.sign(r.method, oauth_url, 100 | query_params ++ form_params, 101 | consumer, token, verifier, callback) 102 | r <:< IMap("Authorization" -> ("OAuth " + oauth_params.map { 103 | case (k, v) => (encode_%(k)) + "=\"%s\"".format(encode_%(v)) 104 | }.mkString(",") )) 105 | } 106 | 107 | def >% [T] (block: IMap[String, String] => T) = r >- ( split_decode andThen block ) 108 | def as_token = r >% { Token(_).getOrElse { sys.error("Token parameters not found in given map") } } 109 | 110 | val split_decode: (String => IMap[String, String]) = { 111 | case null => IMap.empty 112 | case query => IMap.empty ++ query.trim.split('&').map { nvp => 113 | nvp.split("=").map(decode_%) match { 114 | case Array(name) => name -> "" 115 | case Array(name, value) => name -> value 116 | } 117 | } 118 | } 119 | } 120 | 121 | } 122 | -------------------------------------------------------------------------------- /oauth/src/main/scala/foursquare.scala: -------------------------------------------------------------------------------- 1 | package dispatch.classic.foursquare 2 | import dispatch.classic._ 3 | 4 | object Auth { 5 | private val svc: Request = :/ ("api.foursquare.com") secure 6 | 7 | def apply(key: String, secret: String)(request: Request): Request = 8 | apply((key, secret))(request) 9 | 10 | def apply(creds: (String, String))(request: Request): Request = { 11 | svc <& request < creds._1, "client_secret" -> creds._2) 12 | } 13 | 14 | } 15 | -------------------------------------------------------------------------------- /oauth/src/main/scala/twitter.scala: -------------------------------------------------------------------------------- 1 | package dispatch.classic.twitter 2 | import dispatch.classic._ 3 | 4 | import oauth._ 5 | import oauth.OAuth._ 6 | 7 | object Auth { 8 | val svc = :/("api.twitter.com") / "oauth" 9 | 10 | /** Get a request token with no callback URL, out-of-band 11 | * authorization assumed */ 12 | def request_token(consumer: Consumer): Handler[Token] = 13 | request_token(consumer, OAuth.oob) 14 | 15 | def request_token(consumer: Consumer, callback_url: String) = 16 | svc.secure.POST / "request_token" <@ (consumer, callback_url) as_token 17 | 18 | def authorize_url(token: Token) = 19 | svc / "authorize" with_token token 20 | def authenticate_url(token: Token) = 21 | svc / "authenticate" with_token token 22 | 23 | def access_token(consumer: Consumer, token: Token, verifier: String) = 24 | svc.secure.POST / "access_token" <@ (consumer, token, verifier) >% { 25 | m => (Token(m).get, m("user_id"), m("screen_name")) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /oauth/src/test/scala/OAuthSpec.scala: -------------------------------------------------------------------------------- 1 | import org.specs2.mutable.Specification 2 | 3 | object OAuthSpec extends Specification { 4 | import dispatch.classic._ 5 | import oauth._ 6 | import Request._ 7 | import OAuth._ 8 | 9 | val svc = :/("term.ie") / "oauth" / "example" 10 | val consumer = Consumer("key", "secret") 11 | 12 | "OAuth test host" should { 13 | "echo parameters from protected service" in { 14 | pending 15 | val h = new Http 16 | val request_token = h(svc / "request_token.php" <@ consumer as_token) 17 | val access_token = h(svc / "access_token" <@ (consumer, request_token) as_token) 18 | val payload = Map("identité" -> "caché", "identity" -> "hidden", "アイデンティティー" -> "秘密", 19 | "pita" -> "-._~*") 20 | h( 21 | svc / "echo_api.php" <% { 22 | _ must_== (payload) 23 | } 24 | ) 25 | success 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /oauth/src/test/scala/foursquare.scala: -------------------------------------------------------------------------------- 1 | package dispatch.classic.foursquare 2 | 3 | import dispatch.classic._ 4 | import org.specs2.mutable.Specification 5 | 6 | object FoursquareSpec extends Specification { 7 | 8 | private val req = /\ / "v2" / "venues" / "search" < "123.123,456.456", "v" -> "19700101") 9 | 10 | "A basic foursquare request that requires auth" should { 11 | "be able to be formed normally" in { 12 | val builtReq = Auth(("key", "secret"))(req).to_uri.toString 13 | 14 | builtReq.contains("api.foursquare.com/v2/venues/search") must_== true 15 | builtReq.contains("client_id=key") must_== true 16 | builtReq.contains("client_secret=secret") must_== true 17 | builtReq.contains("ll=123.123%2C456.456") must_== true 18 | builtReq.contains("v=19700101") must_== true 19 | builtReq.contains("https://") must_== true 20 | } 21 | 22 | "be able to be formed normally through other apply" in { 23 | val builtReq = Auth("key", "secret")(req).to_uri.toString 24 | 25 | builtReq.contains("api.foursquare.com/v2/venues/search") must_== true 26 | builtReq.contains("client_id=key") must_== true 27 | builtReq.contains("client_secret=secret") must_== true 28 | builtReq.contains("ll=123.123%2C456.456") must_== true 29 | builtReq.contains("v=19700101") must_== true 30 | builtReq.contains("https://") must_== true 31 | } 32 | } 33 | } 34 | 35 | -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=0.13.1 2 | -------------------------------------------------------------------------------- /project/build.scala: -------------------------------------------------------------------------------- 1 | import sbt._ 2 | import Keys._ 3 | 4 | object Dispatch extends Build { 5 | val shared = Defaults.defaultSettings ++ ls.Plugin.lsSettings ++ Seq( 6 | organization := "net.databinder", 7 | version := "0.8.10", 8 | scalaVersion := "2.10.4", 9 | parallelExecution in Test := false, 10 | testOptions in Test += Tests.Argument(TestFrameworks.Specs2, "sequential", "true"), 11 | scalacOptions ++= "-deprecation" :: Nil, 12 | crossScalaVersions := 13 | Seq("2.9.0", "2.9.0-1", "2.9.1", "2.9.1-1", "2.9.2", "2.9.3", "2.10.4", "2.11.1"), 14 | libraryDependencies <++= (scalaVersion) { sv => Seq( 15 | sv.split("[.-]").toList match { 16 | case "2" :: "9" :: _ => 17 | "org.specs2" % "specs2_2.9.2" % "1.12.4" % "test" 18 | case _ => 19 | "org.specs2" %% "specs2" % "2.3.12" % "test" 20 | }) 21 | }, 22 | publishMavenStyle := true, 23 | publishTo <<= version { (v: String) => 24 | val nexus = "https://oss.sonatype.org/" 25 | if (v.trim.endsWith("SNAPSHOT")) 26 | Some("snapshots" at nexus + "content/repositories/snapshots") 27 | else 28 | Some("releases" at nexus + "service/local/staging/deploy/maven2") 29 | }, 30 | credentials += Credentials(Path.userHome / ".ivy2" / ".credentials"), 31 | homepage := 32 | Some(new java.net.URL("http://dispatch-classic.databinder.net/")), 33 | publishArtifact in Test := false, 34 | licenses := Seq("LGPL v3" -> url("http://www.gnu.org/licenses/lgpl.txt")), 35 | pomExtra := ( 36 | 37 | git@github.com:dispatch/reboot.git 38 | scm:git:git@github.com:dispatch/reboot.git 39 | 40 | 41 | 42 | n8han 43 | Nathan Hamblen 44 | http://twitter.com/n8han 45 | 46 | ) 47 | ) 48 | val httpShared = shared ++ Seq( 49 | libraryDependencies += 50 | "org.apache.httpcomponents" % "httpclient" % "4.1.3" 51 | ) 52 | lazy val dispatch = 53 | Project("Dispatch", file("."), settings = shared ++ Seq( 54 | sources in (Compile, doc) <<= 55 | (thisProjectRef, buildStructure) flatMap (aggregateTask(sources)), 56 | dependencyClasspath in (Compile, doc) <<= 57 | (thisProjectRef, buildStructure) flatMap 58 | aggregateTask(dependencyClasspath), 59 | ls.Plugin.LsKeys.skipWrite := true 60 | )) aggregate( 61 | futures, core, http, nio, mime, json, http_json, oauth, gae, tagsoup, 62 | jsoup 63 | ) 64 | lazy val futures = 65 | Project("dispatch-futures", file("futures"), settings = shared ++ Seq( 66 | description := "Common interface to Java and Scala futures", 67 | // https://github.com/harrah/xsbt/issues/85#issuecomment-1687483 68 | unmanagedClasspath in Compile += Attributed.blank(new java.io.File("doesnotexist")), 69 | actorsDependency 70 | )) 71 | lazy val core = 72 | Project("dispatch-core", file("core"), settings = httpShared ++ Seq( 73 | description := 74 | "Core interfaces, applied by dispatch-http and dispatch-nio executors", 75 | xmlDependency 76 | )) 77 | lazy val http = 78 | Project("dispatch-http", file("http"), settings = httpShared ++ Seq( 79 | description := 80 | "Standard HTTP executor, uses Apache DefaultHttpClient", 81 | sources in Test := { 82 | if (scalaVersion.value.startsWith("2.9.")) Nil 83 | else (sources in Test).value 84 | } 85 | )) dependsOn( 86 | core, futures) 87 | lazy val gae = 88 | Project("dispatch-gae", file("http-gae"), settings = httpShared ++ Seq( 89 | description := 90 | "Executor with a modified Apache HttpClient for Google App Engine", 91 | libraryDependencies += 92 | "com.google.appengine" % "appengine-api-1.0-sdk" % "1.5.5" 93 | )) dependsOn(http) 94 | lazy val nio = 95 | Project("dispatch-nio", file("nio"), settings = httpShared ++ Seq( 96 | description := 97 | "NIO HTTP executor, uses Apache DefaultHttpAsyncClient", 98 | libraryDependencies += 99 | ("org.apache.httpcomponents" % "httpasyncclient" % "4.0-alpha1") 100 | )) dependsOn(core, futures) 101 | lazy val mime = 102 | Project("dispatch-mime", file("mime"), settings = httpShared ++ Seq( 103 | description := 104 | "Support for multipart MIME POSTs", 105 | libraryDependencies ++= Seq( 106 | "org.apache.httpcomponents" % "httpmime" % "4.1.2" intransitive(), 107 | "commons-logging" % "commons-logging" % "1.1.1", 108 | "org.apache.james" % "apache-mime4j-core" % "0.7.2" 109 | ), 110 | actorsDependency 111 | )) dependsOn(core) 112 | lazy val json = 113 | Project("dispatch-json", file("json"), settings = shared ++ Seq( 114 | description := "A JSON parser", 115 | sources in Test := { 116 | if (scalaVersion.value.startsWith("2.9.")) Nil 117 | else (sources in Test).value 118 | }, 119 | // https://github.com/harrah/xsbt/issues/85#issuecomment-1687483 120 | unmanagedClasspath in Compile += Attributed.blank(new java.io.File("doesnotexist")), 121 | parserDependency 122 | )) 123 | lazy val http_json = 124 | Project("dispatch-http-json", file("http+json"), 125 | settings = httpShared ++ Seq( 126 | description := "Adds JSON handler verbs to Dispatch" 127 | )) dependsOn(core, json) 128 | lazy val oauth = 129 | Project("dispatch-oauth", file("oauth"), settings = httpShared ++ Seq( 130 | description := "OAuth 1.0a signing for Dispatch requests" 131 | )) dependsOn( 132 | core, http) 133 | lazy val tagsoup = 134 | Project("dispatch-tagsoup", file("tagsoup"), settings = httpShared ++ Seq( 135 | description := "Adds TagSoup handler verbs to Dispatch", 136 | libraryDependencies ++= Seq( 137 | "org.ccil.cowan.tagsoup" % "tagsoup" % "1.2.1", 138 | "org.eclipse.jetty.aggregate" % "jetty-server" % "7.5.4.v20111024" % "test" 139 | ) 140 | )) dependsOn(core, http) 141 | lazy val jsoup = 142 | Project("dispatch-jsoup", file("jsoup"), settings = httpShared ++ Seq( 143 | description := "Adds JSoup handler verbs to Dispatch", 144 | libraryDependencies ++= Seq( 145 | "org.jsoup" % "jsoup" % "1.6.1", 146 | "org.eclipse.jetty.aggregate" % "jetty-server" % "7.5.4.v20111024" % "test" 147 | ) 148 | )) dependsOn(core, http) 149 | 150 | def aggregateTask[T](key: TaskKey[Seq[T]]) 151 | (proj: ProjectRef, struct: BuildStructure) = { 152 | def collectProjects(op: ResolvedProject => Seq[ProjectRef]) 153 | (projRef: ProjectRef, 154 | struct: BuildStructure): Seq[ProjectRef] = { 155 | val delg = Project.getProject(projRef, struct).toSeq.flatMap(op) 156 | // Dependencies/aggregates might have their own dependencies/aggregates 157 | // so go recursive and do distinct. 158 | delg.flatMap(ref => ref +: collectProjects(op)(ref, struct)).distinct 159 | } 160 | collectProjects(_.aggregate)(proj, struct).flatMap( 161 | key in (_, Compile, doc) get struct.data 162 | ).join.map(_.flatten) 163 | } 164 | 165 | lazy val actorsDependency = libraryDependencies <<= (libraryDependencies, scalaVersion){ 166 | (dependencies, scalaVersion) => 167 | if(scalaVersion.startsWith("2.10") || scalaVersion.startsWith("2.11")) 168 | ("org.scala-lang" % "scala-actors" % scalaVersion) +: dependencies 169 | else 170 | dependencies 171 | } 172 | 173 | lazy val xmlDependency = libraryDependencies <<= (libraryDependencies, scalaVersion){ 174 | (dependencies, scalaVersion) => 175 | if(scalaVersion.startsWith("2.11")) 176 | ("org.scala-lang.modules" %% "scala-xml" % "1.0.1") +: dependencies 177 | else 178 | dependencies 179 | } 180 | lazy val parserDependency = libraryDependencies <<= (libraryDependencies, scalaVersion){ 181 | (dependencies, scalaVersion) => 182 | if(scalaVersion.startsWith("2.11")) 183 | ("org.scala-lang.modules" %% "scala-parser-combinators" % "1.0.1") +: dependencies 184 | else 185 | dependencies 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | resolvers ++= Seq( 2 | "less is" at "http://repo.lessis.me", 3 | "coda" at "http://repo.codahale.com" 4 | ) 5 | 6 | addSbtPlugin("me.lessis" % "ls-sbt" % "0.1.3") 7 | 8 | scalacOptions += "-deprecation" 9 | -------------------------------------------------------------------------------- /tagsoup/src/main/ls/0.8.7.json: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "organization":"net.databinder", 4 | "name":"dispatch-tagsoup", 5 | "version":"0.8.7", 6 | "description":"Adds TagSoup handler verbs to Dispatch", 7 | "site":"http://dispatch.databinder.net/", 8 | "tags":[], 9 | "docs":"", 10 | "licenses": [], 11 | "resolvers": ["http://scala-tools.org/repo-releases"], 12 | "dependencies": [{ 13 | "organization":"org.apache.httpcomponents", 14 | "name": "httpclient", 15 | "version": "4.1.2" 16 | },{ 17 | "organization":"org.ccil.cowan.tagsoup", 18 | "name": "tagsoup", 19 | "version": "1.2.1" 20 | }], 21 | "scalas": ["2.8.0","2.8.1","2.8.2","2.9.0","2.9.0-1","2.9.1"], 22 | "sbt": false 23 | } -------------------------------------------------------------------------------- /tagsoup/src/main/ls/0.8.8.json: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "organization":"net.databinder", 4 | "name":"dispatch-tagsoup", 5 | "version":"0.8.8", 6 | "description":"Adds TagSoup handler verbs to Dispatch", 7 | "site":"http://dispatch.databinder.net/", 8 | "tags":[], 9 | "docs":"", 10 | "licenses": [{ 11 | "name": "LGPL v3", 12 | "url": "http://www.gnu.org/licenses/lgpl.txt" 13 | }], 14 | "resolvers": ["http://scala-tools.org/repo-releases"], 15 | "dependencies": [{ 16 | "organization":"org.apache.httpcomponents", 17 | "name": "httpclient", 18 | "version": "4.1.3" 19 | },{ 20 | "organization":"org.ccil.cowan.tagsoup", 21 | "name": "tagsoup", 22 | "version": "1.2.1" 23 | }], 24 | "scalas": ["2.8.0","2.8.1","2.8.2","2.9.0","2.9.0-1","2.9.1"], 25 | "sbt": false 26 | } -------------------------------------------------------------------------------- /tagsoup/src/main/ls/0.8.9.json: -------------------------------------------------------------------------------- 1 | { 2 | "organization" : "net.databinder", 3 | "name" : "dispatch-tagsoup", 4 | "version" : "0.8.9", 5 | "description" : "Adds TagSoup handler verbs to Dispatch", 6 | "site" : "http://dispatch.databinder.net/", 7 | "tags" : [ ], 8 | "docs" : "", 9 | "resolvers" : [ "https://oss.sonatype.org/content/repositories/releases" ], 10 | "dependencies" : [ { 11 | "organization" : "org.apache.httpcomponents", 12 | "name" : "httpclient", 13 | "version" : "4.1.3" 14 | }, { 15 | "organization" : "org.ccil.cowan.tagsoup", 16 | "name" : "tagsoup", 17 | "version" : "1.2.1" 18 | } ], 19 | "scalas" : [ "2.8.0", "2.8.1", "2.8.2", "2.9.0", "2.9.0-1", "2.9.1", "2.9.1-1", "2.9.2" ], 20 | "licenses" : [ { 21 | "name" : "LGPL v3", 22 | "url" : "http://www.gnu.org/licenses/lgpl.txt" 23 | } ], 24 | "sbt" : false 25 | } -------------------------------------------------------------------------------- /tagsoup/src/main/scala/TagSoupHttp.scala: -------------------------------------------------------------------------------- 1 | package dispatch.classic.tagsoup 2 | 3 | import dispatch.classic.{HandlerVerbs, Request} 4 | import xml.parsing.NoBindingFactoryAdapter 5 | 6 | import java.io.InputStreamReader 7 | import org.xml.sax.InputSource 8 | import org.ccil.cowan.tagsoup.jaxp.SAXFactoryImpl 9 | 10 | trait ImplicitTagSoupHandlers { 11 | implicit def handlerToTagSoupHandlers(h: HandlerVerbs) = new TagSoupHandlers(h) 12 | implicit def requestToTagSoupHandlers(req: Request) = new TagSoupHandlers(req); 13 | implicit def stringToTagSoupHandlers(str: String) = new TagSoupHandlers(new Request(str)); 14 | } 15 | 16 | object TagSoupHttp extends ImplicitTagSoupHandlers 17 | 18 | class TagSoupHandlers(subject: HandlerVerbs) { 19 | lazy val parserFactory = new SAXFactoryImpl 20 | /** Process response with TagSoup html processor in block */ 21 | def tagsouped [T] (block: (xml.NodeSeq) => T) = subject >> { (stm, charset) => 22 | block( new NoBindingFactoryAdapter().loadXML(new InputSource(new InputStreamReader(stm, charset)), parserFactory.newSAXParser()) ) 23 | } 24 | /** Alias for verb tagsouped */ 25 | def [T] (block: (xml.NodeSeq) => T) = tagsouped (block) 26 | /** Conveniences handler for retrieving a NodeSeq */ 27 | def as_tagsouped = tagsouped {ns => ns} 28 | } 29 | -------------------------------------------------------------------------------- /tagsoup/src/test/resources/Human.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | Human 6 | 7 | 8 |

    To err is human, to forgive divine

    9 | 10 |

    The verb "err" means to do something wrong; to make a mistake is "to err". 11 | "To err is human" because all people ("humans") make mistakes. 12 |

    "To err is human, to forgive divine" says we should try hard to forgive others because all 13 | people are human and make mistakes. 14 |

    Example: "I am still angry about what my manager did yesterday!" Answer: "It is best to just 15 | let it go; to err is human, to forgive divine." The world of people is "human" 16 | and the world of God is "divine". 17 |

    God's special power to forgive people for their mistakes is called "divine" mercy. 18 | When we forgive other people we are acting "divine". "To err is human, to forgive 19 | divine" says that we are all human and we all make mistakes so we should all try hard to forgive other 20 | people when they make mistakes. 21 |

    Example: "I will never forgive my mother for what she has done!" 22 |

    Answer:

    23 |
  • "Don't be angry at her. To err is human, to forgive divine." (Because we use the 24 | word "is" in the first phrase "to err is human" we do not use "is" again in the second phrase: 25 | "to forgive divine".) We are all people who make mistakes so to forgive others when they make 26 | mistakes is the right thing to do; "to err is human, to forgive divine." 27 | 28 | 29 | -------------------------------------------------------------------------------- /tagsoup/src/test/resources/test.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | ApA 6 | 7 | 8 |

    Ape

    9 |

    Apes are Old World anthropoid mammals, more specifically a clade of tailless catarrhine primates, 10 | belonging to the biological superfamily Hominoidea. The apes are native to Africa and South-east Asia, 11 | although in relatively recent times humans have spread all over the world. 12 | Apes are the world's largest primates; the orangutan, an ape, is the world's largest living arboreal animal. 13 | Hominoids are traditionally forest dwellers, although chimpanzees may range into savanna, and the extinct 14 | australopithecines are famous for being savanna inhabitants, inferred from their morphology. Humans inhabit 15 | almost every terrestrial habitat.

    16 |

    Hominoidea contains two families of living (extant) species:

    17 | 23 | 24 | -------------------------------------------------------------------------------- /tagsoup/src/test/scala/TagSoupSpec.scala: -------------------------------------------------------------------------------- 1 | import java.io.File 2 | import org.eclipse.jetty.server.handler.{DefaultHandler, HandlerList, ResourceHandler} 3 | import org.eclipse.jetty.server.Server 4 | import org.eclipse.jetty.server.nio.SelectChannelConnector 5 | import org.specs2.mutable.Specification 6 | import dispatch.classic._ 7 | import dispatch.classic.tagsoup._ 8 | 9 | object TagSoupSpec extends Specification with ServedByJetty { 10 | val port = 9980 11 | val resourceBase = "./tagsoup/src/test/resources" 12 | 13 | object BadHtml_with_ImplicitTagSoupHandlers 14 | extends Request(:/("localhost", port) / "Human.html") 15 | with ImplicitTagSoupHandlers 16 | 17 | class BadHtmlClass1(request: Request = (:/("localhost", port) / "Human.html")) 18 | extends Request(request: Request) 19 | with ImplicitTagSoupHandlers 20 | 21 | class BadHtmlClass2(request: Request = (:/("localhost", port) / "Human.html")) 22 | extends Request(request: Request) 23 | 24 | object BadHtml2 25 | extends Request(:/("localhost", port) / "Human.html") 26 | with ImplicitTagSoupHandlers 27 | 28 | object BadHtml 29 | extends Request(:/("localhost", port) / "Human.html") 30 | 31 | "Using " should { 32 | "fail to parse resource" in { 33 | withResourceServer { _ => 34 | import XhtmlParsing._ 35 | val request = :/("localhost", port) / "Human.html" 36 | 37 | Http(request { nodes => 38 | (nodes \\ "title").text 39 | }) must throwA[scala.xml.parsing.FatalError] 40 | } 41 | } 42 | } 43 | 44 | """Using """ should { 45 | import TagSoupHttp._ 46 | "successfully parse resource" in { 47 | withResourceServer { _ => 48 | val request = :/("localhost", port) / "Human.html" 49 | Http(request { nodes => 50 | (nodes \\ "title").text 51 | }) must not (throwA[scala.xml.parsing.FatalError]) 52 | } 53 | } 54 | } 55 | 56 | "Extending implicit TagSoup" should { 57 | "make BadHtml parsable" in { 58 | withResourceServer { _ => 59 | val request = BadHtml_with_ImplicitTagSoupHandlers 60 | val title = Http(request tagsouped { nodes => 61 | (nodes \\ "title").text 62 | }) 63 | title must be_==("Human") 64 | } 65 | } 66 | 67 | "make BadHtmlClass1 (class extend implicit) parsable, though this is ugly" in { 68 | withResourceServer { _ => 69 | var request = new BadHtmlClass1() 70 | val title = Http(request.requestToTagSoupHandlers(request) tagsouped { nodes => 71 | (nodes \\ "title").text 72 | }) 73 | title must be_==("Human") 74 | } 75 | } 76 | 77 | "make BadHtmlClass2 (instance extends implicit) parsable, though this is ugly" in { 78 | withResourceServer { _ => 79 | var request = new BadHtmlClass2() with ImplicitTagSoupHandlers 80 | val title = Http(request.requestToTagSoupHandlers(request) tagsouped { nodes => 81 | (nodes \\ "title").text 82 | }) 83 | 84 | title must be_==("Human") 85 | } 86 | } 87 | } 88 | 89 | "Implicit TagSoupHttp converters in scope" should { 90 | import TagSoupHttp._ 91 | "make Request parsable" in { 92 | withResourceServer { _ => 93 | val request = :/("localhost", port) / "Human.html" 94 | val title = Http(request tagsouped { nodes => 95 | (nodes \\ "title").text 96 | }) 97 | 98 | title must be_==("Human") 99 | } 100 | } 101 | 102 | "make BadHtmlClass1 (class extend implicit) parsable" in { 103 | withResourceServer { _ => 104 | var request = new BadHtmlClass1() 105 | val title = Http(request tagsouped { nodes => 106 | (nodes \\ "title").text 107 | }) 108 | 109 | title must be_==("Human") 110 | } 111 | } 112 | 113 | "make BadHtmlClass2 (instance extends implicit) parsable" in { 114 | withResourceServer { _ => 115 | var request = new BadHtmlClass2() with ImplicitTagSoupHandlers 116 | val title = Http(request tagsouped { nodes => 117 | (nodes \\ "title").text 118 | }) 119 | 120 | title must be_==("Human") 121 | } 122 | } 123 | 124 | "make BadHtmlClass2 parsable" in { 125 | withResourceServer { _ => 126 | var request = new BadHtmlClass2() 127 | val title = Http(request tagsouped { nodes => 128 | (nodes \\ "title").text 129 | }) 130 | 131 | title must be_==("Human") 132 | } 133 | } 134 | 135 | "make BadHtml (object) parsable" in { 136 | withResourceServer { _ => 137 | val request = BadHtml 138 | val title = Http(request tagsouped { nodes => 139 | (nodes \\ "title").text 140 | }) 141 | 142 | title must be_==("Human") 143 | } 144 | } 145 | } 146 | 147 | """Using the verb """ should { 148 | "do the same thing as the verb tagsouped" in { 149 | withResourceServer { _ => 150 | val request = BadHtml_with_ImplicitTagSoupHandlers 151 | 152 | val title1 = Http(request { nodes => 153 | (nodes \\ "title").text 154 | }) 155 | val title2 = Http(request tagsouped { nodes => 156 | (nodes \\ "title").text 157 | }) 158 | 159 | title1 must be_==(title2) 160 | } 161 | } 162 | } 163 | 164 | "Using the verb as_tagsouped" should { 165 | "return the nodes" in { 166 | withResourceServer { _ => 167 | val request = BadHtml_with_ImplicitTagSoupHandlers 168 | val ns = Http(request as_tagsouped) 169 | 170 | (ns \\ "title").text must be_==("Human") 171 | } 172 | } 173 | } 174 | } 175 | 176 | trait ServedByJetty { 177 | val port: Int 178 | val resourceBase: String 179 | 180 | def withResourceServer[A](op: Unit => A): A = { 181 | // Configure Jetty server 182 | val connector = new SelectChannelConnector 183 | connector.setHost("localhost") 184 | connector.setPort(port) 185 | 186 | val handler = new ResourceHandler 187 | handler.setDirectoriesListed(true) 188 | handler.setResourceBase(resourceBase) 189 | val handlers = new HandlerList 190 | handlers.setHandlers(Array(handler, new DefaultHandler)) 191 | 192 | val server = new Server 193 | server.addConnector(connector) 194 | server.setHandler(handlers) 195 | 196 | // Run server for test and then stop 197 | try { 198 | server.start 199 | op(()) 200 | } finally { 201 | server.stop 202 | } 203 | } 204 | } 205 | -------------------------------------------------------------------------------- /temp: -------------------------------------------------------------------------------- 1 | Adding Dispatch to a Project 2 | ---------------------------- 3 | 4 | The latest release of Databinder Dispatch is [$version$][notes]. 5 | 6 | [notes]: http://implicit.ly/dispatch-080 7 | 8 | ### Modules 9 | 10 | The library is divided into separate modules so that client 11 | applications may depend only on the parts of Dispatch they use. Each 12 | module is [cross-built][sbt] against several versions of Scala and 13 | [published to the scala-tools][st] repository with the organization-id 14 | "net.databinder". The modules have the Scala version they are built 15 | against appended; for Scala $scala$: 16 | 17 | * dispatch-http_$scala$ 18 | * dispatch-core_$scala$ 19 | * dispatch-nio_$scala$ 20 | * dispatch-futures_$scala$ 21 | * dispatch-mime_$scala$ 22 | * dispatch-json_$scala$ 23 | * dispatch-http-json_$scala$ 24 | * dispatch-lift-json_$scala$ 25 | * dispatch-oauth_$scala$ 26 | * dispatch-meetup_$scala$ 27 | * dispatch-couch_$scala$ 28 | * dispatch-twitter_$scala$ 29 | * dispatch-s3_$scala$ 30 | * dispatch-google_$scala$ 31 | * dispatch-gae_$scala$ 32 | 33 | [dn]: http://databinder.net/repo/ 34 | [st]: http://scala-tools.org/repo-releases/net/databinder/ 35 | [sbt]: http://code.google.com/p/simple-build-tool/wiki/CrossBuild 36 | 37 | With simple-build-tool it's best to have the Scala version 38 | [automatically appended][sbt] so it will always match your 39 | project's. For Maven or Ivy + Ant, specify a full artifact-id like 40 | those given above. 41 | 42 | Module dependencies are transitive, so you only need to depend on 43 | dispatch-twitter to have that module as well as -oauth, -json, and 44 | -http on the classpath. 45 | 46 | ### Source 47 | 48 | Dispatch $version$'s full source is available: { [zip][zip] | [tar.bz2][tar.bz2] | [github][gh] } 49 | 50 | [zip]: http://technically.us/git?p=dispatch.git;a=snapshot;h=$version$;sf=zip 51 | [tar.bz2]: http://technically.us/git?p=dispatch.git;a=snapshot;h=$version$;sf=tbz2 52 | 53 | [gh]: https://github.com/n8han/Databinder-Dispatch 54 | --------------------------------------------------------------------------------