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.