Announcement Announcement Module
Collapse
No announcement yet.
ValidationUtil Page Title Module
Move Remove Collapse
X
Conversation Detail Module
Collapse
  • Filter
  • Time
  • Show
Clear All
new posts

  • ValidationUtil

    Hi folks, I have been working with Spring for some time, and I it works pretty well. I am currently working on a public website which must be "idiot-proof", so that involves a lot of validation. My validation-classes were getting bigger and bigger, so I started looking for a simpeler approach to validate empty or blank fields. Of course, org.springframework.validation.ValidationUtils can help you out, but this has limited functionality.

    I like writing code for common use, so I started this little validation framework. I was inspired by Hibernates Criteria approach. I really like to hear what you guys think of the idea.

    Code:
    public interface Expression {
    
    	public String getField();
    
    	public boolean evaluate(Object actual);
    
    }
    Code:
    public class ValidationCriteria {
    
    	private final Class<?>					commandClass;
    	private final Map<String, Map<Expression, String>>	criteriaData;
    	private final String					defaultErrorCode;
    
    	public ValidationCriteria(Class<?> commandClass) {
    		this(commandClass, "invalid");
    	}
    
    	public ValidationCriteria(Class<?> commandClass, String defaultErrorCode) {
    		this.defaultErrorCode = defaultErrorCode;
    		Assert.assertNotNull(commandClass);
    		this.commandClass = commandClass;
    		criteriaData = new HashMap<String, Map<Expression, String>>();
    	}
    
    	public Class<?> getCommandClass() {
    		return commandClass;
    	}
    
    	public Map<String, Map<Expression, String>> getCriteriaData() {
    		return criteriaData;
    	}
    
    	public ValidationCriteria add(Expression expression) {
    		return add(expression, defaultErrorCode);
    	}
    
    	public ValidationCriteria add(Expression expression, String errorCode) {
    		if (!criteriaData.containsKey(expression.getField())) {
    			/**
    			 * ATTENTION: the order of the used implementation of Map is important;
    			 * please use LinkedHashMap to implement a FIFO-like ordering principle;
    			 * @see constructor comment of ValidationUtil()
    			 */
    			criteriaData.put(expression.getField(), new LinkedHashMap<Expression, String>());
    		}
    		criteriaData.get(expression.getField()).put(expression, errorCode);
    		return this;
    	}
    
    	public static Expression equals(String field, Object expected) {
    		return new Equals(field, expected);
    	}
    
    	public static Expression notEquals(String field, Object expected) {
    		return new NotEquals(field, expected);
    	}
    
    	public static Expression notEmpty(String field) {
    		return new NotEmpty(field);
    	}
    
    	public static Expression notBlank(String field) {
    		return new NotBlank(field);
    	}
    
    	public static Expression notNull(String field) {
    		return new NotNull(field);
    	}
    
    	@SuppressWarnings("unchecked")
    	public static Expression greaterThan(String field, Comparable expected) {
    		return new GreaterThan(field, expected);
    	}
    
    	@SuppressWarnings("unchecked")
    	public static Expression greaterThanOrEqual(String field, Comparable expected) {
    		return new GreaterThanOrEqual(field, expected);
    	}
    
    	@SuppressWarnings("unchecked")
    	public static Expression lessThan(String field, Comparable expected) {
    		return new LessThan(field, expected);
    	}
    
    	@SuppressWarnings("unchecked")
    	public static Expression lessThanOrEqual(String field, Comparable expected) {
    		return new LessThanOrEqual(field, expected);
    	}
    
    	public static Expression patternMatch(String field, String pattern) {
    		return new PatternMatch(field, pattern);
    	}
    
    	public static Expression maxLength(String field, int maxLength) {
    		return new MaxLength(field, maxLength);
    	}
    
    	public static Expression minLength(String field, int minLength) {
    		return new MinLength(field, minLength);
    	}
    
    }
    Code:
    public class Equals implements Expression {
    
    	private final String	field;
    	private final Object	expected;
    
    	public Equals(String field, Object expected) {
    		this.field = field;
    		this.expected = expected;
    	}
    
    	public String getField() {
    		return field;
    	}
    
    	public boolean evaluate(Object actual) {
    		if (expected == null && actual == null) {
    			return true;
    		}
    		if (expected == null || actual == null) {
    			return false;
    		}
    		return expected.equals(actual);
    	}
    
    }
    Code:
    public class MaxLength implements Expression {
    
    	private final String	field;
    	private final int	expectedLength;
    
    	public MaxLength(String field, int expectedLength) {
    		this.field = field;
    		this.expectedLength = expectedLength;
    	}
    
    	public String getField() {
    		return field;
    	}
    
    	public boolean evaluate(Object actual) {
    		if (actual instanceof String) {
    			return ((String) actual).length() <= expectedLength;
    		}
    		if (actual instanceof Collection<?>) {
    			return ((Collection<?>) actual).size() <= expectedLength;
    		}
    		return false;
    	}
    
    }
    Code:
    public class NotBlank implements Expression {
    
    	private final String	field;
    
    	public NotBlank(String field) {
    		this.field = field;
    	}
    
    	public String getField() {
    		return field;
    	}
    
    	public boolean evaluate(Object actual) {
    		if (actual instanceof String) {
    			return !((String) actual).trim().isEmpty();
    		}
    		return false;
    	}
    
    }
    Code:
    public class PatternMatch implements Expression {
    
    	private final String	field;
    	private final Pattern	expectedPattern;
    
    	public PatternMatch(String field, String expectedPattern) {
    		this.field = field;
    		this.expectedPattern = Pattern.compile(expectedPattern);
    	}
    
    	public String getField() {
    		return field;
    	}
    
    	public boolean evaluate(Object actual) {
    		if (actual instanceof CharSequence) {
    			return expectedPattern.matcher((CharSequence) actual).matches();
    		}
    		return false;
    	}
    
    }
    Code:
    public class ValidationUtil {
    
    	private int	maxErrorsPerField;
    
    	public ValidationUtil() {
    		maxErrorsPerField = -1;
    	}
    
    	/**
    	 * @param maxErrorsPerField The maximum error count per attribuut; using the FIFO principle:
    	 * when the maximum is reached, new errors will be ignored.
    	 * Use -1 for unlimited errors per field or 0 for no errors at all
    	 */
    	public void setMaxErrorsPerField(int maxErrorsPerField) {
    		this.maxErrorsPerField = maxErrorsPerField;
    	}
    
    	public void validate(Object command, ValidationCriteria criteria, Errors errors) {
    		if (!command.getClass().isAssignableFrom(criteria.getCommandClass())) {
    			throw new IllegalArgumentException("This ValidationCriteria instance supports only type: " + criteria.getCommandClass() + "; given class: "
    					+ command.getClass().getName());
    		}
    		Map<String, Map<Expression, String>> data = criteria.getCriteriaData();
    		for (String field : data.keySet()) {
    			for (Expression expression : data.get(field).keySet()) {
    				if (maxErrorsPerField < 0 || errors.getFieldErrorCount(field) < maxErrorsPerField) {
    					Object value = getValue(field, command);
    					if (!expression.evaluate(value)) {
    						errors.rejectValue(field, data.get(field).get(expression));
    					}
    				}
    			}
    		}
    	}
    
    	private Object getValue(String field, Object command) {
    		try {
    			return command.getClass().getMethod(getGetterName(command, field), new Class<?>[] {}).invoke(command);
    		} catch (NoSuchMethodException e) {
    			throw new RuntimeException("No getter found for field: " + field);
    		} catch (NoSuchFieldException e) {
    			throw new RuntimeException("No getter found for field: " + field);
    		} catch (Exception e) {
    			throw new RuntimeException("Unable to obtain the value for field: " + field);
    		}
    	}
    
    	private String getGetterName(Object command, String field) throws NoSuchFieldException {
    		StringBuilder builder = new StringBuilder();
    		String prefix = command.getClass().getDeclaredField(field).getType() == Boolean.TYPE ? "is" : "get";
    		builder.append(prefix);
    		builder.append(field.substring(0, 1).toUpperCase());
    		builder.append(field.substring(1));
    		return builder.toString();
    	}
    
    }
    A Spring Validator at work, using my ValidationUtil:

    Code:
    public class RegisterStep1Validator implements Validator {
    
    	private final String			EMAIL_PATTERN	= "(\\w+)@(\\w+\\.)(\\w+)(\\.\\w+)*";
    
    	private final OfferProfileRepository	offerProfileRepository;
    	private final ValidationUtil		validationUtil;
    
    	private int				minPasswordLength;
    
    	public RegisterStep1Validator(OfferProfileRepository offerProfileRepository, ValidationUtil validationUtil) {
    		this.offerProfileRepository = offerProfileRepository;
    		this.validationUtil = validationUtil;
    	}
    
    	@SuppressWarnings("unchecked")
    	public boolean supports(Class clazz) {
    		return clazz.isAssignableFrom(RegisterStep1Command.class);
    	}
    
    	public void validate(Object c, Errors errors) {
    		RegisterStep1Command command = (RegisterStep1Command) c;
    
    		ValidationCriteria criteria = new ValidationCriteria(RegisterStep1Command.class);
    		criteria.add(ValidationCriteria.notBlank("firstName"), "empty");
    		criteria.add(ValidationCriteria.notBlank("lastName"), "empty");
    		criteria.add(ValidationCriteria.notNull("gender"), "empty");
    		criteria.add(ValidationCriteria.notBlank("email"), "empty");
    		criteria.add(ValidationCriteria.notBlank("password2"), "empty");
    
    		criteria.add(ValidationCriteria.minLength("password1", minPasswordLength), "tooShort");
    		criteria.add(ValidationCriteria.notEquals("password1", command.getEmail()), "sameAsUsername");
    		criteria.add(ValidationCriteria.equals("password2", command.getPassword1()), "notEqual");
    
    		criteria.add(ValidationCriteria.patternMatch("email", EMAIL_PATTERN));
    		criteria.add(new Expression() {
    
    			public boolean evaluate(Object actual) {
    				String email = (String) actual;
    				return offerProfileRepository.getByLogin(email) == null;
    			}
    
    			public String getField() {
    				return "email";
    			}
    
    		}, "alreadyInUse");
    
    		validationUtil.validate(command, criteria, errors);
    	}
    
    	public void setMinPasswordLength(int minPasswordLength) {
    		this.minPasswordLength = minPasswordLength;
    	}
    
    }

  • #2
    Here a test validator while developing and testing some new functionality:

    Code:
    public class TestValidator implements Validator {
    
    	@SuppressWarnings("unchecked")
    	public boolean supports(Class arg0) {
    		return true;
    	}
    
    	public void validate(Object c, Errors errors) {
    		TestCommand command = (TestCommand) c;
    		command.setAge(11);
    		command.setName("piet");
    		command.setMoney(9.5);
    		command.setPostalCode("7415XM");
    		command.setNewsLetter(true);
    		command.setEmail("test1");
    		ValidationUtil validationUtil = new ValidationUtil();
    		validationUtil.setMaxErrorsPerField(1);
    		ValidationCriteria criteria = new ValidationCriteria(TestCommand.class);
    		criteria.add(ValidationCriteria.equals("name", "piet"));
    		criteria.add(ValidationCriteria.greaterThan("age", 10));
    		criteria.add(ValidationCriteria.greaterThan("money", 8.5));
    		criteria.add(ValidationCriteria.patternMatch("postalCode", "^[0-9]{4}[a-z|A-Z]{2}$"));
    		criteria.add(ValidationCriteria.maxLength("postalCode", 6));
    		criteria.add(ValidationCriteria.minLength("postalCode", 1));
    		criteria.add(ValidationCriteria.equals("newsLetter", Boolean.TRUE));
    
    		criteria.add(new Expression() {
    
    			public boolean evaluate(Object actual) {
    				String email = (String) actual;
    				//TODO insert data in a database
    				return email.equals("test");
    			}
    
    			public String getField() {
    				return "email";
    			}
    
    		}, "email_exists_already");
    
    		validationUtil.validate(command, criteria, errors);
    
    		System.out.println(errors);
    	}
    }
    Adding the ValidationUtil to the Spring context, so it can be used for autowiring:

    Code:
    <bean id="validationUtil" class="nl.chrisgunnink.commons.validation.ValidationUtil" autowire="constructor">
    		<property name="maxErrorsPerField" value="1" />
    	</bean>
    Adding the ValidationUtil to the Spring context, so it can be used for autowiring:

    Code:
    <bean id="validationUtil" class="nl.chrisgunnink.commons.validation.ValidationUtil" autowire="constructor">
    	<property name="maxErrorsPerField" value="1" />
    </bean>
    I hope you're not overwelmed with code . Please tell me what you think!

    Comment

    Working...
    X