Exporting CSV in Grails

Recently I needed to implement a controller action that generates a CSV (Comma Separated Values) representation of some data. It was pretty simple to do, but I couldn’t find a decent writeup of how to write the code and test it. This blog post will show what I ended up implementing.

Generating CSV

If you’re generating CSV using Groovy’s join() function… you’re doing it wrong!

 

I use the OpenCSV library for reading and writing CSV. It handles all the tricky bits, such as character encoding. Download the archive from http://opencsv.sourceforge.net/ , extract the JAR, and put it in your project’s lib directory. You’ll also want configure your IDE so it knows about the library. At the time of this writing I’m using OpenCSV version 2.3.

NOTE: there’s a Grails CSV plugin that you can use instead. It gives you a nice DSL (Domain Specific Language) for generating CSV. Honestly I didn’t even bother looking at Grails plugins until after I had already written this example. Perhaps I’m Doing It Wrong as well. ;-)

Domain Class

For this example we’ll use a simple Person domain class that looks like:

package leapingmind

class Person {
   String firstName
   String lastName
   Date birthDate

   Date dateCreated
   Date lastUpdated

   static constraints = {
      birthDate(nullable:true)
   }
}

Config

I can’t remember if I had to explicitly add csv to the grails.mime.types setting in Config.groovy. Check yours to verify that it has the following entry: csv: ‘text/csv’

Controller Action

I added a list action to my PeopleController that renders the data in various representations:


def list = {
   def people = Person.list()
   if ( people ) {
      withFormat {
         json {
            if ( params.callback ) {
               render(contentType: 'application/json',
                  text: "${params.callback}(${people as JSON})")
            } else {
               render people as JSON
            }
         }
         xml {
            render people as XML
         }
         csv {
            response.setHeader("Content-disposition",
               "attachment; filename=people.csv")
            response.contentType = "text/csv"
            def out = response.outputStream
            out.withWriter { writer ->
               String[] properties = new String[3]
               def csvWriter = new CSVWriter(writer)
               people.each { person ->
                  properties[0] = person.firstName
                  properties[1] = person.lastName
                  properties[2] = person.birthDate ? person.birthDate.format('yyyy-MM-dd hh:mm:ss') : ''
                  csvWriter.writeNext(properties)
               }
               csvWriter.flush()
            }
         }
      }
   } else {
      render(text: 'No people found', status: 404)
   }
}

Integration Test

I tried doing this as a unit test rather than an integration test, but the controller action was always rendering JSON rather than CSV. Obviously there’s something I’m missing when setting controller.request.contentType in a unit test. Nonetheless, the integration test worked like a charm.


void testListCsv() {

   // Start empty
   Person.executeUpdate("delete from Person")
   assert Person.count() == 0

   def people = []
   def john = new Person(
      firstName: 'John',
      lastName: 'Doe',
      birthDate: new Date()
   )
   people << john

   def jane = new Person(
      firstName: 'Jane',
      lastName: 'Doe',
      birthDate: new Date(),
   )
   people << jane

   people.each { assertNotNull it.save() }

   controller.request.contentType = 'text/csv'
   controller.list()
   def responseString = controller.response.contentAsString

   def reader = new StringReader(responseString)
   def csvReader = new CSVReader(reader)
   def records = csvReader.readAll()

   assertEquals people.size(), records.size() // One line of CSV for each person

   records.each { csvArray ->
      assertEquals 3, csvArray.size()
      def firstName = csvArray[0]
      def lastName = csvArray[1]
      def birthDate = csvArray[2]

      assertTrue people*.firstName.contains(firstName)

      switch ( firstName ) {
      case 'John':
         assertEquals john.birthDate.format('yyyy-MM-dd hh:mm:ss'), birthDate
         assertEquals john.lastName, lastName
         break
      case 'Jane':
         assertEquals jane.birthDate.format('yyyy-MM-dd hh:mm:ss'), birthDate
         assertEquals jane.lastName, lastName
         break
      default:
         throw new RuntimeException("${firstName} was not expected")
      }
   }
}

That’s it! It’s all pretty simple, but hopefully you found it informative or useful.

About these ads

About Eric Turner

I started developing software professionally back in 2001, and love it! Before that I did system administration and networking for a newspaper. Currently I earn my living developing web applications using Grails, Javascript, etc... for a small software consulting company, and am interested in the issues and technologies related to that. I'm more of a pragmatist than a purist, happy with a 90% solution that quickly gets code into the hands of people that need it (undelivered, unused code has little value).
This entry was posted in Grails and tagged , . Bookmark the permalink.

3 Responses to Exporting CSV in Grails

  1. Jean-Sébastien says:

    Hi, thank you for your useful post.
    I try to do something similar in my grails app but I’ve a problem :

    If I follow your example, I got this error :
    getOutputStream() has already been called for this response

    I try to understand and to modify your code with the example given in the grails cvs plugin (http://grails.org/plugin/csv) :

    def sw = new StringWriter()
    def b = new CSVWriter(sw, {
    col1 { it.val1 }
    col2:”foo” { it.val2 }
    })
    b << [val1: 'a', val2: 'b']
    b << [val1: 'c', val2: 'd']

    and I send the composed CSV to the response object like this :
    response.setHeader("Content-disposition", "attachment; filename=file.csv")
    response.contentType = "text/csv"
    response.outputStream << sw

    this works fine but is not adapted for the cases with a dynamic number of columns.

    I would like to ask you some explanations for the lines 20, 21 and 23 in your example. I don't realy understand the role of the closure.
    The usage of closures in a function call isn't yet very clear for me.

    Thank you very much,
    Jean-Sébastien

  2. Flash says:

    thanks, it’s just what I need!

  3. Olegs Klujs says:

    Thanks, perfect!

Comments are closed.