Making Java Groovy

21 December 2015

There is a book of exactly the same title from manning.com. I don't have it yet but father Christmas will put it underneath my tree this Christmas.

What I do have is Spring Boot in Action which covers mixing Java with Groovy. This is what this post is all about. I am going to create a Spring Boot project but will primarily use groovy.

Creating the project

$ mkdir paluwagan
$ cd paluwagan
$ mkdir static
$ spring init --dependencies web,h2,data-jpa,jdbc,groovy-templates --build gradle --packaging jar --extract
Using service at https://start.spring.io
Project extracted to '/home/drmanalo/workspace/paluwagan'

By default, the package belongs to com.demo and the main application is called DemoApplication. A quick refactor will allow you to change to a more meaningful package or application class name.

build.gradle

apply plugin: "groovy"
apply plugin: "spring-boot"
apply plugin: "application"
apply plugin: "jar"

sourceCompatibility = "1.6"
targetCompatibility = "1.8"

version = "${version}"
group = "${group}"
mainClassName = "com.manalo.PaluwaganApplication"

buildscript {
    ext {
        springBootVersion = '1.3.0.RELEASE'
    }    
    repositories {
        mavenCentral()
    }
    dependencies {
        classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
    }
}

war {
    baseName = 'paluwagan'
    version =  '1.0.0'
}

repositories {
    mavenCentral()
}

dependencies {
    runtime "com.h2database:h2"
    compile "org.codehaus.groovy:groovy"
    compile "org.codehaus.groovy:groovy-templates"
    compile "org.webjars:startbootstrap-sb-admin-2:1.0.8-1"
    compile "org.springframework.boot:spring-boot-starter-web"
    compile "org.springframework.boot:spring-boot-starter-actuator"
    compile "org.springframework.boot:spring-boot-starter-data-jpa"
    compile "org.springframework.boot:spring-boot-starter-redis"
    compile "org.springframework.boot:spring-boot-starter-tomcat"
    compile "org.springframework.boot:spring-boot-starter-security"
    compile "org.springframework.session:spring-session:1.0.2.RELEASE"
    testCompile "org.springframework.boot:spring-boot-starter-test"
    testCompile "org.spockframework:spock-core:1.0-groovy-2.4"
    testCompile "org.spockframework:spock-spring:1.0-groovy-2.4"
    testRuntime "cglib:cglib-nodep:3.2.0"
}

application.properties

I want to avoid the default port 8080 therefore I'm changing it to a different port.

# general
server.port=8000

This stops Hibernate from generating my tables which is a very good feature but if you're planning to use Flyway or Liquibase in the future, it is best to set ddl-auto to none. The standard Hibernate property values are none, validate, update, create, or create-drop.

# Java Persistence API
spring.jpa.generate-ddl=false
spring.jpa.hibernate.ddl-auto=none

application-dev.properties

# logger
logging.level.*=INFO
logging.level.com.manalo.*=DEBUG

# Hypersonic web console
spring.h2.console.enabled=true
spring.h2.console.path=/h2

If you want more control to your schema and its data, you will need schema.sql and data.sql. By convention, you can have schema-{platform} and data-{platform} if you are using more than one relational database.

# datasource
spring.datasource.initialize=true
spring.datasource.platform=h2
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.password=password
spring.datasource.username=testuser
spring.datasource.url=jdbc:h2:mem:test;MODE=PostgreSQL;USER=${spring.datasource.username};PASSWORD=${spring.datasource.password};

schema-h2.sql

create table user (
    id       identity,
    username varchar(20)  not null,
    password varchar(255) not null,
    fullname varchar(100)
);

data-h2.sql

insert into user (id, username, password, fullname) values (1, 'admin', 'secret', 'James Bond');

SpringBootApplication

The default DemoApplication is written in java where the equivalent groovy class is like this.

package com.manalo

import org.springframework.boot.SpringApplication
import org.springframework.boot.autoconfigure.SpringBootApplication

@SpringBootApplication
class PaluwaganApplication {

    static void main(String[] args) {
        SpringApplication.run(PaluwaganApplication.class, args)
    }

}

Data Access Layer

A very simple way of using what Spring Data offers.

package com.manalo.repository

import com.manalo.domain.User
import org.springframework.data.jpa.repository.JpaRepository

interface UserRepository extends JpaRepository<User, Integer> {

    User findByUsername(String username)

}

Service Layer

A simple CRUD service

package com.manalo.service

import com.manalo.domain.User
import com.manalo.repository.UserRepository
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.stereotype.Service

import javax.persistence.criteria.CriteriaBuilder.In

@Service
class UserService {

    @Autowired
    private UserRepository userRepository

    Collection<User> list() {
        userRepository.findAll()
    }

    User findById(Integer id) {
        userRepository.findOne(id)
    }

    User save(User user) {
        if (user.id) { // update
            def existing = this.findById(user.id)
            existing.with {
                username = user.username
                password = user.password
                userRepository.save(existing)
            }
        } else { // create
            userRepository.save(user)
        }
    }

    void delete(User user) {
        def existing = this.findById(user.id)
        if (existing != null) {
            userRepository.delete(existing)
        }
    }
}

Domain Object

package com.manalo.domain

import javax.persistence.Entity
import javax.persistence.GeneratedValue
import javax.persistence.Id

@Entity
class User {

    @Id
    @GeneratedValue
    Integer id

    String username
    String password
}

Controller

package com.manalo.controller

import com.manalo.service.UserService
import com.manalo.domain.User
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.web.bind.annotation.ModelAttribute
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RequestMethod
import org.springframework.web.servlet.ModelAndView
import org.springframework.stereotype.Controller;

@Controller
@RequestMapping("/user")
class UserController {

    @Autowired
    private UserService userService

    @RequestMapping("/list")
    ModelAndView list() {
        return new ModelAndView('views/user/list', [users: userService.list()])
    }

    @RequestMapping("/add")
    ModelAndView add() {
        return new ModelAndView("views/user/edit", [user: new User()])
    }

    @RequestMapping("{id}")
    ModelAndView view(@PathVariable("id") Integer id) {
        return new ModelAndView("views/user/edit", [user: userService.findById(id)])
    }

    @RequestMapping(value = "save", method = RequestMethod.POST)
    ModelAndView save(User user) {
        userService.save(user)
        return new ModelAndView("redirect:/user/list")
    }

    @RequestMapping(value = "delete", method = RequestMethod.POST)
    ModelAndView delete(User user) {
        userService.delete(user)
        return new ModelAndView("redirect:/user/list")
    }
}

View: Main Groovy Template

yieldUnescaped '<!DOCTYPE html>'
html {
    head {
        title(pageTitle)
        link rel: "stylesheet", href: "/webjars/bootstrap/3.3.6/css/bootstrap.min.css"
        link rel: "stylesheet", href: "/webjars/font-awesome/4.5.0/css/font-awesome.min.css"
        link rel: "stylesheet", href: "/webjars/startbootstrap-sb-admin-2/1.0.8-1/css/sb-admin-2.css"
        link rel: "stylesheet", href: "/webjars/metisMenu/1.1.3-1/metisMenu.min.css"
    }
    body {
        div(id: "wrapper") {
            nav(class: "navbar navbar-default navbar-static-top", role: "navigation", style: "margin-bottom: 0") {
                div(class: "nav-header") {
                    button(type: "button", class: "navbar-toggle", "data-toggle": "collapse", "data-target": ".navbar-collapse") {
                        span class: "sr-only", "Toggle navigation"
                        span class: "icon-bar", ""
                        span class: "icon-bar", ""
                        span class: "icon-bar", ""
                    }
                    a class: "navbar-brand", href: "index.html", "Paluwagan"
                }
                ul(class: "nav navbar-top-links navbar-right") {
                    li(class: "dropdown") {
                        a(class: "dropdown-toggle", "data-toggle": "dropdown", href: "#") {
                            i class: "fa fa-user fa-fw", ""
                            i class: "fa fa-caret-down", ""
                        }
                    }
                }
                div(class: "navbar-default sidebar", role: "navigation") {
                    div(class: "sidebar-nav navbar-collapse") {
                        ul(class: "nav", id: "side-menu") {
                            li {
                                a href: "/user/list", "Users"
                            }
                        }
                    }
                }
            } // end nav

            mainBody()
        } // end wrapper

        script src: "/webjars/jquery/1.11.1/jquery.min.js", ""
        script src: "/webjars/bootstrap/3.3.6/js/bootstrap.min.js", ""
        script src: "/webjars/metisMenu/1.1.3-1/metisMenu.min.js", ""
        script src: "/webjars/startbootstrap-sb-admin-2/1.0.8-1/js/sb-admin-2.js", ""
    } // end body
}

View: edit.tpl

layout "layouts/main.tpl",
pageTitle: "Paluwagan :: Manage User",
mainBody: contents {
    div(id: "page-wrapper", style: "min-height: 450px") {
        div(class: "row") {
            div(class: "col-lg-12") {
                h3 class: "page-header", "Manage User"
                form(id: "editForm", action: "/user/save", method: "post", class: "form-horizontal") {
                    input name: "id", type: "hidden", value: user.id ?: ""
                    div(class: "form-group") {
                        label for: "username", "Username", class: "col-sm-2 control-label"
                        div(class: "col-sm-3") {
                            input name: "username", type: "text", value: user.username ?: ""
                        }
                    }
                    div(class: "form-group") {
                        label for: "password", "Password", class: "col-sm-2 control-label"
                        div(class: "col-sm-3") {
                            input name: "password", type: "password", value: user.password ?: ""
                        }
                    }
                    div(class: "form-group") {
                        div(class: "col-sm-offset-2 col-sm-3") {
                            button type: "submit", class: "btn btn-default", "Submit"
                        }
                    }
                }
            }
        }
    }
}

View: list.tpl

layout 'layouts/main.tpl',
pageTitle: 'Paluwagan :: Users',
mainBody: contents {
    div(id: "page-wrapper", style: "min-height: 450px") {
        div(class: "row") {
            div(class: "col-lg-12") {
                h3 class: "page-header", "List of Users"
                table(class: "table table-striped") {
                    thead {
                        tr {
                            th "Username"
                            th "Password"
                            th "Manage"
                        }
                    }
                    tbody {
                        users.each { user ->
                            tr {
                                td "$user.username"
                                td "$user.password"
                                td {
                                    a(href: "$user.id") {
                                        i class: "fa fa-pencil", " Edit "
                                    }
                                }
                            }
                        }
                    }
                }
                div {
                    a(href: "/user/add") {
                        i class: "fa fa-plus", " Add user "
                    }
                }
            }
        }
    }
}

Testing using Spock

On my next post I will detail how I wrote the unit and integration tests for my repository, service and controller Groovy classes.