In my previous post, I described an approach for binding data from uploaded files into entity instances. One important part was left out from this post – validation.
Since the play framework supports JPA as a persistence mechanism out of the box, and defining a constraint once beats defining it twice, it makes sense to re-use the JPA annotations to provide on-the-fly validation during the binding process.
First up, we need a class that describes the JPA constraints on a field. Keeping it simple, here we have nullable and maximum length along with the name of the field as given in the @be.objectify.led.Property annotation.
public class FieldDescriptor { public String name; public boolean nullable = true; public int maxLength = -1; }
Next, we need something that checks the model for @Property-annotated fields. Again, I’m keeping this simple by only looking at @Column annotations, but you can have @MaxSize, @OneToOne, @ManyToOne, @Required, etc. – any possible annotation. In fact, they don’t even need to be JPA annotations if you have some other way of indicating constraints.
public class ConstraintsParser { public static Map<String, FieldDescriptor> parseConstraints(Class clazz) { Map<String, FieldDescriptor> fieldDescriptors = new HahsMap<String, FieldDescriptor>(); for (Field field : clazz.getDeclaredFields()) { if (field.isAnnotationPresent(Property.class)) { FieldDescriptor fieldDescriptor = new FieldDescriptor(); Property ledProperty = field.getAnnotation(Property.class); fieldDescriptor.name = ledProperty.value(); if (field.isAnnotationPresent(Column.class)) { Column column = field.getAnnotation(Column.class); fieldDescriptor.nullable = column.nullable(); fieldDescriptor.maxLength = column.length(); } fieldDescriptors.put(fieldDescriptor.name, fieldDescriptor); } } return fieldDescriptors; } }
In order to validate values during binding, we need to implement a be.objectify.led.factory.ValidationFunction.
public class MyValidationFunction implements ValidationFunction { private Map<String, FieldDescriptor> fieldDescriptors; public MyValidationFunction(Map<String, FieldDescriptor> fieldDescriptors) { this.fieldDescriptors = fieldDescriptors; } public void validate(String propertyName, String propertyValue) throws ValidationException { FieldDescriptor fieldDescriptor = fieldDescriptors.get(propertyName); if (!fieldDescriptor.nullable && StringUtils.isEmpty(propertyValue)) { throw new ValidationException(propertyName, String.format("% is required", propertyName)); } if (fieldDescriptor.maxLength != -1 && propertyValue.length() > fieldDescriptor.maxLength) { throw new ValidationException(propertyName, String.format("%s has a maximum length of [%d] but was [%d]", fieldDescriptor.maxLength, propertyValue.length())); } } }
Finally, going back to the ExcelBinder class from the previous post we change the bind method to add the validation.
public static <T> List bind(File file, Class<T> modelType) throws ValidationException { jxl.Workbook workbook = jxl.Workbook.getWorkbook(file); jxl.Sheet sheet = workbook.getSheet(0); List<String> headerNames = getHeaderNames(workbook); Map<String, FieldDescriptor> fieldDescriptorMap = ConstraintsParser.parseConstraints(modelType); ValidationFunction validator = new MyValidationFunction(fieldDescriptorMap); List objects = new ArrayList(); // iterate over each non-header row and make it into an object for (int i = 1; i < sheet.getRows(); i++) { PropertyContext propertyContext = getPropertyContext(sheet, i, headerNames); PropertySetter propertySetter = new PropertySetter(context); T t = modelType.newInstance(); propertySetter.process(t, validator); objects.add(t); } return objects; }
With this, we've added a generic way of validating the input without having to resort to anything as boring as writing it by hand and it will always be in sync with your model!