How to build and render data in JSON format with Grails

Nowadays, an awful lot of web based applications are using JSON as a principal data to load and render information rapidly. While some applications store data into persistence volume as JSON string, the others still use relational databases as the primary storages. Loading data from these relational databases from backend, then building JSON objects and sending them to frontend are all things a web developer often does on a daily basis.

In this post, I will show you how to build JSON objects and pass them to client, such as JavaScript where you have to use the data for displaying or reprocessing.

To ease reading and absorbing the knowledge I am writing here, let’s play with a simple project. The project aims to retrieve organisms and the number of models classified in each organism from BioModels, then make a a bubble chart using D3js library. Thus, you can conceive the idea from getting data from the backend and plotting data to the frontend.

The graphical representation of the illustrated application

Obviously, the query to fetch all organisms will give you a result in JSON or XML format, so then you can use that format data directly into the view (i.e. frontend) without converting it again. In reality, we often re-process data returned external API before embedding them to internal services. Also, it is very often to obtain data from multiple calls and combine the results before using them in a different place. Moreover, I create a class named Organism to capture the properties such as name, label, taxonomy accession and the number of models for the specific purpose in this post.

Note: This project was built in grails 2.5.6, therefore, it would be valuable if you have experienced and familiarised with it before making your hands dirty.

Create Organism class

Now, we need to fetch from EBI Search Server data related to BioModels repository. To retrieve data via RESTful API with Grails, I have recently posted an article how to consume web services in Grails which you are free to read and test it. Below is the snippet in BiomodelService to do what has been addressed.

package com.itersdesktop.javatechs.grails

import grails.plugins.rest.client.RestBuilder
import grails.transaction.Transactional

@Transactional
class BiomodelsService {

    def fetchAllOrganisms() {
        final String BM_SEARCH_URL = "https://wwwdev.ebi.ac.uk/ebisearch/ws/rest/biomodels/topterms"
        String queryURL = "${BM_SEARCH_URL}/TAXONOMY?facetfield:label&size=500&format=json"
        println queryURL
        RestBuilder rest = new RestBuilder(connectTimeout: 10000, readTimeout: 100000, proxy: null)
        def response = rest.get(queryURL) {
            accept("application/json")
            accept("application/xml")
            contentType("application/json;charset=UTF-8")
        }
        if (response.status == 200) {
            return response.json
        }
        return null
    }
}

The result we hope to get if the application is running looks like the output of the curl command below.

curl -X GET 'https://wwwdev.ebi.ac.uk/ebisearch/ws/rest/biomodels/topterms/TAXONOMY?facetfield:label&size=500&format=json' -H "application/json"

We expect to receive the output like

{"totalTermCount":1745,"topTerms":[{"text":"9606","docFreq":713},{"text":"4932","docFreq":137},{"text":"10090","docFreq":114},{"text":"40674","docFreq":95},{"text":"131567","docFreq":77},{"text":"562","docFreq":45},{"text":"10114","docFreq":36},{"text":"2759","docFreq":30},{"text":"8355","docFreq":27},{"text":"10116","docFreq":24},{"text":"3702","docFreq":22},{"text":"9986","docFreq":17},{"text":"33090","docFreq":14},{"text":"3701","docFreq":12},{"text":"10141","docFreq":12},{"text":"9615","docFreq":11},{"text":"7711","docFreq":11},{"text":"7227","docFreq":11},{"text":"5691","docFreq":10},{"text":"39107","docFreq":10},{"text":"5141","docFreq":7},{"text":"4896","docFreq":7},{"text":"9913","docFreq":5},{"text":"83333","docFreq":5},{"text":"33208","docFreq":5},{"text":"2242","docFreq":5},{"text":"1773","docFreq":5},{"text":"10088","docFreq":5},{"text":"8782","docFreq":4},{"text":"8292","docFreq":4},{"text":"7742","docFreq":4},{"text":"7215","docFreq":4},{"text":"5658","docFreq":4},{"text":"559292","docFreq":4},{"text":"33154","docFreq":4},{"text":"3193","docFreq":4},{"text":"2208","docFreq":4},{"text":"210","docFreq":4},{"text":"1423","docFreq":4},{"text":"1358","docFreq":4},{"text":"9685","docFreq":3},{"text":"5833","docFreq":3},{"text":"4952","docFreq":3},{"text":"4922","docFreq":3},{"text":"2","docFreq":3},{"text":"11676","docFreq":3},{"text":"11320","docFreq":3},{"text":"10117","docFreq":3},{"text":"10095","docFreq":3},{"text":"9984","docFreq":2},{"text":"9940","docFreq":2},{"text":"9796","docFreq":2},{"text":"9544","docFreq":2},{"text":"7787","docFreq":2},{"text":"746128","docFreq":2},{"text":"70448","docFreq":2},{"text":"6678","docFreq":2},{"text":"6618","docFreq":2},{"text":"64495","docFreq":2},{"text":"6035","docFreq":2},{"text":"5518","docFreq":2},{"text":"5507","docFreq":2},{"text":"5501","docFreq":2},{"text":"5482","docFreq":2},{"text":"5478","docFreq":2},{"text":"5476","docFreq":2},{"text":"5346","docFreq":2},{"text":"5306","docFreq":2},{"text":"5297","docFreq":2},{"text":"5270","docFreq":2},{"text":"5207","docFreq":2},{"text":"520","docFreq":2},{"text":"5180","docFreq":2},{"text":"51453","docFreq":2},{"text":"5062","docFreq":2},{"text":"5061","docFreq":2},{"text":"5057","docFreq":2},{"text":"5037","docFreq":2},{"text":"4959","docFreq":2},{"text":"4924","docFreq":2},{"text":"4897","docFreq":2},{"text":"4837","docFreq":2},{"text":"40563","docFreq":2},{"text":"40559","docFreq":2},{"text":"38033","docFreq":2},{"text":"3704","docFreq":2},{"text":"36914","docFreq":2},{"text":"36911","docFreq":2},{"text":"36630","docFreq":2},{"text":"34099","docFreq":2},{"text":"336810","docFreq":2},{"text":"33188","docFreq":2},{"text":"33178","docFreq":2},{"text":"329376","docFreq":2},{"text":"3055","docFreq":2},{"text":"29883","docFreq":2},{"text":"28985","docFreq":2},{"text":"262014","docFreq":2},{"text":"197043","docFreq":2},{"text":"186490","docFreq":2},{"text":"155892","docFreq":2},{"text":"1496","docFreq":2},{"text":"148305","docFreq":2},{"text":"140110","docFreq":2},{"text":"13684","docFreq":2},{"text":"117187","docFreq":2},{"text":"109871","docFreq":2},{"text":"1063","docFreq":2},{"text":"1047171","docFreq":2},{"text":"104341","docFreq":2},{"text":"9989","docFreq":1},{"text":"9823","docFreq":1},{"text":"9598","docFreq":1},{"text":"9541","docFreq":1},{"text":"9031","docFreq":1},{"text":"85569","docFreq":1},{"text":"83332","docFreq":1},{"text":"7955","docFreq":1},{"text":"7668","docFreq":1},{"text":"7108","docFreq":1},{"text":"70699","docFreq":1},{"text":"7029","docFreq":1},{"text":"672","docFreq":1},{"text":"6499","docFreq":1},{"text":"644223","docFreq":1},{"text":"63577","docFreq":1},{"text":"629395","docFreq":1},{"text":"61434","docFreq":1},{"text":"58853","docFreq":1},{"text":"5850","docFreq":1},{"text":"5791","docFreq":1},{"text":"5782","docFreq":1},{"text":"5548","docFreq":1},{"text":"5544","docFreq":1},{"text":"53533","docFreq":1},{"text":"511145","docFreq":1},{"text":"5076","docFreq":1},{"text":"49990","docFreq":1},{"text":"4930","docFreq":1},{"text":"4929","docFreq":1},{"text":"4894","docFreq":1},{"text":"4892","docFreq":1},{"text":"4547","docFreq":1},{"text":"452646","docFreq":1},{"text":"44689","docFreq":1},{"text":"416870","docFreq":1},{"text":"4113","docFreq":1},{"text":"4097","docFreq":1},{"text":"402676","docFreq":1},{"text":"39782","docFreq":1},{"text":"3847","docFreq":1},{"text":"381512","docFreq":1},{"text":"36420","docFreq":1},{"text":"33169","docFreq":1},{"text":"32524","docFreq":1},{"text":"317","docFreq":1},{"text":"314146","docFreq":1},{"text":"29875","docFreq":1},{"text":"294746","docFreq":1},{"text":"272634","docFreq":1},{"text":"272563","docFreq":1},{"text":"267377","docFreq":1},{"text":"227321","docFreq":1},{"text":"2257","docFreq":1},{"text":"215813","docFreq":1},{"text":"211044","docFreq":1},{"text":"2104","docFreq":1},{"text":"2099","docFreq":1},{"text":"197911","docFreq":1},{"text":"1758","docFreq":1},{"text":"173629","docFreq":1},{"text":"162425","docFreq":1},{"text":"1590","docFreq":1},{"text":"1472294","docFreq":1},{"text":"132504","docFreq":1},{"text":"11292","docFreq":1},{"text":"11276","docFreq":1},{"text":"11103","docFreq":1},{"text":"1034331","docFreq":1},{"text":"10239","docFreq":1},{"text":"10160","docFreq":1},{"text":"101201","docFreq":1},{"text":"10036","docFreq":1},{"text":"10029","docFreq":1},{"text":"10026","docFreq":1},{"text":"1","docFreq":1}]}

From the JSON output, we am going to build a list of Organism object which are rendered as JSON to be sent to views/frontend. The methods are located in BiomodelService under the name toJsonFromString() and buildOrganisms().

def toJsonFromString(final String jsonString) {
    def slurper = new JsonSlurper()
    slurper.parseText(jsonString)
}

 Map buildOrganisms(final String jsonString) {
    def parsedJson = toJsonFromString(jsonString)
    def totalTermCount = parsedJson.totalTermCount as long
    def jsonOrganisms = parsedJson.topTerms
    List topTerms = new ArrayList<Organism>()
    for (def entry : jsonOrganisms) {
        String taxonomy = entry.text
        long count = entry.docFreq
        String name = resolveTaxonomyTerm(taxonomy)
        topTerms.add(new Organism(name: name, 
            taxonomy: taxonomy, count: count))
    }
    return [totalTermCount: totalTermCount, topTerms: topTerms]
}

String resolveTaxonomyTerm(String s) {
   String serviceURL = "https://www.ebi.ac.uk/ebisearch/ws/rest/taxonomy/entry"
   serviceURL = "${serviceURL}/$s?fields=name"
   RestBuilder rest = new RestBuilder(connectTimeout: 10000,   
                               readTimeout: 100000, proxy: null)
   def response = rest.get(serviceURL) {
       accept("application/xml")
       contentType("application/xml;charset=UTF-8")
   }
   if (response.status == 200) {
       return response.xml
   }
   return null
}

def build() {
    def jsonString = '''
{"totalTermCount":1745,"topTerms":[{"text":"9606","docFreq":713},{"text":"4932","docFreq":137},{"text":"10090","docFreq":114}]}
    // Remember to fetch the full JSON string
    def result = buildOrganisms(jsonString)
    def organismsMap = result["topTerms"].collect { entry ->
        [name: "${entry.name} (${entry.taxonomy})" as String,
         count: entry.count as Integer,
         taxonomy: entry.taxonomy as String]
    }
    organismsMap
}

Let’s inspect these methods and understand mechanism behind the scene. At the end of buildOrganism, a list of Organism objects look like

class com.itersdesktop.javatechs.grails.Organism
class com.itersdesktop.javatechs.grails.Organism
class com.itersdesktop.javatechs.grails.Organism
class com.itersdesktop.javatechs.grails.Organism
class com.itersdesktop.javatechs.grails.Organism
class com.itersdesktop.javatechs.grails.Organism

The second important build is the method build where the list of Organism objects will be converted to a map of looked-like-Organism objects including three properties needed to be populated for the chart.

The list of Organism will be populated to the view, i.e. the bubble nut chart presented in the further section. The key thing here is to build POJOs and render them as JSON in JavaScript, i.e. view/client, where the data will be embedded into charts as well as visualisations. Below is the simple snippet of the output in JSON format.

{
  "topTerms": [
    {
      "name": "Homo sapiens (9606)",
      "count": "713"
    },
    {
      "name": "Saccharomyces cerevisiae (4932)",
      "count": "137"
    },
    {
      "name": "Mus musculus (10090)",
      "count": "114"
    },
    {
      "name": "Mammalia (40674)",
      "count": "95"
    },
    {
      "name": "cellular organisms (131567)",
      "count": "77"
    },
    {
      "name": "Escherichia coli (562)",
      "count": "45"
    }
  ]
}

Now, it is the time to render this data in graphical presentation with d3js. Because of making a bubble chart, the topTerms key will be replaced with children. What you can see in the controller and view looks like

def visualise() {
    def organisms = ["children": biomodelsService.build()] as JSON
    render(view: "visualise", model: [organisms: organisms])
 }

The highlights below should be taken more consideration

def organismsMap = result[“topTerms”].collect { entry ->
[name: “${entry.name} (${entry.taxonomy})” as String,
count: entry.count as Integer,
taxonomy: entry.taxonomy as String]
}

And

def organisms = [“children”: biomodelsService.build()] as JSON
<%@ page contentType="text/html;charset=UTF-8" %>
<html>
<head>
    <meta name="layout" content="main"/>
    <title>Visualisation of Organisms from BioModels</title>
    <script type="text/javascript" src="https://d3js.org/d3.v4.min.js"></script>
</head>

<body>
<div id="organismsChart" class="div-center-content"></div>
<g:javascript>
    var dataset = JSON.parse("${organisms}");
    console.log(dataset);
    var width = 600, height = 600, diameter = 600;
    var color = d3.scaleOrdinal(d3.schemeCategory20);
    var bubble = d3.pack(dataset).size([diameter, diameter]).padding(1.5);

    var svg = d3.select("#organismsChart")
        .append("svg")
        .attr("width", width)
        .attr("height", height)
        .attr("class", "bubble");

    var nodes = d3.hierarchy(dataset)
        .sum(function(d) {
            return d.count; });

    var node = svg.selectAll(".node")
        .data(bubble(nodes).descendants())
        .enter()
        .filter(function(d){
        return  !d.children
        })
        .append("g")
        .attr("class", "node")
        .attr("transform", function(d) {
            console.log(d);
        return "translate(" + d.x + "," + d.y + ")";
    });

    node.append("title")
        .text(function(d) {
            return d.data.name + ": " + d.data.count;
    });

    node.append("circle")
        .attr("r", function(d) {
        return d.r;
    })
        .style("fill", function(d,i) {
        return color(i);
    });

    node.append("text")
        .attr("dy", ".2em")
        .style("text-anchor", "middle")
        .text(function(d) {return d.data.name.substring(0, d.r / 3);})
        .attr("font-family", "sans-serif")
        .attr("font-size", function(d) {return d.r/5;})
        .attr("fill", "white");

    node.append("text")
        .attr("dy", "1.3em")
        .style("text-anchor", "middle")
        .text(function(d) { return d.data.count;})
        .attr("font-family",  "Gill Sans", "Gill Sans MT")
        .attr("font-size", function(d) { return d.r/5;})
        .attr("fill", "white");

    d3.select(self.frameElement)
        .style("height", diameter + "px");
    node.on('click', function(d) {
        console.log(d);
        var label = d["data"]["name"];
        var value = d["data"]["count"];
        var taxonomy = d["data"]["taxonomy"];
        var serverURL = "https://www.ebi.ac.uk/biomodels";
        var fixedSearchURL = "/search?domain=biomodels&query=*%3A*+AND+TAXONOMY%3A";
        var prefixSearchURL = serverURL + fixedSearchURL;
        var queryURL = prefixSearchURL + taxonomy;
        window.open(queryURL, '_blank');
    });

</g:javascript>
</body>
</html>

Run ./grailsw to start grails wrapper for downloading grails 2.5.6 framework, then start the application by running run-app from grails prompt. Remember to change your application http port if there are multiple grails applications running on your system.

Output

If the application starts properly, accessing the link http://localhost:8181/build-render-json-demo/biomodels/visualise will take you to the page where the bubble chart is shown like the screenshot below.

Bubble chart representing model distribution on organisms in BioModels

Source Code

The runnable source code of this article has been pushed to bitbucket as the common place for most illustrated projects for our blogs.

Hopefully, this post would bring to you a convenient approach to build and render JSON elements for views in your Grails applications.

All constructive comments are welcomed. If you are willing to contribute financial support for our website, please follow the instructions below.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.