Tuesday, February 1, 2011

Creating a custom Struts2 validator

Recently I was working on a project which had a very specific validation requirement, that the Struts2 validation framework did not currently support. I had a dynamic collection of objects, and I needed to make sure that an attribute of that object was distinct across the entire collection.

I could have added a validate method to my action class and perform the validation there manually, but I wanted to keep everything in the xml validation. So I used the opportunity to learn how to write a custom Struts2 validator.

As it turns out, it is not nearly so complex as it sounds. To start things off, I created a new class that extended FieldValidatorSupport, since I wanted this validator to be a field validator. I then overrode the "validate" method, which does the actual validation. So here would be the very basic validator:
package my.package.name;

import com.opensymphony.xwork2.validator.ValidationException;
import com.opensymphony.xwork2.validator.validators.FieldValidatorSupport;

public class UniqueCollectionValidator extends FieldValidatorSupport
{ 
    @Override
    public void validate(Object object) throws ValidationException
    {
    }
}

I am going to jump ahead now and show you how to hook up said validator, because that comes into play in building the validator. So, first step in wiring up a custom interceptor is adding a file called validators.xml to your project's classpath. The contents of the file should look like this:

    


Second step is just referencing the validator. Open up your action's validation.xml file and add the following validator underneath the field you want it to apply to:


That is everything needed to wire up your custom validator. Now we get to the fun part - making your validator, you know, validate something. First off, and this is the part that needed the wiring in place to properly show, is adding parameters. As it turns out, you can add as many parameters to your validator as needed, and it does not take much. In this case, we need as a parameter the name of the attribute on the object that we need to be distinct.

First step to adding a parameter is to add the private member variable to your validator class, and give it getters and setters, like so:
package my.package.name;

import com.opensymphony.xwork2.validator.ValidationException;
import com.opensymphony.xwork2.validator.validators.FieldValidatorSupport;

public class UniqueCollectionValidator extends FieldValidatorSupport
{
    String uniqueFieldName = "";

    public String getUniqueFieldName()
    {
        return uniqueFieldName;
    }

    public void setUniqueFieldName(String uniqueFieldName)
    {
        this.uniqueFieldName = uniqueFieldName;
    }

    @Override
    public void validate(Object object) throws ValidationException
    {
    }
}

Once we have getters and setters in our validator, we populate them by simply adding those elements to our xml validator.

    <param name="uniqueFieldName">myUniqueFieldName</param>


All that is left now is filling in the details of the validator.
package my.package.name;

import com.opensymphony.xwork2.validator.ValidationException;
import com.opensymphony.xwork2.validator.validators.FieldValidatorSupport;

public class UniqueCollectionValidator extends FieldValidatorSupport
{
    String uniqueFieldName = "";

    public String getUniqueFieldName()
    {
        return uniqueFieldName;
    }

    public void setUniqueFieldName(String uniqueFieldName)
    {
        this.uniqueFieldName = uniqueFieldName;
    }

    @Override
    public void validate(Object object) throws ValidationException
    {
        String fieldName = getFieldName();
        Object val = getFieldValue(fieldName, object);
        
        List collection = null;
        List<String> soFar = new ArrayList<String>();
        
        if ( val instanceof List )
        {
            collection = (List) val;
        }
        else
        {
            return;
        }
        
        for ( Object obj : collection )
        {
            String toCheck = "";
            
            if ( this.uniqueFieldName.length() == 0 )
            {
                toCheck = (String) obj;
            }
            else
            {
                toCheck = (String) getFieldValue(this.uniqueFieldName,obj);
            }
            
            if ( soFar.contains(toCheck) )
            {
                addFieldError(fieldName, object);
                return;
            }
            
            soFar.add(toCheck);
        }
    }
}
This is kind of a lot of code to just dump on you, but most of it is pretty straightforward. There are just a few useful little bits to point out.

First off, when you enter a field validator, the object passed in contains the field parameter we are trying to validate against. In order to get the specific field we are dealing with, we use two member functions of FieldValidatorSupport - getFieldName and getFieldValue.
String fieldName = getFieldName();
Object val = getFieldValue(fieldName, object);
This gets for you the name of the field that we are validating against, and the current value of that field.

This validator can only work against List objects, so the first thing I do after that is make sure that it is a List, and cast the val appropriately. If it is not a List, then I just return without validating anything.
if ( val instanceof List )
{
    collection = (List) val;
}
else
{
    return;
}

At that point, I am able to move forward with my logic for determining uniqueness. The only other key piece of information here is the way to fail validation. If you hit the case where your validation fails, you can simply do the following:
addFieldError(fieldName, object);
return;
This will tell Struts that validation failed on this field, and it will use the normal method of choosing what message to display from the validation xml file.

That's all there is to it.

8 comments:

  1. Nice article.
    It would also be cool to implement the same using annotations.

    ReplyDelete
  2. veggen,

    Thanks for the feedback. I have never really used the Struts2 annotations, but I may try looking into it sometime.

    ReplyDelete
  3. Unfortunately, what you've done is tie reusable business-logic validation to the S2 framework.

    @veggen: doing this with validations is trivial; the same validator is used, and its name used in the annotation (instead of one of the "stock" validator names).

    On the minutiae side, put accessors at the bottom of your source: they are the thing I care about the absolute *least*, and don't deserve to be in the way of finding source that actually matters.

    ReplyDelete
  4. To add client side validation, modify form-close-validate.ftl

    ReplyDelete
  5. Excellent article !
    Very clear and complete explaination.

    Thanks for your time and effort !
    Razvan

    ReplyDelete
  6. Excellent example!

    I have done exactly same, but my custom validator never been called. :(

    Don't know, what am I missing!

    ReplyDelete