The Bean Validation API in Spring Roo Framework

April 20, 2011

Spring Framework

«»

This article is based on Spring Roo in Action, to be published Summer-2011. It is being reproduced here by permission from Manning Publications. Manning publishes MEAP (Manning Early Access Program,) eBooks and pBooks. MEAPs are sold exclusively through Manning.com. All pBook purchases include free PDF, mobi and epub. When mobile formats become available all customers will be contacted and upgraded. Visit Manning.com for more information. [ Use promotional code 'java40beat' and get 40% discount on eBooks and pBooks ]

Externalizing message configuration

The default message definitions file for implementing custom messages for the Bean Validation Framework, as specified in the JSR, is /ValidationMessages.properties. Let’s create this file in the root of src/main/resources, so we can validate using customized messages. A sample follows.

1
2
3
nullValidationMessage=cannot be null.
	minValidationMessage=must be greater than or equal to {value}
	maxValidationMessage=must be less than or equal to {value}

The updated annotations for the ranking field can reference these names using braces:

1
2
3
4
@NotNull(message = "{nullValidationMessage}")
	@Min(value = 0, message="{minValidationMessage}")
	@Max(value = 5, message="{maxValidationMessage}")
	private int ranking;

Replacing the default message for a given annotation is also easy. First, using SpringSource Tool Suite, CTRL/CMD-Click on the @Max annotation to view the source code. You will see a line that looks like this:

1
String message() default "{javax.validation.constraints.Min.message}";

Your customized message can replace the default, built-in message by defining a replacement message for the javax.validation.constraints.Min.message field.

1
2
3
4
5
// the annotated field in a JPA entity:
@Max(value = 5)
private int ranking;
// in ValidationMessages.properties
javax.validation.constraints.Min.message=can be no larger than {value}

What about localizing our messages? That is also simple. Since Roo uses Java resource bundles, we merely have to supply localized versions of our ValidationMessages.properties file, such as ValidationMessages_de_DE.properties.

TESTING LOCALES

Testing your Locales can be an interesting challenge. On the Mac, for example, you can use the System Preferences Languages pane to drag/drop your target language to the top. More information can be found at http://java.sun.com/developer/technicalArticles/J2SE/locale/#using.

So far, we’ve seen how to annotate our entities with validation constraints and how to customize the validation messages, including the default messages for validation.

Custom validation

So, what about writing our own validation annotations? In some cases, the built-in validation annotations will not be enough for our needs. As an example, consider something as complex as ISBN validation. To make this a bit simpler, let’s assume we are using the pre-2007 10-digit ISBN because the 13-digit one is more complex. The 10-digit ISBN checksum is validated by summing each of the first nine numbers in sequence and multiplying it by 10 minus the current position. The sum is divided by 11, and the modulus is taken. The modulus is subtracted from 11, and the result is the checksum.

To build this custom validation, you first have to define a custom validation annotation. In the ISBNCheckDigit annotation in listing 5, we’ll define our validator as a no-argument validation annotation and add the @Pattern and @NotNull annotations, which will require the ISBN is always supplied and exactly 10 digits long.

Listing 5 ISBNCheckDigit validation annotation

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package com.chariot.jpademo.model.validation;
import static java.lang.annotation.ElementType.ANNOTATION_TYPE;
import static java.lang.annotation.ElementType.CONSTRUCTOR;
import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.ElementType.PARAMETER;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import javax.validation.Constraint;
import javax.validation.Payload;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Pattern;
// these are required, and help document the validation to the
// validation engine. Note the validatedBy entry, which ties the
// annotation to a validator
@Documented
@Constraint(validatedBy = ISBNWithCheckDigitValidator.class)
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER })
@Retention(RetentionPolicy.RUNTIME)
@Pattern(regexp = "[0-9]{10}") #1
@NotNull
public @interface ISBNWithCheckDigit {
	String message() default "{message-isbn-valid}"; #2
	Class<?>[] groups() default { }; #3
	Class<? extends Payload>[] payload() default {}; #3
}
 
#1 Add Regular Expression-based pattern validation—10 digits
#2 Externalize message
#3 These are required

The groups() and payload()annotations are required, as are the @Documented, @Target, and @Retention annotations. The Constraint annotation defines what Java Bean will perform our validation for us. The validator class must implement a templated ConstraintValidator interface, which requires us to implement the initialize and isValid methods, as shown in listing 6.

Listing 6 The ISBNCheckDigitValidator

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
package com.chariot.jpademo.model.validation;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import javax.validation.constraints.AssertTrue;
import org.apache.log4j.Logger;
public class ISBNWithCheckDigitValidator implements
ConstraintValidator<ISBNWithCheckDigit, String> {
	private static Logger log = Logger
	.getLogger(ISBNWithCheckDigitValidator.class);
	public void initialize(ISBNWithCheckDigit constraintAnnotation) { #1
}
 
@AssertTrue #2
public boolean isValid(String isbn, ConstraintValidatorContext context) {
	log.info("ISBN is " + isbn + " with length of " + isbn.length());
	int checkSumAccum = 0;
	int factor = 10;
	int actualCheckSum = Integer.parseInt(Character
	.toString(isbn.charAt(9)));
	log.info("actual checksum = " + actualCheckSum);
	for (int i = 0; i < isbn.length() - 1; i++) {
		int val = Integer.parseInt(Character.toString(isbn.charAt(i)));
		log.info("value for position " + i + " is " + val + ", factor = "
		+ factor + ", result = " + factor * val);
		checkSumAccum += factor * val;
		factor--;
		log.info("accumulator is now " + checkSumAccum);
	}
	int modulus = checkSumAccum % 11;
	int checkDigit = 11 - modulus;
	log.info("Checksum accum is now modded by 11, and is: " + modulus);
	log.info("The checksum digit is 11 - modulus, which is " + checkDigit);
	return checkDigit == actualCheckSum;
	}
}
 
#1 This method is a required method from the ContraintValidator interface
#2 Perform validation
#3 Accumulate sum of check values
#4 Perform the modulus and subtract

Yes, that was a complex example! In the real world, validation may take place using external resources such as a database or other Spring Beans. Technically, since Roo entities are marked as @Configurable by the @RooEntity annotation, we can also include references to other Spring Beans using the @Autowired annotation. Please be aware of the overhead that the calls to outside beans may incur and don’t try to dig into child collections because they may end up resulting in additional fetches or may throw exceptions.

To wrap up our discussion on validations, let’s look at writing a rule using arbitrary code within our entities. These rules can be defined using the @AssertTrue or @AssertFalse validations.

Using the @AssertTrue validation

Take a very arbitrary example of a popularity-ranking engine. Let’s say you want to delegate your Author popularity check to a Spring Bean, which can be implemented in a number of ways. In one implementation, the author needs to be interrogated, for some strange reason, to make sure anyone with the last name of Rimple or Dickens automatically has a high popularity rating. Pretentious, moi?

In this case, we are going to use another annotation, @AssertTrue, which lets us code the validation inside of our entity object. This validation rule expects to annotate a method named isValid(). During validation, the results of the validation determine whether this validation rule is fired. See listing 7.

Listing 7 Validation within an Entity using @AssertTrue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
...
@Entity
@RooJavaBean
@RooEntity
public class Author {
	...
	@AssertTrue(message="Must be a 5 if it's the guys from Roo in Action!")
	@Transient
	private boolean isValid() {
		if (lastName == null) return true;
		if (lastName.startsWith("Dickens") ||
		lastName.startsWith("Rimple")) {
			return ranking == 5;
		} else {
			return true;
		}
	}
	...
}
 
(#1) Establish the validation with the @AssertTrue annotation. Provide the message for the error, if this method returns false.
(#2) The method that implements @AssertTrue has to have the signature private boolean isValid().

In this example, our isValid() method is annotated with @AssertTrue, using a custom message. Since our method is executed on the instance of the bean itself, it has access to other fields within our entity during the validation process.

Let’s look at an integration test fragment that exercises this logic. Review listing 8 below.

Listing 8 Testing the @AssertTrue validator

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Test
public void testRooInActionAuthorsWithError() {
	AuthorDataOnDemand dod = new AuthorDataOnDemand();
	Author a = dod.getNewTransientAuthor(0);
	// should pass
	a.setRanking(3);
	a.setLastName("Jones");
	a.persist();
	Assert.assertNotNull(a.getId());
	// should throw our exception
	a = dod.getNewTransientAuthor(2);
	a.setRanking(1);
	a.setLastName("Rimple");
	try {
		a.persist();
	} catch (ConstraintViolationException e) {
		return;
	}
	Assert.fail("We should have fired a validation exception.");
}

This example uses the class AuthorDataOnDemand that gets generated using the –testAutomatically flag when we created the entity. Don’t worry if you forget to do this, you can always generate a data-on-demand class using the dod Roo shell command, or get one built for you when you build an integration test using the test integration command.

This mechanism is much easier to deal with than the alternative mechanism for rule-based or multifield-based validation, which is by building validation annotations at the class level. For more details on the Bean Validation API, visit the documentation for JSR-303 online.

IF YOUR UNIT TESTS FAIL AFTER CONFIGURING CUSTOM VALIDATIONS…

If you have a problem with your entity integration tests once you code custom validations, you’re not alone. While this may be corrected in a future release, the JSR-303 validations aren’t completely supported by Roo, and your custom validations will never be. One way to handle this is to open up the DataOnDemand class in your integration test package and implement the getNewTransientEntityName method, replacing it with one that returns valid values for your bean. Spring Roo will remove the method definition from the AspectJ ITD file automatically.

So, if you need to validate your beans before persisting them, you can use the Bean Validation Framework. Try to stick to a few simple rules:

  • Validation by Composition—When building validation for a particular bean, go ahead and stack validators on a field. If you’d like to compose your own grouped validation, just build a validation annotation that is comprised of the others you need. You can get a lot done by using a combination of @NotNull and @Pattern, for example.
  • Be sparing in your processing power—Just because you can call a stored procedure behind a service to validate an entry in that list, do you really want to? Realize that if you’re saving a collection of objects, this validation will be called on each item within the list, thus causing many calls to the same procedure.
  • Use @AssertTrue and isValid() for multicolumn checks—A quick way to get your complex, logic-based validation to work is to build an isValid() method within your entity, annotating it with @AssertTrue. Within this method, you have access to other fields in the entity.

Summary

In this article, we’ve discussed the Bean Validation Framework API and how Spring Roo uses it to validate data. We showed you how to use Bean Validation from Roo and how to customize validation messages and build your own constraints.

email

«»

Comments

comments