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!

Leave a Reply

Your email address will not be published. Required fields are marked *