alexgorbatchev

Wednesday, June 26, 2013

Grails Unmarshal JSON into Domain Objects

Quick primer:

For more information regarding marshaling, consult your local wikipedia page on marshaling, but for our purposes, this is all you need to know.
  • Marshalling: Object transformed into simple format (JSON for this example)
  • Unmarshalling: simple format to Object

Prerequisties

In order to run this example, you need
  • Grails (I'm using 2.2.1)
  • Postman - for posting JSON to the Grails app

Example

1. Setup a new project, for this example I will use "jason".
  • grails create-app jason

2. Create a new domain object, for our example I will use
package example

class Book {
   String author
   String title
}
3. Scaffold the Book artifacts
grails generate-all example.Book
4. Edit the Conf/UrlMappings.groovy.  For now we will just hardcode the url mappings for book, so add the following to the top of the mappings block, such that your UrlMappings.groovy looks like:
static mappings = {
"/book/"(controller: "book", parseRequest:true){
action = [GET:'list', POST:'save']
}
"/book/$id?"(controller: "book", parseRequest:true){
action = [GET:'show'] // could also make this action = [GET:'show', PUT:'update', DELETE:'delete'], but you would need to modify those methods slightly
}

"/$controller/$action?/$id?"{
...
5. Make a few small edits to the BookController, adding the Grails JSON converter and editing save() , list(), and show() methods to return the persisted objects as JSON.
    import grails.converters.JSON;
   ...
    def save() {
        def bookInstance = new Book(params)
        if (!bookInstance.save(flush: true)) {
            render(view: "create", model: [bookInstance: bookInstance])
            return
        }
        render bookInstance as JSON
    }
    def list(Integer max) {
        params.max = Math.min(max ?: 10, 100)
        render Book.list(params) as JSON;
    }

    def show(Long id) {
        def bookInstance = Book.get(id)
        if (!bookInstance) {
            flash.message = message(code: 'default.not.found.message', args: [message(code: 'book.label', default: 'Book'), id])
            redirect(action: "list")
            return
        }

        render bookInstance as JSON;
    }

6. Start your app, grails run-app
7. Post to the application
  1. Open Postman, enter URL request here: http://localhost:8080/jason/book
  2. Select "Post", since we are adding a new Book object
  3. Select Header button
    1. Header: Content-Type
    2. Value: application/json or text/json
  4. Select "raw" from the parameter options (form-data, x-www-form..., raw)
  5. Enter the following in the textbox provided
    {"title": "Programming Grails", "author": "Burt Beckwith"}
  6. Click Send
  7. You should receive a response like:
    {
      "class": "example.Book",
      "id": 1,
      "author": "Burt Beckwith",
      "title": "Programming Grails"
    }
8. Repeat step 8 with different JSON parameters, such as
{"title": "The Definitive Guide to Grails", "author": "Graeme Rocher"}
9. "Get" a Book by id from the application.  Because we mapped the GET action to 'show' in the UrlMappings, we should get the Book by id.
  1. Open Postman, enter URL request here: http://localhost:8080/jason/book/1
    • Alternatively, you could make the url http://localhost:8080/jason/book and add a URL param id=1
  2. Select "Get", since we are retrieving an existing Book object
  3. Select Header button
    1. Header: Content-Type
    2. Value: application/json or text/json
  4. Click Send
  5. You should receive a response like:
    {
      "class": "example.Book",
      "id": 1,
      "author": "Burt Beckwith",
      "title": "Programming Grails"
    }
10. "Get" all Books from the application.  Because we mapped the GET action to "list" in the UrlMappings, we should get all the Books in the application.
  1. Open Postman, enter URL request here: http://localhost:8080/jason/book/
  2. Select "Get", since we are retrieving an existing Book object
  3. Select Header button
    1. Header: Content-Type
    2. Value: application/json or text/json
  4. Click Send
  5. You should receive a response like:
[
    {
        "class": "example.Book",
        "id": 1,
        "author": "Burt Beckwith",
        "title": "Programming Grails"
    },
    {
        "class": "example.Book",
        "id": 2,
        "author": "Graeme Rocher",
        "title": "The Definitive Guide to Grails"
    }
]

11. You can also check that this is working using the in the Grails dbconsole
  • http://localhost:8080/jason/dbconsole
  • By default, the JDBC URL is jdbc:h2:mem:devDb;MVCC=TRUE;LOCK_TIMEOUT=10000, but if you changed it in conf/datasource.groovy, change this value accordingly





Friday, June 7, 2013

org.xml.sax.SAXParseException; ... The processing instruction target matching "[xX][mM][lL]" is not allowed.

In an attempt to test the XML output of a Grails service, I created the control string for my xml unit test as follows:
def testXML = '''
<?xml version='1.0' standalone='no'?>
  <!DOCTYPE labels SYSTEM "label.dtd">
  <labels _FORMAT='labelFormat' >
    <label>
      <variable name='study'>my_study</variable>
      <variable name='visit'>my_visit</variable>
    </label>
  </labels>
'''
This generated the following exception:
org.xml.sax.SAXParseException; lineNumber: 2; columnNumber: 9; The processing instruction target matching "[xX][mM][lL]" is not allowed.
  at com.sun.org.apache.xerces.internal.parsers.DOMParser.parse(DOMParser.java:251)
    com.sun.org.apache.xerces.internal.jaxp.DocumentBuilderImpl.parse(DocumentBuilderImpl.java:300)
    org.custommonkey.xmlunit.XMLUnit.buildDocument(XMLUnit.java:383)
    org.custommonkey.xmlunit.XMLUnit.buildDocument(XMLUnit.java:370)
    org.custommonkey.xmlunit.Diff.<init>(Diff.java:101)
    org.custommonkey.xmlunit.Diff.<init>(Diff.java:93)
After some searching I found a StackOverflow question that lead me to the problem. If you specify processing instruction (PI) for your XML in a groovy mulit-line string, you must start the xml content on the first line of the string, because "Whitespace is not allowed between the opening less-than character and the element tagname or between the prefix, colon, and local name of an element or attribute.". So the xml string in the beginning should be:
def testXML = '''<?xml version='1.0' standalone='no'?>
   <!DOCTYPE labels SYSTEM "label.dtd">
   <labels _FORMAT='labelFormat' >
    <label>
     <variable name='study'>my_study</variable>
     <variable name='visit'>my_visit</variable>
    </label>
   </labels>
  '''
or simply add an forward slash after the opening quotes '''/, like
def testXML = '''/
<?xml version='1.0' standalone='no'?>
   <!DOCTYPE labels SYSTEM "label.dtd">
   <labels _FORMAT='labelFormat' >
    <label>
     <variable name='study'>my_study</variable>
     <variable name='visit'>my_visit</variable>
    </label>
   </labels>
  '''