Spring Controller With Groovy HTML Generation

I was working on a small Java POC and wanted to be able to quickly generate some HTML, so I decided to try Groovy's MarkupBuilder. In theory it meant that I had a single file that handle the entire view, instead of shipping the rendering work off to another file like a JSP. Pretty sure this was not the wisest move ever, but it was an interesting experiment.

The code was in a Spring Boot app, so I created a Controller with a couple of URL mappings. The data being displayed is coming from an XML file, so there are some references to Groovy's XmlParser as well.

The renderHtml method creates the HTML, HEAD, and BODY tags and accepts additional page contents as a closure. In the example, there are two different pages that can be generated, both of which share the same style declarations because they both use renderHtml.

As I said, I don't think I'd do this in an actual application, but it is a way of creating an entire page's contents in a single file.


@RestController
class TableController {
  @Autowired
  ManuscriptCatalog catalog
  
  @RequestMapping(value="/manuscripts/{manuscript}/tables")
  String displayTable(@PathVariable("manuscript") String manuscriptId) {
    def manuscript = new XmlParser().parse(
  new File(catalog.getManuscript(manuscriptId).@url))
    def tableXmls = manuscript.depthFirst().findAll {it.name() == 'table'}

    return renderHtml { builder ->
      builder.h2(manuscript.properties[0].@caption.toString())
      builder.ul {
        tableXmls.each { tableXml ->
          String tableName = tableXml.@id.toString()
          li { a(href: "/manuscripts/$manuscriptId/tables/$tableName",
                       tableName) }
        }
      }
    }
  }
  
  @RequestMapping(value="/manuscripts/{manuscript}/tables/{table}")
  String displayTable(
    @PathVariable("manuscript") String manuscriptId,
    @PathVariable("table") String tableName) {
 
    def manuscript = new XmlParser().parse(
      new File(catalog.getManuscript(manuscriptId).@url))
    def tableXml = manuscript.depthFirst().find {
      it.name() == 'table' && it.attributes()['id'] == tableName}
    def table = Table.parseTableData(tableXml)
    
    return generateTableHtml(table)
  }
  
  private String renderHtml(Closure pageContents) {
    StringWriter out = new StringWriter()
    def builder = new MarkupBuilder(out)
    builder.html {
      head {
        link(rel: 'stylesheet',
       href: 'http://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css')
        style( '''\
          .table-nonfluid {
            width: auto !important;
          }
          th, .key { background-color: #669900; font-weight: bold; color: white }
          tr:hover {
            background-color: #a6a6a6 !important;
          }
        ''')
      }
      builder.body {
        pageContents(builder)
      }
    }
    return out.toString()
  }
  /**
   * Generate HTML based on the tables structure
   */
  private String generateTableHtml(Table table) {
    return renderHtml { builder ->
      builder.h2(table.name)
      builder.table('class' : "table table-condensed table-bordered table-nonfluid") {
        thead {
          table.colKeys.eachWithIndex { key, index ->
            tr {
              if (index == table.colKeys.size() - 1) {
                table.rowKeys.collect { rowKey -> th(rowKey.name) }
              } else {
                th(colspan: table.rowKeys.size())
              }
              key.repeated.times {
                key.values.each { keyValue ->
                  th(colspan : key.span, keyValue)
                }
              }
            }
          }
        }
        tbody {
          table.rows.eachWithIndex { row, rowIndex ->
            tr {
              row.keys.eachWithIndex { key, keyIndex ->
                if (table.rowKeys[keyIndex].span == 1
      || rowIndex % table.rowKeys[keyIndex].span == 0) {
                  td('class' : 'key', rowspan : table.rowKeys[keyIndex].span, key)
                }
              }
              row.values.collect { val -> td(val) }
            }
          }
        }
      }
    }
  }
}

Post a Comment