Skip to content

Inline field validation in Scala/Lift using JPA and JSR 303

December 7, 2009

In my experiments with Lift and JPA, there didn’t seem to be a way to display field validation errors next to the field itself (I believe Mapper and Record do this out of the box).  The default approach is to use S.error() to collect up errors and display them all at the bottom of the form. For a large form it is difficult for a user to spot and correct mistakes.  I also wanted to be able to use JSR 303 annotations to perform validation

Lift turns out to be quite flexible, and the Lift community is eager to assist newbies with problems. Alex Siman got me going with a nice tip on generating inline error fields using css. After a bit of hacking (I don’t claim this is pretty) I was able to perform validation using JSR 303.

Generating JPA text fields

Text fields are generated using a jpa helper utility. This is what the binding looks like in a snippet:


bind("user", xhtml,
 "id" -> SHtml.hidden(() => requestVar(current)),
 jpaLabeledTextField("nickName", user, user.nickName, user.nickName = _),
 jpaLabeledTextField("firstName", user, user.firstName, user.firstName = _),
 jpaLabeledTextField("lastName", user, user.lastName, user.lastName = _),
 jpaLabeledTextField("email", user, user.email, user.email = _),
 jpaLabeledTextField("mobilePhone", user, user.mobilePhone, user.mobilePhone = _))

This utility generates a label and an html text field. The label text is looked up using the bean property name. If the bean property is annotated with a @Size attribute the value will be used to set the length of the text field. Here is an example of what a generated form looks like:

Nothing fancy here – but you can see the Mobile phone field is shorter than the name fields. This is because we used a @Size annotation on the mobilePhone field.

Validation

Validations are performed using another utility function. For example:

val valid = jpaIsValid(user)
 if( valid ) {
     val newuser = Model.mergeAndFlush(user)
 .....

The jpaIsValid() function runs JSR 303 validations on the object and returns true if the object is valid.  As a side effect this function will call Lift’s S.error() function and assign validation error messages to the appropriate form fields.  When the form is re-displayed  these error messages will rendered alongside each field.

Here is an example of what this looks like in action:

The Utility Code

And now, without further ado is the utility function. I’m a Scala/Lift newbie – so the style is probably not great (suggestions welcome 🙂 )

Apologies for the formatting – I don’t know the wordpress tricks to format this nicely. For some reason wordpress also converts some comments to upper case (no, I was not shouting when I wrote this code..)

package com.nextplz.model

/**
* JPAUtil
*/

import net.liftweb.util.{Log, Helpers}

import Helpers._
import net.liftweb.http.{S, SHtml}
import org.scala_tools.javautils.Implicits._

import xml.{Elem, Text, NodeSeq}
import javax.validation.Validation
import javax.validation.constraints.Size
import reflect.Manifest
import collection.mutable.HashMap

/**
* Utilities to generate text fields  using JPA annotations amd to validate those
* fields based on JSR 303 annotations
*
*/
object JPAUtil  {

val validatorFactory = Validation.buildDefaultValidatorFactory();
val validator = validatorFactory.getValidator();

/**
* Generate a text field binding for an object with a JPA String property.
* This will also generate a field label. The label will be looked up using the bean propertyName as a message key
* If the property is annotated with a JSR 303 @Size annotation - the max size will be used to set the
* length of the generated html input text field.
* <br/>
* Example:  bind("user", xhtml, jpaLabeledTextField("firstName", user, user.firstName, user.firstName = _))
* <br/>
* propertyName - must be a valid Java Bean property name for a mapped JPA String field  (e.g. "firstName" )
* obj - the jpa object
* value - the default value to set the input field to
* assign - function that will be called to set the string value of the field
*
* @author Warren Strange
* @author Alex Siman
*
*/
def jpaLabeledTextField[T <: AnyRef](propertyName:String,
obj:T,     // jpa object. In the future we could use reflection to get the value
value:String,
assign:(String) => Any)(implicit m:Manifest[T]):TheBindParam = {

var sz = getMaxSize(m,propertyName)
if( sz == 0 || sz > 150) sz = 20; // todo: what is a sane default for text field size???
TheBindParam(propertyName, formField(propertyName,
SHtml.text(value, assign, "id" -> generateId(obj,propertyName),"size" -> sz.toString()) ))
}

/**
* Run JSR 303 validations on the passed object.
* If there are violations set error message text on the associated text input field
*
*/
def jpaIsValid[T](obj:T):Boolean = {
val violations = validator.validate(obj)
if( violations == null || violations.size() == 0 )
return true

violations.foreach( violation => {
Log.debug("Constraint violation found=" + violation)
val s =  violation.getPropertyPath().toString()
S.error(generateId(obj,s), violation.getMessage() )
})
return false
}

// generate a unique id to identify this input field
private def generateId[T](obj:T, prop:String) = prop + (obj.hashCode.toString())

// Generate a text form field - along with any inline error messages
// Courtesty of Alex Siman
private def formField(label: String, input: Elem): NodeSeq = {
val fixedLabel = label match {
case "" => ""
case s: String => S.?(s) + ":"
}

val id = (input \ "@id").toString

Log.debug("*** Validate id=" + id + " input=" + input + " S.errors=" + S.errors)

val messageList = S.messagesById(id)(S.errors)
//val messageList = S.messagesById(id)(ferrors)
val hasMessages = messageList.size > 0
val cssClass = if (hasMessages) "error" else ""
val messages = messageList match {
case list: List[NodeSeq] if hasMessages => {
<ul>
{messageList.map(m => <li>
{m}
</li>)}
</ul>
}
case _ => Nil
}

<table class={cssClass} style="width: 100%;">
<tr>
<td style="text-align: right; vertical-align: top; width: 10em;">
<b>
{fixedLabel}
</b> &nbsp;
</td>
<td style="text-align: left;">
{input}{messages}
</td>
</tr>
</table>
}

// for caching size values
private val sizeMap = new HashMap[String, Int]
/**
*  If the object is annotated with a @SIZE constraint, grab
* the size to use for the input field length. Return 0 if no max size has been set,
*
*
*/
private def getMaxSize[T](m:Manifest[T], property:String):Int = {
val c = m.erasure
val s = c.getSimpleName() + property
val cached = sizeMap.get(s)
if( cached.isDefined)
return cached.get

val beanDescriptor = validator.getConstraintsForClass(c)
val pd = beanDescriptor.getConstraintsForProperty(property)

Log.debug("propdescriptor=" + pd + " beanDescrip=" + beanDescriptor + "Constraint=" + constraint)
if( constraint.getAnnotation().isInstanceOf[Size]) {
val size = constraint.getAnnotation.asInstanceOf[Size]
sizeMap.put(s, size.max() )
return size.max()
}
})

}
return 0
}

}
Advertisements
4 Comments
  1. Timothy Maxwell permalink
    February 2, 2010 3:23 pm

    Nice work. I would like to use this code on a commercial website I develop. What is the license for this code? Could you please clarify. Thanks in advance.

    • wstrange permalink*
      February 2, 2010 3:37 pm

      Hi Timothy

      It is a fairly trivial example – so please go ahead and cut n paste 🙂

  2. cody koeninger permalink
    February 5, 2010 9:19 am

    How are you avoiding having to write bean getter methods manually?

    @Size{ val min=1, val max=255 }
    var nickName: String = “”

    will generate a validation exception, and @BeanInfo or @BeanProperty annotations aren’t sufficient.

    var nickName: String = “”
    @Size{ val min=1, val max=255 }
    def getNickName: String = “”

    will of course work but is somewhat ugly . . .

    • wstrange permalink*
      February 10, 2010 5:54 pm

      Unfortunately you have to write bean getters. My understanding is that this is a restriction of the JSR 303 implementation.

Comments are closed.

%d bloggers like this: