Loading...

November 20, 2015

Ratpacked: Validating Forms

Ratpack is a lean library to build HTTP applications. Ratpack for example doesn't include functionality to validate forms that are submitted from a web page. To add form validation to our Ratpack application we must write our own implementation.

Let's write a simple application with a HTML form. We will use Hibernate Validator as a JSR 303 Bean Validation API implementation to validate the form fields. IN our application we also use the MarkupTemplateModule so we can use Groovy templates to generate HTML. We have a simple form with two fields: username and email. The username field is required and the email field needs to have a valid e-mail address. The following class uses annotations from Hibernate Validator to specify the constraints for these two fields:

// File: src/main/groovy/com/mrhaki/ratpack/validation/User.groovy
package com.mrhaki.ratpack.validation

import groovy.transform.CompileStatic
import groovy.transform.ToString
import org.hibernate.validator.constraints.Email
import org.hibernate.validator.constraints.NotEmpty

import javax.validation.constraints.NotNull

@CompileStatic
@ToString(includeNames = true)
class User implements Serializable {
    
    @NotEmpty
    String username

    @Email
    String email

}

Now we write our Ratpack application to handle a form submitted to /submit. To validate our User class we must get a java.validation.Validator instance. We use the buildDefaultValidatorFactory method of the javax.validation.Validation class to create a Validator instance. We then add it to the registry, so handlers can use it when needed.

Next we use the parse method of Context to parse the input to a Form instance. We then use the values from the Form instance to populate our properties in the User class. We validate the User instance and get back a set of ConstraintViolation objects if there any validation errors. In our logic we display the validation errors on the HTML page, if there are no errors a next page is shown.

// File: src/ratpack/Ratpack.groovy
import com.mrhaki.ratpack.validation.User
import ratpack.form.Form
import ratpack.groovy.template.MarkupTemplateModule

import javax.validation.ConstraintViolation
import javax.validation.Validation
import javax.validation.Validator

import static ratpack.groovy.Groovy.groovyMarkupTemplate
import static ratpack.groovy.Groovy.ratpack

ratpack {

    bindings {
        // Add module to support Groovy templates.
        module(MarkupTemplateModule)
        
        // Create Validator instance to be used
        // in the handlers.
        bindInstance(Validator, Validation.buildDefaultValidatorFactory().validator)
    }

    handlers {
        // Handle submitted data. The closure
        // argument has the Validator instance
        // we created in the bindings{} block.
        post('submit') { Validator validator ->
            
            // Parse the values that are submitted to
            // a Form object.
            parse(Form)
                .map { Form form ->
                    // Transform Form to a User object
                    // and set the username and email 
                    // properties.
                    new User(
                        username: form.username,
                        email: form.email)
                }
                .then { User user ->
                    // Validate the User object.
                    final Set<ConstraintViolation<User>> constraintViolations =
                            validator.validate(user)

                    if (constraintViolations.size() > 0) {
                        // If there are violations we convert them
                        // to a map where the keys are the field names
                        // and the value the ConstraintViolation object.
                        // We use this in our template to display the errors.
                        final Map<String, ConstraintViolation<User>> formErrors =
                                constraintViolations.collectEntries { violation ->
                                    [(violation.propertyPath.toString()): violation]
                                }
    
                        // Render page so we can display errors.
                        render groovyMarkupTemplate('index.gtpl', user: user, formErrors: formErrors)
                    } else {
                        // Everything was ok, redirect to
                        // different page.
                        redirect("thankyou?username=${user.username}")
                    }
                }
        }
        
        get('thankyou') {
            // Show page with the value of request parameter username.
            final String username = request.queryParams.username
            render groovyMarkupTemplate('thankyou.gtpl', username: username)
        }

        get { 
            // Show default page.
            render groovyMarkupTemplate('index.gtpl', user: new User())
        }
        
        files {
            dir "public"
        }
    }

}

With the following two Groovy templates we can render the HTML we want:

// File: src/ratpack/template/index.gtpl
yieldUnescaped '<!DOCTYPE html>'
html {
    head {
        meta(charset:'utf-8')
        title("Ratpack: Form validation")
    
        link(href: '/images/favicon.ico', rel: 'shortcut icon')
    }
    body {
        section {
            h2 'Form'
            form(action: 'submit', method: 'POST') {
            
                def inputFields = [
                    username: [type: 'text', value: user.username],
                    email: [type: 'email', value: user.email]
                ]
                
                inputFields.each { fieldName, fieldConf ->
                    div(style: formErrors?.get(fieldName) ? 'color: red' : '') {
                        label(for: fieldName, "${fieldName.capitalize()}: ")
                        input(
                            type: fieldConf.type, 
                            value: fieldConf.value, 
                            name: fieldName, 
                            id: fieldName)
                            
                        if (formErrors?.get(fieldName)) {
                            yield(formErrors[fieldName].message)
                        }
                    }
                }
                
                div {
                    input(type: 'submit', value: 'Submit')
                }
            }
        }
    }
}
// File: src/ratpack/template/thankyou.gtpl
yieldUnescaped '<!DOCTYPE html>'
html {
    head {
        meta(charset:'utf-8')
        title("Ratpack: Thank you")
    
        link(href: '/images/favicon.ico', rel: 'shortcut icon')
    }
    body {
        section {
            h2 'Thank you'
            
            p "You have used our Ratpack application to register as ${username}."
        }
    }
}

What is left is to add the dependencies for Hibernate Validator to our project.

// File: build.gradle
plugins {
    id 'io.ratpack.ratpack-groovy' version '1.1.1'
    id 'com.github.johnrengelman.shadow' version '1.2.2'
}

repositories {
    jcenter()
}

dependencies {
    compile 'org.hibernate:hibernate-validator:5.2.2.Final'
    
    runtime 'javax.el:javax.el-api:2.2.4'
    runtime 'org.glassfish.web:javax.el:2.2.4'
}

We are ready to run our application. Once we have started the application we can open the default page in our browser. If we leave the username empty and use a invalid email value we get the following response when we submit the form:

Written with Ratpack 1.1.1.