Show Navigation

Grails Events

Grails Events to handle a common scenario. A user registers himself in an application and the app sends the user a welcome email.

Authors: Sergio del Amo

Grails Version: 3.3.1

1 Getting Started

In this guide, you are going to use Grails Events to handle a common scenario. A user registers in an application and the app sends the user a welcome email. For example, asking him to verify his email address. We are going to trigger the Email Notification by publishing an event when the user is registered.

1.1 What you will need

To complete this guide, you will need the following:

  • Some time on your hands

  • A decent text editor or IDE

  • JDK 1.7 or greater installed with JAVA_HOME configured appropriately

1.2 How to complete the guide

To get started do the following:

or

The Grails guides repositories contain two folders:

  • initial Initial project. Often a simple Grails app with some additional code to give you a head-start.

  • complete A completed example. It is the result of working through the steps presented by the guide and applying those changes to the initial folder.

To complete the guide, go to the initial folder

  • cd into grails-guides/grails-events/initial

and follow the instructions in the next sections.

You can go right to the completed example if you cd into grails-guides/grails-events/complete

2 Writing the Application

Grails 3.3 introduces a new Events API that replaces the previous implementation that was based on Reactor 2.x (which is no longer maintained and deprecated).

In Grails 3.3 and above a new EventBus abstraction has been introduced. Like the PromiseFactory notion, there are implementations of the EventBus interface for common asynchronous frameworks like GPars and RxJava.

Your project already contains the events dependency:

build.gradle
    compile "org.grails.plugins:events"

Grails Events capabilites documentation can be found in async.grails.org.

2.1 Domain Classes

Add two domain classes.

grails-app/domain/demo/User.groovy
package demo

class User {
    String firstName
    String lastName
    String email

    static constraints = {
        firstName nullable: false
        lastName nullable: false
        email nullable: false, email: true, unique: true
    }
}
grails-app/domain/demo/Notification.groovy
package demo

class Notification {
    String email
    String subject

    static constraints = {
        subject nullable: false
        email nullable: false, email: true
    }
}

2.2 Data Services

Introduced in GORM 6.1, Data Services take the work out of implemented service layer logic by adding the ability to automatically implement abstract classes or interfaces using GORM logic.

Add two data services classes for the previous domain classes.

grails-app/services/demo/UserService.groovy
package demo

import grails.gorm.services.Service
import groovy.transform.CompileStatic

@Service(User)
@CompileStatic
interface UserService {
    int count()
    void deleteByEmail(String email)
}
grails-app/services/demo/NotificationService.groovy
package demo

import grails.gorm.services.Service
import groovy.transform.CompileStatic

@Service(Notification)
@CompileStatic
interface NotificationService {
    int count()
    void deleteByEmail(String email)
}

2.3 Url Mapping

Modify UrlMappings to map the endpoints which handle User registration.

grails-app/controllers/demo/UrlMappings.groovy
        "/signup"(controller: 'register', action: 'index')
        "/register"(controller: 'register', action: 'save')

/signup presents a registration form.

/register handles a POST submission which saves a user.

2.4 View

Create a GSP file which contains the user registration form.

grails-app/views/register/index.gsp
<html>
<head>
    <title>Sign Up</title>
    <meta name="layout" content="main" />
    <style type="text/css">
        ol li {
            list-style-type: none;
            margin: 10px auto;
        }
        ol li label {
            width: 120px;
        }
    </style>
</head>
<body>
<div id="content" role="main">
    <g:hasErrors bean="${signUpInstance}">
    <ul class="errors">
        <g:eachError bean="${signUpInstance}">
            <li><g:message error="${it}"/></li>
        </g:eachError>
    </ul>
    </g:hasErrors>
    <p class="message">${flash.message}</p>

    <h1><g:message code="register.title" default="Register Form"/></h1>
    <g:form controller="register" action="save" method="POST">
        <ol>
            <li>
                <label for="firstName">First Name</label>
                <g:textField name="firstName" id="firstName" value="${signUpInstance.firstName}" />
            </li>
            <li>
                <label for="lastName">Last Name</label>
                <g:textField name="lastName" id="lastName" value="${signUpInstance.lastName}" />
            </li>
            <li>
                <label for="email">Email</label>
                <g:textField name="email" id="email" value="${signUpInstance.email}" />
            </li>
            <li>
                <g:submitButton name="Submit" id="submit"/>
            </li>

        </ol>

    </g:form>
</div>
</body>
</html>

2.5 Services

Add a service which handles the registration of a user in the database.

grails-app/services/demo/RegisterService.groovy
package demo

import grails.events.annotation.Publisher
import grails.gorm.transactions.Transactional
import grails.validation.ValidationException
import groovy.transform.CompileStatic
import org.springframework.context.MessageSource
import org.springframework.dao.DuplicateKeyException

@CompileStatic
class RegisterService {

    MessageSource messageSource

    @Transactional
    @Publisher (1)
    User saveUser(User user) {
        user.save(failOnError: true) (2)
        user
    }

    SaveUserResult register(RegisterCommand cmd, Locale locale) {
        User user = cmd as User
        try {
            saveUser(user)
            return new SaveUserResult(message: userSavedMessage(cmd.email, locale))
        } catch (ValidationException | DuplicateKeyException validationException) {
            return new SaveUserResult(error: userSavedErrorMessage(cmd.email, locale))
        }
    }

    String userSavedErrorMessage(String email, Locale locale) {
        messageSource.getMessage('user.saved.error',
                [email] as Object[],
                "Error while saving user with ${email} email address",
                locale)
    }

    String userSavedMessage(String email, Locale locale) {
        messageSource.getMessage('user.saved',
                [email] as Object[],
                "User saved with ${email} email address",
                locale)
    }
}

@CompileStatic
class SaveUserResult {
    String message
    String error
}
1 Annotate the method with @Publisher. The event Id uses the method name saveUser
2 Raise an exception if the validation fails. This rollbacks the transaction. If the transaction is rollbacked, no event is triggered.

We consume the event in a different service. The consumer is completely decoupled from the sender. In a real application you will probably send an email to the user who just registered asking him to verify his email address. In this guide, we save a Notification in the database.

grails-app/services/demo/WelcomeEmailService.groovy
package demo

import grails.events.annotation.Subscriber
import grails.gorm.transactions.Transactional
import groovy.transform.CompileStatic
import groovy.util.logging.Slf4j

@Slf4j
@CompileStatic
class WelcomeEmailService {

    @Transactional
    @Subscriber (1)
    void saveUser(User user) {
        Notification notification = new Notification(email: user.email, subject: 'Welcome to Grails App')
        // TODO Send Email
        if ( !notification.save() ) {
            log.error('unable to save notification {}', notification.errors.toString())
        }
    }
}
1 To consume an event use the Subscriber annotation. The method name saveUser is used by default for the event id, although it can start with the word "on". In other words either a method name of saveUser or onSaveUser would work for the this guide example.

2.6 Controller

Create a Command Object to handle the form data binding and validation.

grails-app/controllers/demo/RegisterCommand.groovy
package demo

import grails.validation.Validateable

class RegisterCommand implements Validateable {
    String firstName
    String lastName
    String email

    static constraints = {
        firstName nullable: false
        lastName nullable: false
        email nullable: false, email: true
    }

    Object asType(Class clazz) {
        if (clazz == User) {
            def user = new User()
            copyProperties(this, user)
            return user
        }
        else {
            super.asType(clazz)
        }
    }

    def copyProperties(source, target) {
        source.properties.each { key, value ->
            if (target.hasProperty(key) && !(key in ['class', 'metaClass'])) {
                target[key] = value
            }
        }
    }
}

Create a controller with two actions, one action presents the registration form the other action handles the form submission.

grails-app/controllers/demo/RegisterController.groovy
package demo

import groovy.transform.CompileStatic

@CompileStatic
class RegisterController {

    static allowedMethods = [index: 'GET', save: 'POST']

    RegisterService registerService

    def index() {
        [signUpInstance:  new RegisterCommand()]
    }

    def save(RegisterCommand cmd) {

        if ( cmd.hasErrors() ) {
            render view: 'index', model: [signUpInstance: cmd]
            return
        }

        SaveUserResult result = registerService.register(cmd, request.locale)
        flash.message = result.message
        if ( result.error ) {
            flash.error = result.error
            render view: 'index', model: [signUpInstance: cmd]
            return
        }
        redirect action: 'index'
    }
}

2.7 Testing

Create a functional test which verifies that a Notification instance is created, due to the event triggering, when a User is created.

src/integration-test/groovy/demo/RegisterControllerSpec.groovy
package demo

import geb.spock.GebSpec
import grails.gorm.transactions.Rollback
import grails.testing.mixin.integration.Integration
import spock.lang.IgnoreIf

@IgnoreIf({ !System.getProperty('geb.env') })
@Integration
class RegisterControllerSpec extends GebSpec {

    NotificationService notificationService

    UserService userService

    @Rollback
    def "If you signup a User, an Event triggers which causes a Notification to be saved"() {
        when: 'you signup with a non existing user'
        go '/signup'
        $(name: "firstName") << 'Sergio'
        $(name: "lastName") << 'del Amo'
        $(name: "email") << '[email protected]'
        $('#submit').click()

        then: 'the user gets created and a notification is saved due to the event being triggered'
        userService.count() == old(userService.count()) + 1
        notificationService.count() == old(notificationService.count()) + 1

        when: 'you try to signup with a user which is already in the database'
        go '/signup'
        $(name: "firstName") << 'Sergio'
        $(name: "lastName") << 'del Amo'
        $(name: "email") << '[email protected]'
        $('#submit').click()

        then: 'The user is not saved and no event gets triggered'
        noExceptionThrown()
        userService.count() == old(userService.count())
        notificationService.count() == old(notificationService.count())


        cleanup:
        userService.deleteByEmail('[email protected]')
        notificationService.deleteByEmail('[email protected]')
    }
}

3 Running the Tests

To run the tests:

./grailsw
grails> test-app
grails> open test-report

or

./gradlew check
open build/reports/tests/index.html