I'm completely new to Groovy, but have many years of Java experience, and recently I have started working on a Grails application, currently it contains the following:
- User and Person domain classes
- UserService service class
- UserController controller class to offer a REST API
This application is written using Groovy 2.4.3 and Grails 3.0.2.
I'd like to have this code reviewed and if possible extra attention on the transition from Java to Groovy and on the mixture of Grails and Spring code.
domain/User.groovy
class User {
static constraints = {
email email: true, unique: true
person nullable: true
registrationDate nullable: true
loginDate nullable: true
}
String email
String password
Person person
Instant registrationDate
Instant loginDate
}
domain/Person.groovy
class Person {
static constraints = {
middleName nullable: true
city nullable: true
street nullable: true
houseNumber nullable: true
zipCode nullable: true
email email: true, nullable: true
bankAccount nullable: true
}
static hasMany = [
telephoneNumbers: String
]
String firstName
String middleName
String lastName
String city
String street
String houseNumber
String zipCode
String email
List<String> telephoneNumbers
String bankAccount
}
conf/spring/resources.groovy
beans = {
clock(Clock) { bean ->
bean.factoryMethod = "systemDefaultZone"
}
}
services/UserService.groovy
@Transactional
class UserService {
Clock clock
User createUser(String firstName, String middleName, String lastName, String email, String password) {
if (User.countByEmail(email)) {
throw new CreateUserException("EMAIL_IN_USE")
}
def salt = BCrypt.gensalt(12)
def encodedPassword = BCrypt.hashpw(password, salt)
def user = new User(email: email, password: encodedPassword, registrationDate: Instant.now(clock))
def person = new Person(firstName: firstName, middleName: middleName, lastName: lastName)
person.save()
user.person = person
user.save()
}
User loginUser(String email, String password) {
def user = User.findByEmail(email)
if (!user) {
throw new LoginUserException("EMAIL_NOT_FOUND")
}
if (!BCrypt.checkpw(password, user.password)) {
throw new LoginUserException("PASSWORD_INCORRECT")
}
user.loginDate = Instant.now(clock)
user.save()
}
}
@InheritConstructors
class CreateUserException extends RuntimeException { }
@InheritConstructors
class LoginUserException extends RuntimeException { }
controllers/UserController.groovy
@RequestMapping("/api/user")
class UserController {
UserService userService
@RequestMapping("/create_user")
def @ResponseBody createUser(@RequestBody CreateUserMessage createUserMessage) {
try {
userService.createUser(*createUserMessage.values())
def builder = new JsonBuilder()
builder {
success true
}
} catch (CreateUserException ex) {
def builder = new JsonBuilder()
builder {
success false
message ex.message
}
}
}
@RequestMapping("/login_user")
def @ResponseBody loginUser(@RequestBody LoginUserMessage loginUserMessage) {
if (session.user) {
def builder = new JsonBuilder()
return builder {
success false
message "ALREADY_LOGGED_IN"
}
}
try {
def user = userService.loginUser(*loginUserMessage.values())
session.user = user
session.token = UUID.randomUUID().toString()
def builder = new JsonBuilder()
builder {
success true
token session.token
}
} catch (LoginUserException ex) {
def builder = new JsonBuilder()
builder {
success false
message ex.message
}
}
}
@RequestMapping("/logout_user")
def @ResponseBody logoutUser(@RequestBody Object object) {
if (!session.user) {
def builder = new JsonBuilder()
return builder {
success false
message "NOT_LOGGED_IN"
}
}
session.user = null
session.token = null
def builder = new JsonBuilder()
builder {
success true
}
}
}
@TupleConstructor
class CreateUserMessage {
String firstName
String middleName
String lastName
String email
String password
List values() {
return [firstName, middleName, lastName, email, password]
}
}
@TupleConstructor
class LoginUserMessage {
String email
String password
List values() {
return [email, password]
}
}
As you can see I'm also injecting a Clock
instance as bean in order to facilitate running the server on a different timezone if needed and to facilitate unit testing dates.
The repository can be found here. It also includes lots of unit tests, but I'm not posting those up for review right now, I might do it at a later point in time or if someone makes a special request for it.