This post describes two approaches to implementing file download in Lift framework. Firstly, we will have a look at the implementation that uses ResponseShortcutException described in the Lift Cookbook. Then, I’ll show how to solve the same task with the help of REST service in the way that follows a common Lift approach. Each of the methods has its own pros and cons, so it’s up to you to decide which one works better for your task.
Note: All the scenarios described in the post are implemented for Lift version 2.6.2 and Scala version 2.11.4.
As you know, a specific response with appropriate headers and requested data as a body should be sent to a client in order to trigger file download from the server. In Lift, this response can be initialized by using the InMemoryResponse
class (or any other subclass of LiftResponse
):
code.lib.Respondent.scala
package code.lib
import net.liftweb.http.InMemoryResponse
trait Respondent {
def makeResponse(text: String,
fileName: String = "text.txt"): InMemoryResponse = {
InMemoryResponse(
data = text.getBytes("UTF-8"),
headers = "Content-Type" -> "text/plain; charset=utf-8" ::
"Content-Disposition" -> s"attachment; filename=$fileName" ::
Nil,
cookies = Nil,
code = 200
)
}
}
Code language: JavaScript (javascript)
Then, this newly created response should be returned to the client. The interesting part is that this task is not trivial and can be done in several ways.
Lift Cookbook suggests to throw ResponseShortcutException
with wrapped response in order to transfer it to the dispatcher from any place in the code:
code.snippet.ExceptionExample.scala
throw new ResponseShortcutException(makeResponse(text))
Code language: JavaScript (javascript)
It is a special class of exceptions, which will be caught at a higher level where Lift can extract wrapped response and pass it to the client in order to start downloading.
This implementation looks really simple. But often it can be inconvenient, especially, for a complex code that contains a lot of exception handlers. We always have to keep in mind that, if the code above is placed inside of the try
block, it is necessary to provide an additional case
in the catch
block to re-throw ResponseShortcutException
.
code.snippet.ExceptionExample.scala
try {
// do something
throw new ResponseShortcutException(makeResponse(text))
} catch {
case response: ResponseShortcutException => throw response
case exception: Exception => // handle an exception
}
Code language: JavaScript (javascript)
Also, we can’t use this approach inside a database transaction, because, for example in Squeryl, any exception called inside the transaction block inevitably leads to a rollback.
Fortunately, there is another solution which was mentioned at the beginning. We can create the REST endpoint which returns needed responses to the client. It can be implemented similarly to the Lift implementations of Ajax and Comet (which ensures that we still follow the nice Lift way). We will utilize a special ability of Lift commonly known as “function mapping”, which allows to save functions in the current session as a map with unique names generated by Lift as keys. This ability is usually used for associating functions with some elements on a page. Below is shown a trait which allows to associate a file download call with a page element.
code.lib.FileDownloader.scala
package code.lib
import net.liftweb.http.S._
import net.liftweb.http.js.JsCmds
import net.liftweb.http.{LiftResponse, S}
trait FileDownloader {
def downloadInvoke(response: () => LiftResponse) = {
fmapFunc(NFuncHolder(response))(makeDownloadCall)
}
private def makeDownloadCall(funcName: String) = {
JsCmds.RedirectTo(s"${S.hostAndPath}/download_request?$funcName=_")
}
}
Code language: JavaScript (javascript)
The downloadInvoke
method works the same way as the well known SHtml.ajaxInvoke
method. It saves a passed response into the functions map and associate the makeDownloadCall
method with some element on the page. The makeDownloadCall
method, in turn, with the help of JsCmds.RedirectTo
, makes the GET
request to an endpoint. The parameter of the request is a name of our response in the function map. The implementation of the endpoint is shown below:
code.rest.FileDownload.scala
package code.rest
import net.liftweb.http._
import net.liftweb.http.rest.RestHelper
object FileDownload extends RestHelper {
serve {
case "download_request" :: Nil Get request =>
() => for {
session <- S.session
functions <- session.runParams(request).headOption
response <- serveResponse(functions)
} yield response
}
private def serveResponse(functions: Any): Option[LiftResponse] = functions match {
case (response: LiftResponse) :: Nil => Some(response)
case _ => None
}
}
Code language: JavaScript (javascript)
Now, the only thing that’s left is to hook our new service into LiftRules
by adding the code shown below to the Boot.boot
method:
bootstrap.liftweb.Boot.scala
LiftRules.dispatch.append(FileDownload)
Code language: CSS (css)
Requests will be handled by the created endpoint where the required response will be found by a name, converted and passed to the client. After that, the usual save dialog will be triggered on the client side.
This way we can add file download functionality (for example, using the data-name="download"
selector) by simply adding the following line to a snippet:
code.snippet.RestExample.scala
"data-name=download [onclick]" #> downloadInvoke(() => makeResponse(text))
Code language: PHP (php)
Hope you find this post helpful. Please find the full source code on GitHub.