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.
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
thanks, it’s just what I need!
Thanks, perfect!