Validating Groovy Properties

A lot of Groovy developers rave about Groovy properties and automatically generated getters and setters, and how much better they are than Java beans.

Here is a Java bean:

package info.shelfunit.properties.sample

public class Book {
    
    public Book() { 
    }
    
    private int pages;
    private String title;
    private int year;
    
    public int getPages() { return pages; }
    public void setPages( int pagesArg ) { pages = pagesArg; }
    
    public String getTitle() { return title; }
    public void setTitle( String titleArg ) { title = titleArg; }
    
    public int getYear() { return year; }
    public void setYear( int yearArg ) { year = yearArg; }
}

Here is the same class written in Groovy:

package info.shelfunit.properties.sample

class Book {
    
    int pages
    String title
    int year
}

The fields are made private by default in Groovy. A no-argument constructor is generated for you behind the scenes, as are getters and setters. So instead of

book.setTitle("War and Peace")

You could just write

book.title = 'War And Peace'

That is nice, but there is one problem I always had with that: There is no validation on your setters. It is no different than having your fields public. What if I want my integer field to be between 10 and 100? What if I want my String to be at least 4 characters? In Groovy you could write your own setter, but that gets old fast. Grails has some constraints. Why not do something similar for Groovy?

There are a few projects out there for Java that use javax.validation.constraints.* annotations, like Hibernate Validator. I tried that, but I could not get it to work in Groovy. So I decided to do a little metaprogramming and write some annotations and try it myself.

Here is my integer annotation:

package info.shelfunit.properties.annotations

import java.lang.annotation.Retention
import java.lang.annotation.Target

import java.lang.annotation.ElementType
import java.lang.annotation.RetentionPolicy

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)

public @interface IntAnnotation {
  public int minValue() default 0
  public int maxValue() default 2147483647 // Integer.MAX_VALUE as int
}

Using “Integer.MAX_VALUE” caused a compile error, so I decided to bite the bullet and hard-code it. Here is my String annotation:

package info.shelfunit.properties.annotations

import java.lang.annotation.Retention
import java.lang.annotation.Target

import java.lang.annotation.ElementType
import java.lang.annotation.RetentionPolicy

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)

public @interface StringAnnotation {
  public int minLength() default 0
  public int maxLength() default 2147483647 // Integer.MAX_VALUE
}

All the integer annotation does is set minimum and maximum values, while the String annotation just looks at length.

Here is the class to process the annotations:

package info.shelfunit.properties.annotations

class AnnotationProcessor {
    
    static process( Class theClass ) {
        
        theClass.metaClass.setProperty = { String name, arg ->
            
            def field = theClass.getDeclaredField( name )
            def intAnnotation = field?.getAnnotation( IntAnnotation.class )
            def stringAnnotation = field?.getAnnotation( StringAnnotation.class )
            
            if ( intAnnotation ) {
                if ( ( arg instanceof Integer ) && !( arg < intAnnotation.minValue() ) &&
                    !( arg > intAnnotation.maxValue() ) ) {
                    theClass.metaClass.getMetaProperty( name ).setProperty( delegate, arg )
                }
            } else if ( stringAnnotation ) {
                if ( !( arg.length() < stringAnnotation.minLength() ) &&
                    !( arg.length() > stringAnnotation.maxLength() ) ) {
                    theClass.metaClass.getMetaProperty( name ).setProperty( delegate, arg.toString() )
                }
            } else {
                theClass.metaClass.getMetaProperty( name ).setProperty( delegate, arg ) // this works
            }
        }
        
    } // end process
}

Here is the Book class with the annotations:

package info.shelfunit.properties.sample

import info.shelfunit.properties.annotations.AnnotationProcessor
import info.shelfunit.properties.annotations.IntAnnotation
import info.shelfunit.properties.annotations.StringAnnotation

class Book {
    
    static { 
        AnnotationProcessor.process( Book.class ) 
    }
    
    @IntAnnotation( minValue = 30, maxValue = 400 )
    def pages
    @StringAnnotation( minLength = 5, maxLength = 20 )
    String title
    int year
}

There is a static block that calls the class to process the annotations. I tried to use AST transformations, but after a while I just decided to do it in a static block.

The only catch is that you might want to set an integer field as a def type. If you leave it as “int”, the no-arg constructor will set it to 0. That is fine if that is your minimum value. I want the minimum to be 30. Having the field “def” allows for null values.

This works with the no-arg constructor as well as the map constructor. I have not tried it with the TupleConstructor annotation. I do not plan on adding a @Null annotation; AnnotationProcessor.process only processes one annotation per field, and I want to keep things simple.

I will add long, double and float. I don’t know what I will do about dates, since dates were overhauled in JDK 8. I might add some regex stuff to the String annotation.

The code is on github here. Let me know what you think.

 

 

1 thought on “Validating Groovy Properties”

Comments are closed.