Permalink
Browse files

Recipe for downloading a file from a link.

  • Loading branch information...
1 parent ce192df commit 05b76f96c222bd5ef260f7930799957aaf053ca9 @d6y d6y committed Feb 4, 2013
Showing with 144 additions and 22 deletions.
  1. +119 −1 02-HTML.asciidoc
  2. +22 −20 04-REST.asciidoc
  3. +2 −0 06-Pipline.asciidoc
  4. +1 −1 07-Record-Squeryl.asciidoc
View
@@ -1,7 +1,10 @@
+[[HTML]]
HTML
----
-Generating HTML is often a major component of web applications. This chapter is concerned with Lift's _View First_ approach and use of _CSS Selectors_. Later chapters focus on Form processing, REST web services, and JavaScript (Ajax and Comet).
+Generating HTML is often a major component of web applications. This chapter is concerned with Lift's _View First_ approach and use of _CSS Selectors_. Later chapters focus on dorm processing, REST web services, and JavaScript (Ajax and Comet).
+
+Code for this chapter is at: https://github.com/LiftCookbook/cookbook_html[https://github.com/LiftCookbook/cookbook_html].
Testing and Debugging CSS Selectors
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -777,6 +780,121 @@ See Also
Lift notices are described on the Wiki: http://www.assembla.com/spaces/liftweb/wiki/Lift_Notices_and_Auto_Fadeout[http://www.assembla.com/spaces/liftweb/wiki/Lift_Notices_and_Auto_Fadeout].
+[[DownloadLink]]
+Link to Download Data
+~~~~~~~~~~~~~~~~~~~~~
+
+Problem
+^^^^^^^
+
+You want a button or a link which, which clicked, will trigger a download in the browser rather than visiting a page.
+
+Solution
+^^^^^^^^
+
+Create a link using `SHtml.link`, provide a function to return a `LiftResponse` and wrap the response in a `ResponseShortcutException`.
+
+As an example, we will create a snippet that shows the user a poem and provides a link to download the poem as a text file. The template for this snippet will present each line of the poem separated by a `<br>`:
+
+[source, html]
+-------------------------------------------------------------
+<h1>A poem</h1>
+
+<div data-lift="DownloadLink">
+ <blockquote>
+ <span class="poem">
+ <span class="line">line goes here</span> <br />
+ </span>
+ </blockquote>
+ <a href="">download link here</a>
+</div>
+-------------------------------------------------------------
+
+The snippet itself will render the poem and replace the download link with one which will send a
+response that the browser will interpret as a file to download:
+
+[source, scala]
+-------------------------------------------------------------
+package code.snippet
+
+import net.liftweb.util.Helpers._
+import net.liftweb.http._
+import xml.Text
+
+class DownloadLink {
+
+ val poem =
+ "Roses are red," ::
+ "Violets are blue," ::
+ "Lift rocks!" ::
+ "And so do you." :: Nil
+
+ def render =
+ ".poem" #> poem.map(line => ".line" #> line) &
+ "a" #> downloadLink
+
+ def downloadLink =
+ SHtml.link("/notused",
+ () => throw new ResponseShortcutException(poemTextFile),
+ Text("Download") )
+
+ def poemTextFile : LiftResponse =
+ InMemoryResponse(
+ poem.mkString("\n").getBytes("UTF-8"),
+ "Content-Type" -> "text/plain; charset=utf8" ::
+ "Content-Disposition" -> "attachment; filename=\"poem.txt\"" :: Nil,
+ cookies=Nil, 200)
+}
+-------------------------------------------------------------
+
+Recall that `SHtml.link` generates a link and executes a function you supply before following the link.
+
+The trick here is that wrapping the `LiftResponse` in a `ResponseShortcutException` will indicate
+to Lift that the response is complete, so the page being linked to (`notused`) won't be processed. The browser is happy: it has a response to the link the user clicked on, and will render it how it wants to, which in this case will probably be by saving the file to disk.
+
+Discussion
+^^^^^^^^^^
+
+`SHtml.link` works by generating a URL which Lift associates with the function you give it. On a page called `downloadlink`, the URL will look something like:
+
+---------------------------------------------
+downloadlink?F845451240716XSXE3G=_#notused
+---------------------------------------------
+
+When that link is followed, Lift looks up the function and executes it, before processing the linked-to resource. However, in this case, we are short-cutting the Lift pipeline by throwing this particular exception. This is caught by Lift and the response wrapped by the exception is taken as the final response from the request.
+
+This short-cutting is used by `S.redirectTo` via `ResponseShortcutException.redirect`. This companion object also defines `shortcutResponse` which you can use like this:
+
+[source, scala]
+----------------------------------------------------
+import net.liftweb.http.ResponseShortcutException._
+
+def downloadLink =
+ SHtml.link("/notused",
+ () => {
+ S.notice("The file was downloaded")
+ throw shortcutResponse(poemTextFile)
+ },
+ Text("Download") )
+----------------------------------------------------
+
+We've included a `S.notice` to highlight that `throw shortcutResponse` will process Lift notices when the page next loads, whereas `throw new ResponseShortcutException` does not. In this case, the notice will not appear when the user downloads the file, but it will be included the next time notices are shown, such as when the user navigates to another page. For many situations, the difference is immaterial.
+
+This recipe has used Lift's stateful features. You can see how useful it is to be able to close over state (the poem), and offer the data for download from memory. If you've created a report from a database, you can offer it as a download without having to re-generate the items from the database.
+
+However, in other situations you might want to avoid holding this data as a function on a link. In that case, you'll want to create a REST service that returns a `LiftResponse`.
+
+See Also
+^^^^^^^^
+
+<<REST>> looks at REST-based services in Lift.
+
+<<RestStreamContent>> discusses `InMemoryResponse` and similar responses to return content to the browser
+
+For reports, the Apache POI project, http://poi.apache.org/[http://poi.apache.org/], includes libraries for generating Excel files; and OpenCSV, http://opencsv.sourceforge.net[http://opencsv.sourceforge.net], is a library for generating CSV files.
+
+
+
Rendering Textile Markup
~~~~~~~~~~~~~~~~~~~~~~~~
View
@@ -1,3 +1,4 @@
+[[REST]]
REST
----
@@ -57,6 +58,7 @@ Solution
Simply create a file (e.g. `sitemap.html`) in your `webapp` folder with
a valid XML-Sitemap markup:
+[source, xml]
----
<?xml version="1.0" encoding="utf-8" ?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
@@ -81,17 +83,17 @@ Make a snippet to fill the required gaps:
[source,scala]
----
-class MySitemapContent \{
+class MySitemapContent {
lazy val entries = MyDBRecord.findAll(..)
- def base: CssSel =
- "loc *" #> "http://%s/".format(S.hostName) &
+ def base: CssSel =
+ "loc *" #> "http://%s/".format(S.hostName) &
"lastmod *" #> someDate.toString("yyyy-MM-dd'T'HH:mm:ss.SSSZZ")
- def list: CssSel =
- "url *" #> entries.map(post =>
- "loc *" #> "http://%s%s".format(S.hostName, post.url) &
+ def list: CssSel =
+ "url *" #> entries.map(post =>
+ "loc *" #> "http://%s%s".format(S.hostName, post.url) &
"lastmod *" #> post.date.toString("yyyy-MM-dd'T'HH:mm:ss.SSSZZ"))
}
@@ -114,7 +116,7 @@ import net.liftweb.http._
object MySitemap extends RestHelper {
serve {
case Req("sitemap" :: Nil, _, GetRequest) =>
- XmlResponse(S.render(<lift:embed what="sitemap" />,
+ XmlResponse(S.render(<lift:embed what="sitemap" />,
S.request.get.request).head)
}
}
@@ -125,14 +127,14 @@ Wire this into your application in `Boot.scala`, for example:
[source,scala]
----
-LiftRules.statelessDispatchTable.append(code.lib.MySitemap)
+LiftRules.statelessDispatchTable.append(code.lib.MySitemap)
----
Test this service using a tool like cURL:
-[source,scala]
+[source,bash]
----
-\$ curl http://127.0.0.1:8080/sitemap
+$ curl http://127.0.0.1:8080/sitemap
----
See Also
@@ -163,7 +165,7 @@ private def reunite(name: String, suffix: String) =
if (suffix.isEmpty) name else name+"."+suffix
serve {
- case "download" :: name :: Nil Get req =>
+ case "download" :: name :: Nil Get req =>
Text("You requested "+reunite(name, req.path.suffix))
}
----
@@ -172,9 +174,9 @@ Requesting this URL with a command like cURL will show you the filename
as expected:
----
-\$ curl http://127.0.0.1:8080/download/123.png
+$ curl http://127.0.0.1:8080/download/123.png
<?xml version="1.0" encoding="UTF-8"?>
-You requested 123.png
+You requested 123.png
----
Discussion
@@ -258,7 +260,7 @@ would match with...
[source,scala]
----
-case Req("reports" :: name :: Nil, "", GetRequest) => ...
+case Req("reports" :: name :: Nil, "", GetRequest) => ...
----
...with `name` set to "foo.csv" not "foo".
@@ -297,7 +299,7 @@ import net.liftweb.http._
object MyUpload extends RestHelper {
serve {
- case "upload" :: Nil Post req =>
+ case "upload" :: Nil Post req =>
for {
bodyBytes <- req.body ?~ "No Body Bytes"
} yield <b>got an image of {bodyBytes.length} bytes</b>
@@ -309,17 +311,17 @@ Wire this into your application in `Boot.scala`, for example:
[source,scala]
----
-LiftRules.statelessDispatchTable.append(code.lib.MyUpload)
+LiftRules.statelessDispatchTable.append(code.lib.MyUpload)
----
Test this service using a tool like cURL:
-[source,scala]
+[source]
----
$ url -X POST --data-binary "@dog.jpg" \
-H 'Content-Type: image/jpg' http://127.0.0.1:8080/upload
<?xml version="1.0" encoding="UTF-8"?>
-<b>got an image of 43685 bytes</b>
+<b>got an image of 43685 bytes</b>
----
Discussion
@@ -372,8 +374,8 @@ import net.liftweb.json.JsonDSL._
object QuotationAPI extends RestHelper {
serve {
- case "quotation" :: Nil JsonGet _ =>
- ("text" -> "A beach house isn't just real estate. It's a state of mind.") ~
+ case "quotation" :: Nil JsonGet _ =>
+ ("text" -> "A beach house isn't just real estate. It's a state of mind.") ~
("by" -> "Douglas Adams") : JObject
}
View
@@ -1,3 +1,4 @@
+[[Pipeline]]
Pipeline
--------
@@ -9,6 +10,7 @@ _Exploring Lift_ at http://exploring.liftweb.net/master/index-9.html#toc-Section
and also from from the Lift pipeline Wiki page at
http://www.assembla.com/spaces/liftweb/wiki/HTTP_Pipeline[http://www.assembla.com/spaces/liftweb/wiki/HTTP_Pipeline].
+[[RestStreamContent]]
Streaming Content
~~~~~~~~~~~~~~~~~
@@ -1430,7 +1430,7 @@ class MySnippet extends Loggable {
}
---------------------------------------------------------------
-This will log queries according to the settings for the logging system, typically the Logback project configured in `src/resources/logback.xml` or `src/resources/props/default.logback.xml`.
+This will log queries according to the settings for the logging system, typically the Logback project configured in `src/resources/props/default.logback.xml`.
It can be inconvenient to have to enable logging in each snippet during development. To trigger logging for all snippets, you can modify the `addAround` call in `Boot.scala`.

0 comments on commit 05b76f9

Please sign in to comment.