4/07/2008

Objects as Functions Part II: A lightweight web app validation utility

Binding and validation is something almost all web frameworks must have a solution for. Some are more elegant than others, but the problem with most of them is that they are tied specifically to the use of that framework. I've yet to see a reusable utility that is framework agnostic for handling this. This is another take on Objects as Functions post I did a while back. Only now I'm applying it to binding and valiation for the web. The results are usable by any Java developer using any framework they want. Creating yet another idea "From Frameworks to Object Oriented Utilities."

I had some code that I had written a while back where I coded the binding and validation by hand. In other words it was just a bunch of if else statement ladders. It resembled something like the following:


setEmail( request.getParameter("email") );

List<String> errors = new ArrayList<String>();

if( isNotSpecified( getEmail() ) ) {
errors.add("Email is missing.");
} else if( isNotSpecified( confirmEmail ) ) {
errors.add("Confirm email is missing.");
} else if( validateEmailFormat( getEmail() ) ) {
errors.add("Email address provided is not a valid.");
} else if( validateEmailFormat( confirmEmail ) ) {
errors.add("Confirm Email address provided is not a valid.");
} else if( !confirmEmail.equals( getEmail() ) ) {
errors.add("Email and Confirm Email did not match.");
}

return errors;


Ok so yikes! I just like jumped back 10 years by writing code like that! But, I did it because I was in a situation where the "architect" hadn't really thought about these problems, and hadn't picked a framework that gave us that ability. So most people weren't doing any validation, and very poor binding. Think Vietnam of web apps here.

So after I wrote this code once I knew I needed something better, but it wasn't until I was about to write it again that I decided to go back and try to refactor out the common code into a utility to make my job easier that I came up with a general solution. If you'll notice in that code above there are some handy instance methods I created in this class to help specifying the validation language. So I started pulling those common methods out into a separate class. I'll spare you the details of the refactoring for another blog post. I'll start with some simple examples:


public List<String> validateAndBind( RequestValidater validater ) {
setFirstName( validater.param("firstName").require().toValue() );
return validater.getErrors();
}


This first example simply validates that the parameter "firstName" was specified in the request, fetches that value, and binds it into the instance object using a setter method. You'll notice there is no reflection taking place here. I'm a huge fan of reflection, but I think you'll see that this is so easy you actually don't need it. Remember that even in reflective frameworks you have to specify the validation rules, and specifying the binding (i.e. making you call the setter method by hand) isn't really where all the hard work is.

Going into detail on what this does. The first step requests the "firstName" parameter from the validater object. Then it calls the require() method on it. This methods checks to see if the parameter is present if not it adds a default error message. The RequestValidater object keeps track of all the errors it encounters while executing the validation rules. Finally toValue() method returns that parameter's value as a String passing it to the object's setter method. If the value isn't present it simply returns null.

In this simple example, if firstName parameter was missing then it would create an error message like: "First Name is missing". Because the parameter used camel case (i.e. firstName) the validater can infer a display name from that by breaking apart the parameter's name on the capital letter boundaries. So "firstName" would become "First Name". You can override this by supplying a second parameter to the param() method. Like:


setFirstName( validater.param("firstName", "First name").require().toValue() );


It will also infer using underscores as well (i.e. "first_name" = "First Name"). It's better to accept the default since it's less work, but realize that you can customize it if you so wish. The second way is to supply an actual error message in the require() method as a parameter. While this might be necessary sometimes, particularly with the matches() method, it's usually best to accept the defaults.

Here's another example that validates and binds a date object.


public List<String> validateBindings( RequestValidater validater ) {
setBirthDate( validater.param("birthDate").require().toDate() );

return validater.getErrors();
}



The key difference in this example is the call to toDate() rather than toValue(). The to*() methods convert strings into other values like integer, dates, etc. These methods will usually end the validation rule methods. You can also pass a default value into the to*() methods to provide a default date, integer, etc. Of course you wouldn't do that with a require() validation rule provided.

Here is a couple more examples:


public List<String> validateBindings( RequestValidater validater ) {
setUsername( validater.param("username").require().between( 5, 30).toValue() );
setEmail( validater.param("email").require().validateEmail().equals( validater.param("confirmEmail") );

return validater.getErrors();
}


This example we see some more methods for performing validations. The between() method is used to validate a parameter's length is between the two values. If not it adds an error message. You can see in the email example. Two methods validateEmail() which makes sure the value conforms to an email address, and the equals method which tests to see if the value matches some other value. In this example you can see how the validater.param("confirmEmail") can be used again to refer to another parameter in the request.

Finally, there's a matches() method for making sure parameters conform to a regular expression. Here is an example of that:


public List<String> validateAndBind( RequestValidater validater ) {

Pattern phoneNumber = Pattern.compile("\\(?\\d\\d\\d\\)?(-|\\s)\\d\\d\\d(-|\\s)\\d\\d\\d\\d");

setPhonNumber( validater
.param("phoneNumber")
.require().matches( phoneNumber, "Phone Number provided does not look like a phone number.")
.toValue() );

return validater.getErrors();
}



Here's how you can use the RequestValidater in your controllers:


MyObject obj = new MyObject();
List<Errors> errors = obj.validateBindings( new RequestValidater( request ) );
if( errors.isEmpty() ) {
// no errors means the user's request was valid
} else {
// we have some errors so send them back with the form data.
}



After I finished writing this utility I went back and refactored by code to use it. I had something like 100-150 lines of validation code that I reduced to a simple 8 lines of code. Actually I added some new lines and formatting between some of the chained method calls which inflated it to like 25 lines, but still that's an amazing amount of code reduction. And, that doesn't include the lines I would've written to do validation in the second object.

This is yet another example of how you can use Objects in Java as functions to really change how you can reuse code. Notice that I didn't create some static method utility class to do this because there was state being kept and managed inside RequestValidater for me. If I had a static method I'd have to keep track of the state for me. Even the error messages can be standardized across my entire app by using this class. Also notice that the fact I'm using binding and validating against HttpServletRequest is hidden from my model objects. This is another great example of how encapsulation hides the details of the system from my model objects. Something static utilties can't do for me. Why is this important? Well I didn't go into it, but I also made changes to the RequestValidater such that it's easy to use in unit tests by just instantiating it with a HashMap of parameters. That makes it really easy to automate your validation testing because your model object's aren't bound to the HttpServletRequest interface. Without using encapsulation it wouldn't have been that easy to reuse RequestValidater in a different context. You can see an example in the main method included in the source of how to reuse it in unit tests.

Finally, my last thought on this is that it's a single class. There is no framework you have to adopt to use this code. Java has many choices when it comes to web frameworks. It's both a blessing and a curse, but the reality of the matter is most people are using Struts! Yuck! Why continue to build code that no one else can use? This marks the time when we need to move away from frameworks and to utilities.

Now here's the source:


package com.cci.web.validation;

import javax.servlet.http.HttpServletRequest;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.regex.Pattern;

public class RequestValidater {
private HttpServletRequest request;
private Map<String,String> params;
private List<String> errors = new ArrayList<String>();

public RequestValidater(HttpServletRequest request) {
this.request = request;
}

public RequestValidater( Map<String,String> params ) {
this.params = params;
}

public boolean hasErrors() {
return !errors.isEmpty();
}

public List<String> getErrors() {
return errors;
}

public Parameter param( String name ) {
return new Parameter( name );
}

public Parameter param( String name, String displayName ) {
return new Parameter( name, displayName );
}

protected String getParameter( String name ) {
if( request != null ) {
return request.getParameter(name);
} else {
return params.get(name);
}
}

public class Parameter {
private String displayName;
private String name;
private String value;

public Parameter(String name) {
this.name = name;
this.displayName = convertToDisplay( name );
this.value = getParameter(name);
}

public Parameter(String name, String displayName) {
this.name = name;
this.displayName = displayName;
this.value = getParameter(name);
}

private String convertToDisplay(String camelCase) {
StringBuilder builder = new StringBuilder();
if( !camelCase.contains("_") ) {
builder.append( Character.toTitleCase( camelCase.charAt(0) ) );
for( int i = 1; i < camelCase.length(); i++ ) {
char next = camelCase.charAt(i);

if(Character.isUpperCase( next ) ) {
builder.append( ' ' );
}
builder.append( next );
}
} else {
String[] words = camelCase.split("_");
for( String word : words ) {
builder.append( Character.toUpperCase( word.charAt(0) ) );
builder.append( word.subSequence( 1, word.length() ) );
}
}
return builder.toString();
}

public Parameter require() {
return require( displayName + " is missing." );
}

public Parameter require(String error ) {
if( name == null || name.length() < 1 ) {
errors.add( error );
}
return this;
}

public Parameter between( int minSize, int maxSize ) {
return between( minSize, maxSize, displayName + " must be at least " + minSize + " characters, but no more than " + maxSize + " characters.");
}

public Parameter between( int minSize, int maxSize, String error ) {
if( value == null ) return this;

if( value.length() < minSize || value.length() > maxSize ) {
errors.add( error );
}
return this;
}

public Parameter matches( Pattern pattern, String error ) {
if( value == null ) return this;

if( !pattern.matcher( value ).matches() ) {
errors.add( error );
}
return this;
}

public Parameter validateAsEmail() {
if( value == null ) return this;

if( !value.matches("(\\w|\\.)+@\\w+\\.\\w+(\\.\\w+)*") ) {
errors.add( value + " is not a valid email address.");
}
return this;
}

public Parameter equals( Parameter param ) {
return equals( param.value, displayName + " does not match " + param.displayName + "." );
}

public Parameter equals( String aValue, String error ) {
if( value == null ) return this;

if( !value.equals( aValue ) ) {
errors.add( error );
}
return this;
}

public Date toDate() {
return toDate( "MM/dd/yyyy");
}

public Date toDate( String datePattern ) {
return toDate( datePattern, value + " is not a valid date. (" + datePattern + ")" );
}

public Date toDate( String datePattern, String error ) {
if( value == null ) return null;

try {
SimpleDateFormat dateFormat = new SimpleDateFormat( datePattern );
return dateFormat.parse( value );
} catch( ParseException pex ) {
errors.add( error );
return null;
}
}

public Integer toInt() {
return toInt( (Integer)null );
}

public Integer toInt( Integer defaultVal ) {
return toInt( displayName + " must be a number without a decimal point.", defaultVal );
}

public Integer toInt( String error ) {
return toInt( error, null );
}

public Integer toInt( String error, Integer defaultValue ) {
if( value == null ) return defaultValue;

try {
return Integer.parseInt( value );
} catch( NumberFormatException nex ) {
errors.add( error );
return null;
}
}

public String toValue() {
return value;
}

public String toValue( String defaultValue ) {
return value != null ? value : defaultValue;
}
}

public static void main(String[] args) {
Map<String,String> params = new HashMap<String,String>();
params.put("email", "jep1957@mindspring.com" );
params.put("email1", "this.email@bad" );
params.put("email2", "my address@bad.com" );

validateThese( "jep1957@mindspring.com", "charlie.hubbard@coreconcept.com", "this.email@bad", "my address@bad.com", "bad@bad@bad@bad", "hiya", "foo.bar" );
}

private static void validateThese( String... emails ) {
for( String email : emails ) {
Map<String,String> params = new HashMap<String,String>();
params.put("email", email );

RequestValidater validater = new RequestValidater( params );
String val = validater.param("email").validateAsEmail().toValue();
System.out.println( val + " was " + ( validater.hasErrors() ? "not valid!" : "valid" ) );
}
}
}

No comments: