diff --git a/05-JavaScript-Ajax-Comet.asciidoc b/05-JavaScript-Ajax-Comet.asciidoc index 423853a..91be42b 100644 --- a/05-JavaScript-Ajax-Comet.asciidoc +++ b/05-JavaScript-Ajax-Comet.asciidoc @@ -1,3 +1,4 @@ +[[Ajax]] Javascript, Ajax, Comet ----------------------- @@ -22,7 +23,7 @@ import net.liftweb.http.SHtml import net.liftweb.http.js.JsCmds class MySnippet { - + def easy = "#myButton [onclick]" #> SHtml.ajaxInvoke( () => { println("click") JsCmds.Alert("Hi") @@ -102,7 +103,7 @@ that we're setting up this binding in our server-side Lift code: [source,scala] ---- class HelloWorld { - def buttonBind = + def buttonBind = "#button [onclick]" #> "$('#button').fadeOut('slow')" } ---- @@ -121,7 +122,7 @@ Lift will render the page as: [source,html] ----
-
---- @@ -135,7 +136,7 @@ combine basic commands: [source,scala] ---- -def buttonBind = +def buttonBind = "#button [onclick]" #> ( Alert("Here we go...") & RedirectTo("http://liftweb.net") ).toJsCmd @@ -159,7 +160,7 @@ like this: [source,scala] ---- -def buttonBind = +def buttonBind = "#button [onclick]" #> JE.Call("greet", "you").toJsCmd ---- @@ -277,7 +278,7 @@ on the page. [source,scala] ---- -package code.snippet +package code.snippet import net.liftweb.util._ import Helpers._ diff --git a/10-Around.asciidoc b/10-Around.asciidoc index b88b21d..a197907 100644 --- a/10-Around.asciidoc +++ b/10-Around.asciidoc @@ -5,7 +5,7 @@ In and Around Lift This section gives examples of working with Lift and other systems, such as sending email or scheduling tasks. -Code for this chatper can be found at: https://github.com/LiftCookbook/cookbook_around[https://github.com/LiftCookbook/cookbook_around]. +Many of the recipes in this chapter have code examples in a project at Github: https://github.com/LiftCookbook/cookbook_around[https://github.com/LiftCookbook/cookbook_around]. [[SendTextEmail]] @@ -43,20 +43,28 @@ negotiating with an SMTP server. There's also a `blockingSendMail` method if you want to wait. By default, the SMTP server used will be `localhost`. You can change -this by setting the `mail.smtp.host` property. For example, edit `src/mail/resources/props/default.props` and -add the line: +this by setting the `mail.smtp.host` property. +For example, edit `src/mail/resources/props/default.props` and add the line: -------------------------------- mail.smtp.host=smtp.example.org -------------------------------- -Mailer also supports JNDI as a source of email sessions. - The signature of `sendMail` requires a `From`, `Subject` and then any -number of `MailTypes`. In the example we added two: `PlainMailBodyType` -and `To`. You can add others including `BCC`, `ReplyTo` and -`MessageHeader` (a name and value pair), and you can add them multiple -times: +number of `MailTypes`: + +* `To` -- the recipient email address. +* `CC` +* `BCC` +* `ReplyTo` +* `MessageHeader` -- key/value pairs to include as headers in the message. +* `PlainMailBodyType` -- a plain text email sent with UTF-8 encoding. +* `PlainPlusBodyType` -- a plain text email, where you specify the encoding. +* `XHTMLMailBodyType` -- for HTML email (<>. +* `XHTMLPlusImages` -- for attachments (<>). + +In the example above we added two types:`PlainMailBodyType` +and `To`. Adding more as as you'd expect: [source,scala] ---------------------------------------- @@ -69,6 +77,20 @@ Mailer.sendMail( PlainMailBodyType("Hello from Lift") ) ---------------------------------------- + +The address-like `MailTypes` (`To`, `CC` etc) can be given an optional "personal name": + +[source,scala] +---------------------------------------- +From("you@example.org", Full("Example Corporation")) +---------------------------------------- + +This would appear in your mailbox as: + +---------------------------------------- +From: Example Corporation +---------------------------------------- + The default character set is UTF-8. If you need to change this replace the use of `PlainMailBodyType` with `PlainPlusBodyType("Hello from Lift", "ISO8859_1")`. @@ -76,7 +98,9 @@ the use of `PlainMailBodyType` with See Also ^^^^^^^^ -Appendix F of _Exploring Lift_ discusses the sending of email, including sending multiple formats of message. Find it online at: http://exploring.liftweb.net/master/index-F.html#toc-Appendix-F[http://exploring.liftweb.net/master/index-F.html#toc-Appendix-F]. +<> describes email with attachments. + +For HTML email, see <>. [[LogEmail]] @@ -134,7 +158,7 @@ This recipe has changed just the behaviour of `Mailer` when your Lift application is in developer mode (which it is by default). We're logging just the body part of the message. -It seems that `javax.mail` doesn't include a utility to display all the +It seems that Java Mail doesn't include a utility to display all the parts of an email, so if you want more information you'll need to roll your own function. For example: @@ -142,11 +166,20 @@ roll your own function. For example: --------------------------------------------------------- def display(m: MimeMessage) : String = { - def from = "From: "+m.getFrom.map(_.toString).mkString(",") - def subj = "Subject: "+m.getSubject - def body = m.getContent.toString + val from = "From: "+m.getFrom.map(_.toString).mkString(",") + val subj = "Subject: "+m.getSubject + + def parts(mm: MimeMultipart) = (0 until mm.getCount).map(mm.getBodyPart) + + val body = m.getContent match { + case mm: MimeMultipart => + val bodyParts = for (part <- parts(mm)) yield part.getContent.toString + bodyParts.mkString(nl) - def to = for { + case otherwise => otherwise.toString + } + + val to = for { rt <- List(RecipientType.TO, RecipientType.CC, RecipientType.BCC) address <- Option(m.getRecipients(rt)) getOrElse Array() } yield rt.toString + ": " + address.toString @@ -154,7 +187,6 @@ def display(m: MimeMessage) : String = { val nl = System.getProperty("line.separator") List(from, to.mkString(nl), subj, body) mkString nl - } Mailer.devModeSend.default.set( (m: MimeMessage) => @@ -172,14 +204,35 @@ Subject: Hello Hello from Lift --------------------------------------------------------- +This example `display` function is mostly straight-forward. The `body` value handles multi-part messages by extracting the each body part. This is triggered when sending more structured emails, such as the HTML emails described in <>. + The key part of this recipe is setting a `MimeMessage => Unit` function on `Mailer.devModeSend`. We happen to be logging, but you can use this function to handle the email any way you want. Examples include logging and sending by triggering or recording the send in a database. -debug flags? +If you want to debug the mail system sending email, enable the Java Mail debug mode. In `default.props` add: + +[source, properties] +-------------------------------------- +mail.debug=true +-------------------------------------- + +This produces low-level output from the `javax.mail` system when email is sent: +-------------------------------------- +DEBUG: JavaMail version 1.4.4 +DEBUG: successfully loaded resource: /META-INF/javamail.default.providers +DEBUG SMTP: useEhlo true, useAuth false +DEBUG SMTP: trying to connect to host "localhost", port 25, isSSL false +... +-------------------------------------- +See Also +^^^^^^^^ +Run modes are described at: https://www.assembla.com/spaces/liftweb/wiki/Run_Modes[https://www.assembla.com/spaces/liftweb/wiki/Run_Modes]. + +[[HTMLEmail]] Sending HTML email ~~~~~~~~~~~~~~~~~~ @@ -195,9 +248,10 @@ Give `Mailer` a `NodeSeq` containing your HTML message: [source,scala] ---------------------------------- +import net.liftweb.util.Mailer import net.liftweb.util.Mailer._ -val html = +val msg = Hello @@ -207,10 +261,10 @@ val html = Mailer.sendMail( - From("Myself "), + From("me@example.org"), Subject("Hello"), To("you@example.org"), - html) + msg) ---------------------------------- Discussion @@ -218,16 +272,74 @@ Discussion An implicit converts the `NodeSeq` into a `XHTMLMailBodyType`. This ensures the mime type of the email is "text/html". Despite the name of -XHTML, the message is to converted into a string for transmission using +"XHTML", the message is to converted into a string for transmission using HTML5 semantics. The character encoding for HTML email, UTF-8, can be changed by setting `mail.charset` in your Lift properties file. +If you want to set both the text and HTML version of a message, supply each body wrapped in the appropriate `BodyType` class: + +[source, scala] +--------------------------------------------------- +val html = + + Hello + + +

Hello!

+ + + +var text = "Hello!" + +Mailer.sendMail( + From("me@example.org"), + Subject("Hello"), + To("you@example.org"), + PlainMailBodyType(text), + XHTMLMailBodyType(html) +) +--------------------------------------------------- + +This message would be sent as a "multipart/alternative": + +------------------------------------------ +Content-Type: multipart/alternative; + boundary="----=_Part_1_1197390963.1360226660982" +Date: Thu, 07 Feb 2013 02:44:22 -0600 (CST) + +------=_Part_1_1197390963.1360226660982 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 7bit + +Hello! +------=_Part_1_1197390963.1360226660982 +Content-Type: text/html; charset=UTF-8 +Content-Transfer-Encoding: 7bit + + + + Hello + + +

Hello!

+ + +------=_Part_1_1197390963.1360226660982-- +------------------------------------------ + +When receiving a message with this content, it is up to the mail client to decide which version to show (text or HTML). + + +See Also +~~~~~~~~ + -Sending authenticated email +[[AuthEmail]] +Sending Authenticated Email ~~~~~~~~~~~~~~~~~~~~~~~~~~~ Problem @@ -246,7 +358,7 @@ Modify `Boot.scala` to include: [source,scala] -------------------------------------------------------- -import net.liftweb.util.Mailer._ +import net.liftweb.util.{Props, Mailer} import javax.mail.{Authenticator,PasswordAuthentication} Mailer.authenticator = for { @@ -270,6 +382,8 @@ mail.password=correct horse battery staple mail.smtp.host=smtp.sendgrid.net ------------------------------------------ +When you send email, the credentials in `default.props` will be used to authenticate with the SMTP server. + Discussion ^^^^^^^^^^ @@ -280,19 +394,19 @@ authentication settings, but our `production.default.props` did, then no authentication would happen in development mode, ensuring we can't accidentally send email outside of a production environment. -But you don't have to use a properties file for this (the Lift Mailer -also supports JNDI). However, some mail services do require -`mail.smtp.auth=true` to be set. +You don't have to use a properties file for this: the Lift Mailer +also supports JNDI, or you could lookup a username and password some other way and set `Mailer.authenticator` when you have the values. + +However, some mail services such as SendGrid do require `mail.smtp.auth=true` to be set, and that should go into your Lift properties file or set as a JVM argument: `-Dmail.smtp.auth=true`. See Also ^^^^^^^^ -* The http://javamail.kenai.com/nonav/javadocs/com/sun/mail/smtp/package-summary.html[com.sun.mail.smtp description] - - +As well as `mail.smtp.auth` there are a range of settings to control the Java Mail API. For example, controlling port numbers and timeouts. These are listed at: http://javamail.kenai.com/nonav/javadocs/com/sun/mail/smtp/package-summary.html[http://javamail.kenai.com/nonav/javadocs/com/sun/mail/smtp/package-summary.html]. -Sending email with attachments +[[EmailWithAttachments]] +Sending Email with Attachments ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Problem @@ -303,7 +417,7 @@ You want to send an email with one or more attachments. Solution ^^^^^^^^ -Use Mailer's `XHTMLPlusImages` body types: +Use the `Mailer` `XHTMLPlusImages` to package a message with one or more attachments: [source,scala] -------------------------------------------------------------- @@ -333,29 +447,63 @@ Discussion ^^^^^^^^^^ `XHTMLPlusImages` can also accept more than one `PlusImageHolder` if you -have more than one file to attach. +have more than one file to attach. Although the name `PlusImageHolder` may suggest it is for attachment images, you can attach any kind of data as an `Array[Byte]` with an appropriate mime type. -Messages are sent using the "related" multi-part mime heading with -"inline" disposition. +By default the attachment is sent with an "inline" disposition. This controls the `Content-Disposition` header in the message, and "inline" means the content is intended for display automatically when the message is shown. The alternative is "attachment", and this can be indicated with a option final parameter to `PlusImageHolder`: -See Also -^^^^^^^^ +[source,scala] +-------------------------------------------------------------- +PlusImageHolder(attach.filename, attach.mime, attach.bytes, attachment=true) +-------------------------------------------------------------- + +In reality, the mail client will display the message how it wants to, but this extra parameter may give you a little more control. + + +To attach a pre-made file, you can use `LiftRules.loadResource` to fetch content from the classpath. As an example, if our project contained `Kepler-22b_System_Diagram.jpg` in the `src/main/resources/` folder, we could load and attach it like this: + +[source,scala] +-------------------------------------------------------------- +val filename = "Kepler-22b_System_Diagram.jpg" + +val msg = + for ( bytes <- LiftRules.loadResource("/"+filename) ) + yield XHTMLPlusImages( +

Please research this planet.

, + PlusImageHolder(filename, "image/jpg", bytes) ) + +msg match { + case Full(m) => + Mailer.sendMail( + From("me@example.org"), + Subject("Planet attachment"), + To("you@example.org"), + m) + + case _ => + logger.error("Planet file not found") +} +-------------------------------------------------------------- -* Lift ticket 1197 to https://github.com/lift/framework/issues/1197[improve Mailer functionality for attachments] -* Wikipedia entry on http://en.wikipedia.org/wiki/MIME[Multipurpose Internet Mail Extensions (MIME)] +As the contents of `src/main/resources` is included on the classpath, we pass the filename to `loadResource` with a leading `/` character so the file can be found. +See Also +^^^^^^^^ + +Messages are sent using the "multipart/related" mime heading, with an "inline" disposition. Lift ticket #1197 links to a discussion regarding "multipart/mixed" which may be preferable for working around issues with Microsoft Exchange. See: https://github.com/lift/framework/issues/1197[https://github.com/lift/framework/issues/1197]. +RFC 2183 describes the `Content-Disposition" header: http://www.ietf.org/rfc/rfc2183.txt[http://www.ietf.org/rfc/rfc2183.txt]. -Run a task later +[[RunLater]] +Run a Task Later ~~~~~~~~~~~~~~~~ Problem ^^^^^^^ -You want to schedule code to run later. +You want to schedule code to run at some future time. Solution ^^^^^^^^ @@ -364,31 +512,43 @@ Use `net.liftweb.util.Schedule`: [source,scala] ------------------------------------------------ -Schedule( () => println("doing it"), 30 seconds) +import net.liftweb.util.Schedule +import net.liftweb.util.Helpers._ + +Schedule(() => println("doing it"), 30 seconds) ------------------------------------------------ -This would cause "doing it" to be printed on the console after 30 -seconds. +This would cause "doing it" to be printed on the console 30 +seconds from now. Discussion ^^^^^^^^^^ -`Schedule` also includes a `schedule` method which will send a specified -actor a specified message after a given delay. +The signature for `Schedule` that we're using in the recipe expects a function to call of type `() => Unit`, which is the thing we want to happen in the future, and a `TimeSpan` from Lift's `TimeHelpers` which is when we want it to happen. The `30 seconds` value gives us a `TimeSpan` via the `Helpers._` import, but there's a variation which accepts a `Long` millisecond value if you prefer that: -The above example makes use of the Lift `TimeHelpers`, but there are -variant calls that accept `Long` millisecond values. +[source,scala] +------------------------------------------------ +Schedule.perform(() => println("doing it"), 30*1000L) +------------------------------------------------ + +Behind the scenes, Lift is making use of the `ScheduledExecutorService` from `java.util.concurrent`, and as such returns a `ScheduledFuture[Unit]`. You can use this future to `cancel` the operation before it runs. + +It may be a surprise to find that you can call `Schedule` with just a function as an argument, and not a delay value. This version runs the function immediately, but on a worker thread. This is a convenient way to asynchronously run other tasks without going to the trouble of creating an actor for the purpose. + +There is also a `Schedule.schedule` method which will send a specified +actor a specified message after a given delay. This takes a `TimeSpan` delay, but again there's also a `Schedule.perform` version that accepts a `Long` as a delay. -Schedule returns a `ScheduledFuture[Unit]` from the Java concurrency -library, which allows you to `cancel` the activity. See Also ^^^^^^^^ -* https://github.com/lift/framework/blob/master/core/util/src/main/scala/net/liftweb/util/Schedule.scala[Schedule.scala] -source. -* http://docs.oracle.com/javase/6/docs/api/java/util/concurrent/ScheduledFuture.html[java.util.concurrent.ScheduledFuture] JavaDoc. -* Recipe for link:Run+tasks+periodically.html[Running tasks periodically] +<> includes an example of scheduling with actors. + +`ScheduledFuture` is documented via the Java Doc for `Future` at: http://docs.oracle.com/javase/6/docs/api/java/util/concurrent/Future.html[http://docs.oracle.com/javase/6/docs/api/java/util/concurrent/Future.html]. If you're building complex, low-level, cancellable concurrency functions, it's advisable to have a copy of _Java Concurrency in Practice_ close by (Goetz _et al._, 2006, Addison-Wesley Professional). + + + + [[RunTasksPeriodically]] Run Tasks Periodically @@ -397,7 +557,7 @@ Run Tasks Periodically Problem ^^^^^^^ -You want a scheduled task to run periodically. +You want a scheduled task to run periodically (repeatedly. Solution ^^^^^^^^ @@ -419,8 +579,7 @@ object MyScheduledTask extends LiftActor { private var stopped = false def messageHandler = { - case DoIt => - if (!stopped) + case DoIt if !stopped => Schedule.schedule(this, DoIt, 10 minutes) // ... do useful work here @@ -435,11 +594,19 @@ a `DoIt` message, the actor re-schedules itself before doing whatever useful work needs to be done. In this way, the actor will be called every 10 minutes. +Discussion +^^^^^^^^^^ + The `Schedule.schedule` call is ensuring that `this` actor is sent the `DoIt` message after 10 minutes. To start this process off, possibly in `Boot.scala`, just send the -`DoIt` message to the actor. +`DoIt` message to the actor: + +[source,scala] +---------------------------------------------------------------------------- +MyScheduledTask ! MyScheduledTask.DoIt +---------------------------------------------------------------------------- To ensure the process stops correctly when Lift shuts down, we register a shutdown hook in `Boot.scala` to send the `Stop` message to prevent @@ -450,10 +617,7 @@ future re-schedules: LiftRules.unloadHooks.append( () => MyScheduledTask ! MyScheduledTask.Stop ) ---------------------------------------------------------------------------- -Discussion -^^^^^^^^^^ - -Without the `Stop` message your actor would continue to be rescheduled +Without the `Stop` message the actor would continue to be rescheduled until the JVM exits. This may be acceptable, but note that during development with SBT, without the `Stop` message, you will continue to schedule tasks after issuing the `container:stop` command. @@ -464,82 +628,179 @@ library, which allows you to `cancel` the activity. See Also ^^^^^^^^ -* https://github.com/lift/framework/blob/master/core/util/src/main/scala/net/liftweb/util/Schedule.scala[Schedule.scala] source. -* http://docs.oracle.com/javase/6/docs/api/java/util/concurrent/ScheduledFuture.html[java.util.concurrent.ScheduledFuture] JavaDoc. -* Chapter 1 of _Lift in Action_ includes a CometActor clock example that -uses `Schedule`, and further examples can be found in chapters 4 and 9. +Chapter 1 of _Lift in Action_ (Perrett, 2011, Manning Publications Co) includes a CometActor clock example that +uses `Schedule`. + + +[[FetchURLs]] Fetching URLs ~~~~~~~~~~~~~ Problem ^^^^^^^ -You want to fetch a URL from inside your Lift app. +You want to fetch a URL from inside your Lift application. Solution ^^^^^^^^ -Use _Dispatch_, "a library for HTTP interaction, from asynchronous GETs -to multi-part OAuth-enticated POSTs". Before you start, include the -dependencies in your `build.sbt` file: +Use _Dispatch_, "a library for asynchronous HTTP interaction". + +Before you start, include Dispatch dependency in your `build.sbt` file: [source,scala] ------------------------------------------------- -libraryDependencies ++= Seq( - "net.databinder" %% "dispatch-core" % "0.8.8", - "net.databinder" %% "dispatch-http" % "0.8.8", - "net.databinder" %% "dispatch-tagsoup" % "0.8.8" -) +libraryDependencies += "net.databinder.dispatch" %% "dispatch-core" % "0.9.5" ------------------------------------------------- -Databinder is structured into a set of modules (e.g., for oAuth and -Twitter). Above we're including a set for an example of fetching a URL -and extracting all the meta tags: +Using the example from the Dispatch documentation, we can make a HTTP request to try to determine the country from the service at http://www.hostip.info/use.html[http://www.hostip.info/use.html]: [source,scala] ------------------------------------------ -import scala.xml.NodeSeq import dispatch._ -import dispatch.tagsoup.TagSoupHttp._ +val svc = url("http://api.hostip.info/country.php") +val country : Promise[String] = Http(svc OK as.String) + +println(country()) +------------------------------------------ + +Note that the result `country` is not a `String` but a `Promise[String]`, and we resolve the value of with `country()`. + +The result printed will be a country code such as `GB`, or `XX` if the country cannot be determined from your IP address. + +Discussion +^^^^^^^^^^ + +This short example expects a 200 (OK) status result and turns the result into a `String`, but that a tiny part of what Dispatch is capable of. We'll explore further in this section. -val page = url("http://www.w3.org/") +What if the request doesn't return a 200? In that case, with the code we have, we'd get an exception such as: "Unexpected response status: 404". There are a few ways to change that. -def metas(ns: NodeSeq) = ns \\ "meta" +We can ask for an `Option`: -val result: NodeSeq = Http(page metas) +[source,scala] +------------------------------------------ +val result : Option[String] = country.option() ------------------------------------------ -The above produces: +...which as you'd expect will give a `None` or `Some[String]`. However, if you have debug level logging enabled in your application you'll see the request and response and error messages from the underlying Netty library. You can tune these messages by adding a logger setting to `default.logback.xml` file: + +[source, xml] +------------------------------------------ + +------------------------------------------ + +A second possibility is to use `either` with the usual convention that the `Right` is the correct result and `Left` signifies a failure: [source,scala] ----------------------------------------------------------------------------------------- -NodeSeq(, - , - ) ----------------------------------------------------------------------------------------- +------------------------------------------ +country.either() match { + case Left(status) => println(status.getMessage) + case Right(cc) => println(cc) +} +------------------------------------------ -Discussion -^^^^^^^^^^ +This will print a result as we are forcing the evaluation with `either()`. + +`Promise[T]` implements `map`, `flatMap`, `filter`, `fold` and all the usual methods you'd expect to allow you to compose. This means you can use the promise with a for comprehension: + +[source,scala] +------------------------------------------ +val codeLength = for (cc <- country) yield cc.length +------------------------------------------ + +Note that `codeLength` is a `Promise[Int]`. To get the value you can evaluate `codeLength()` (and you'll get a result of 2). -As URL fetching has latency, you will want to look at making the request -from an actor, a lazy-load snippet, via the various Dispatch executors -or similar mechanism. +As well as extracting string values with `as.String` there are other options, including... -_Dispatch_ offers a range of operators in addition to the `` XML one -used above. You can extract text, JSON, consume the stream, or throw -away the content. The _Periodic table_ gives a great high-level view of -what's available. +* `as.Bytes` -- to work with `Promise[Array[Byte]]`. +* `as.File` -- to write to a file, as in `Http(svc > as.File(new File("/tmp/cc")) )`. +* `as.Response` -- to allow you to provide a `client.Response => T` function to use on the response. +* `as.xml.Elem` -- to parse XML response. + +As an example of `as.xml.Elem`: + +[source,scala] +------------------------------------------ +val svc = url("http://api.hostip.info/?ip=12.215.42.19") +val country = Http(svc > as.xml.Elem) +println(country.map(_ \\ "description")()) +------------------------------------------ + +This example is parsing the XML response to the request, which returns a `Promise[scala.xml.Elem]`. We're picking out the description node of the XML via a `map`, which will be a `Promise[NodeSeq]` which we then force to evaluate. The output is something like: + +------------------------------------------ + + This is the Hostip Lookup Service + +------------------------------------------ + +That example assumes the request is going to be well-formed. In addition to the core Databinder library, there are extensions for JSoup and TagSoup to assist in parsing HTML that isn't necessarily well-formed. + +For example, to use JSoup, include the dependency: + +[source,scala] +------------------------------------------------- +libraryDependencies += "net.databinder.dispatch" %% "dispatch-core" % "0.9.5" +------------------------------------------------- + +You can then use the features of JSoup, such as picking out elements of a page using CSS selectors: + +[source,scala] +------------------------------------------------- +import org.jsoup.nodes.Document + +val svc = url("http://www.example.org").setFollowRedirects(true) +val title = Http(svc > as.jsoup.Document).map(_.select("h1").text).option +println( title() getOrElse "unknown title" ) +------------------------------------------------- + +He we are applying JSoup's `select` function to pick out the `

` element on the page, taking the text of the element which we turn into a `Promise[Option[String]]`. The result, unless _example.org_ has changed, will be "Example Domain". + +As a final example of using Dispatch, we can pipe a request into Lift's JSON library: + +[source,scala] +------------------------------------------------- +import net.liftweb.json._ +import com.ning.http.client + +object asJson extends (client.Response => JValue) { + def apply(r: client.Response) = JsonParser.parse(r.getResponseBody) +} + +val svc = url("http://api.hostip.info/get_json.php?ip=212.58.241.131") +val json : Promise[JValue] = Http(svc > asJson) + +case class HostInfo(country_name: String, country_code: String) +implicit val formats = DefaultFormats + +val hostInfo = json.map(_.extract[HostInfo])() +------------------------------------------------- + + +The URL we're calling returns a simple JSON representation of the IP address we've passed. + +By providing a `Response => JValue` to Dispatch we're able to pass the response body through to the JSON parser. We can then map on the `Promise[JValue]` to apply whatever Lift JSON functions we want to: in this case, we're extracting a simple case class. + +The result from the above would show `hostInfo` as: + +------------------------------------------------- +HostInfo(UNITED KINGDOM,GB) +------------------------------------------------- -Related to _TagSoup_, _Dispatch_ also integrates with _JSoup_, which -includes functions for manipulating the real-world HTML you fetch. See Also ^^^^^^^^ -* http://dispatch.databinder.net/Dispatch.html[Dispatch]. -* http://www.flotsam.nl/dispatch-periodic-table.html[Periodic table of Dispatch operators]. -* http://dispatch.databinder.net/JSoup.html[JSoup Dispatch] documentation. +The Dispatch documentation is well-written and guides you through the way Dispatch approaches HTTP. Do spend some time with it at: http://dispatch.databinder.net/Dispatch.html[http://dispatch.databinder.net/Dispatch.html]. + +To see what's available on Dispatch's `Promise`, browse the source: https://github.com/dispatch/reboot/blob/master/core/src/main/scala/promise.scala[https://github.com/dispatch/reboot/blob/master/core/src/main/scala/promise.scala]. + +The Dispatch Google Group is at: https://groups.google.com/forum/#!forum/dispatch-scala[https://groups.google.com/forum/#!forum/dispatch-scala]. + +The previous major version of Dispatch, 0.8.x ("Dispatch Classic"), is quite different from the "reboot" of the project as version 0.9. Consequently, examples you may see that use 0.8.x will need some conversion to run with 0.9.x. Nathan Hamblen's blog describes the change: http://code.technically.us/post/17038250904/fables-of-the-reconstruction-part-2-have-you-tried[http://code.technically.us/post/17038250904/fables-of-the-reconstruction-part-2-have-you-tried]. + +There's a cookbook for JSoup: http://jsoup.org/cookbook/[http://jsoup.org/cookbook/].