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.