Lift, scala technologies

Implementing file download functionality in Lift

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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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
    )
  }

}

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
1
throw new ResponseShortcutException(makeResponse(text))

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
1
2
3
4
5
6
7
8
try {
  // do something
  throw new ResponseShortcutException(makeResponse(text))
} catch {
  case response: ResponseShortcutException => throw response
  case exception: Exception => // handle an exception
}

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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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=_")
  }

}

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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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
  }

}

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
1
LiftRules.dispatch.append(FileDownload)

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
1
"data-name=download [onclick]" #> downloadInvoke(() => makeResponse(text))

Hope you find this post helpful. Please find the full source code on GitHub.

Thanks,

Looking to hire a software developer?
Don't hesitate to contact us.

Comments