dimanche févr. 21, 2010

Just say no to Scarlett Johansson, Brad Pitt and the rest (but say yes to Bean Validation)!

This blog has moved to alexismp.wordpress.com
Follow the link for the most up-to-date version of this blog entry.

Every single application with user interaction needs some sort of validation. Java EE 6 now has its very own standard Bean Validation API (JSR 303) with a number of easy-to-use built-in constraints (@NotNull, @Size, ...). GlassFish v3 ships with the Hibernate Validator reference implementation which offers some additional annotations such as @Email. I've decided for the sake of this blog entry that it was not good enough for me as it considers null, empty and foo@barcom as all valid (and does the check as part of the ConstraintValidator, not in the definition of the @Email annotation).

So here's my new implementation of a constraint to check for valid email addresses with the additional feature of rejecting dummy data, such as angelina.jolie@foo.bar (too bad it it was the real one trying to post a comment to your blog or mine!). In my implementation the annotation constraint is called @EmailAddress and defined as follows :

@Size(min=5, message="{foo.bar.min_size}")
@NotNull(message="{foo.bar.cannot_be_null}")
@Pattern(regexp="[a-z0-9!#$%&'\*+/=?\^_`{|}~-]+(?:\\\\.[a-z0-9!#$%&'\*+/=?\^_`{|}~-]+)\*" +
                 "@" +
                 "(?:[a-z0-9](?:[a-z0-9-]\*[a-z0-9])?\\\\.)+[a-z0-9](?:[a-z0-9-]\*[a-z0-9])?",
          message="{foo.bar.fails_regexp_validation}")
@Constraint(validatedBy=EmailValidator.class)
@Documented
@Target({ElementType.ANNOTATION_TYPE, ElementType.METHOD, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface EmailAddress {
     String message() default "{foo.bar.invalid_email}";
     Class[] groups() default {};
     Class[] payload() default {};
}

Things to note:
• A constrained value can fail one or more validation tests in no particular order. Each failure will be reported and available upon validation. In particular you cannot assume that the data passed to the constraint validator will not be null (and the use of @NotNull could arguably be removed since nullability is tested in the validation code below).
• I'm using the @Pattern annotation here rather than manipulating the regex in the explicit validation code (the regex itself comes from a slightly simplified version of the one defined by RFC 2822).
• error messages use externalized strings located in a resources bundle ValidationMessages.properties file.

Here's the rather straightforward validation code (see @Constraint above) :

public class EmailValidator implements ConstraintValidator<EmailAddress, String> {
     ArrayList<String> blackListedDomains;
     ArrayList<String> blackListedNames;

     public void initialize(EmailAddress constraintAnnotation) {
         // a better implementation would probably retrieve data
         // from a file, a database or a web service.
         blackListedDomains = new ArrayList<String>();
         blackListedDomains.add("guerrillamail.com");
         blackListedDomains.add("mailinator.com");
         blackListedDomains.add("foo.com");
         blackListedDomains.add("foo.bar");

         blackListedNames = new ArrayList<String>();
         blackListedNames.add("james.bond");
         blackListedNames.add("jamesbond");
         blackListedNames.add("nicole.kidman");
         blackListedNames.add("brad.pitt");
         blackListedNames.add("scarlett.johansson");
         blackListedNames.add("angelina.jolie");
         blackListedNames.add("foo");
         blackListedNames.add("bar");
         blackListedNames.add("foo.bar");
         blackListedNames.add("foobar");
         blackListedNames.add("toto");
         blackListedNames.add("titi");
     }

     public boolean isValid(String value, ConstraintValidatorContext context) {
         int at = value.indexOf('@');
         if (value == null || "".equals(value) || at<0 ) return false;

         String username = value.substring(0, at).toLowerCase();
         String domainname = value.substring(at+1).toLowerCase();

         if ( blackListedDomains.contains(domainname) ) return false;
         if ( blackListedNames.contains(username)) return false;

         return true;
     }
}

The main thing to notice in the code above is the use of generics in ConstraintValidator interface which in turn defines the argument types in the initialize() and isValid() methods.

This newly defined @EmailAddress constraint can now be applied to class attributes or getters. In the case of JPA entities or JSF attributes there is no extra step, all the glue is provided to validate data upon calling the JPA life-cycle methods and applying the JSF request life-cycle, with error messages surfaced to the user via those two APIs. In other cases, Bean Validation offers API to explicitly validate the data illustrated in this sample unit test :

public class TestEmailConstraint {
    Validator validator;
    Customer myCustomer;

    @Before
    public void setUp() {
       ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
       validator = factory.getValidator();
       myCustomer = new Customer();
    }

    @Test
    public void testEmailBV() {
       myCustomer.setEmail("angelina.jolie@oracle.com");
       Set<ConstraintViolation<Customer>> violations = validator.validate(myCustomer);
       for (ConstraintViolation<Customer> violation : violations) {
          System.out.print(violation.getInvalidValue() + " => ");
          System.out.println(violation.getMessage());
       }
       assertTrue(violations.size() > 0);
    }
}

About

This blog has moved

Alexis Moussine-Pouchkine's Weblog

GlassFish - Stay Connected

Search

Archives
« avril 2014
lun.mar.mer.jeu.ven.sam.dim.
 
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
    
       
Today
Blogroll

No bookmarks in folder

News

No bookmarks in folder