Inline field validation in Scala/Lift using JPA and JSR 303
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> </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 } }
Comments are closed.
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.
Hi Timothy
It is a fairly trivial example – so please go ahead and cut n paste 🙂
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 . . .
Unfortunately you have to write bean getters. My understanding is that this is a restriction of the JSR 303 implementation.