package org.resthub.web.validation; import org.springframework.context.annotation.Profile; import javax.inject.Named; import javax.validation.Validation; import javax.validation.Validator; import javax.validation.ValidatorFactory; import javax.validation.metadata.BeanDescriptor; import javax.validation.metadata.ConstraintDescriptor; import javax.validation.metadata.PropertyDescriptor; import java.lang.reflect.Modifier; import java.util.*; /** * JSR303, BeanValidation compliant implementation of {@link org.resthub.web.validation.ValidationService}. * * This implementation relies on BeanValidation standard but is Validation framework agnostic. * * This service is defined under the "resthub-validation" Spring profile and can be activated by adding this * profile to your context. * * @see org.resthub.web.validation.ValidationService */ @Profile("resthub-validation") @Named("validationService") public class ValidationServiceImpl implements ValidationService { private static final ValidatorFactory FACTORY = Validation.buildDefaultValidatorFactory(); private static final Validator VALIDATOR = FACTORY.getValidator(); /** * {@inheritDoc} */ @Override public ModelConstraint getConstraintsForClassName(String canonicalClassName) throws ClassNotFoundException { return this.getConstraintsForClassName(canonicalClassName, null); } /** * {@inheritDoc} */ @Override public ModelConstraint getConstraintsForClassName(String canonicalClassName, Locale locale) throws ClassNotFoundException { return this.getConstraintsForClass(Class.forName(canonicalClassName), locale); } /** * {@inheritDoc} */ @Override public ModelConstraint getConstraintsForClass(Class<?> clazz) { return this.getConstraintsForClass(clazz, null); } /** * {@inheritDoc} */ @Override public ModelConstraint getConstraintsForClass(Class<?> clazz, Locale locale) { ModelConstraint modelConstraint = new ModelConstraint(clazz.getCanonicalName()); BeanDescriptor bd = VALIDATOR.getConstraintsForClass(clazz); if (bd.isBeanConstrained() && !Modifier.isAbstract(clazz.getModifiers())) { modelConstraint.setConstraints(this.getConstraints(bd, locale)); } return modelConstraint; } /** * Build a complete map of object property / list of {@link org.resthub.web.validation.ValidationConstraint} * from a given {@link javax.validation.metadata.BeanDescriptor} <tt>bd</tt> instance and a given {@link java.util.Locale} * <tt>locale</tt> */ private Map<String, List<ValidationConstraint>> getConstraints(BeanDescriptor bd, Locale locale) { Map<String, List<ValidationConstraint>> constraints = new HashMap<String, List<ValidationConstraint>>(); for (PropertyDescriptor pd : bd.getConstrainedProperties()) { // if property has defined constraints directly or delegates validation through cascading option if ((pd.getPropertyName() != null) && (pd.hasConstraints() || pd.isCascaded())) { constraints.put(pd.getPropertyName(), this.getValidationConstraints(pd, locale)); } } return constraints; } /** * Build a list of {@link org.resthub.web.validation.ValidationConstraint} associated to a given * {@link javax.validation.metadata.PropertyDescriptor} <tt>pd</tt> instance and a given {@link java.util.Locale} * <tt>locale</tt> */ private List<ValidationConstraint> getValidationConstraints(PropertyDescriptor pd, Locale locale) { List<ValidationConstraint> validationConstraints = new ArrayList<ValidationConstraint>(); // copy any directly defined constraint into wrapper for (ConstraintDescriptor cd : pd.getConstraintDescriptors()) { ValidationConstraint validationConstraint = new ValidationConstraint(); validationConstraint.setType(this.getType(cd)); validationConstraint.setMessage(this.getMessage(cd, locale)); validationConstraint.setAttributes(this.getAttributes(cd)); validationConstraints.add(validationConstraint); } // manage cascading option by adding a custom "Valid" type and referencing underling model class name if (pd.isCascaded()) { ValidationConstraint validationConstraint = new ValidationConstraint(); validationConstraint.setType("Valid"); validationConstraint.addAttribute("model", pd.getElementClass().getCanonicalName()); validationConstraints.add(validationConstraint); } return validationConstraints; } private String getType(ConstraintDescriptor cd) { String type = cd.getAnnotation().annotationType().toString(); return type.substring(type.lastIndexOf('.') + 1, type.length()); } /** * Resolves message for a {@link javax.validation.metadata.ConstraintDescriptor} <tt>cd</tt> against the given * {@link java.util.Locale} <tt>locale</tt>. * * If <tt>locale</tt> is null, returns the default message depending on the underlying validation framework. * * This methods resolves also messages parameters through message interpolation. */ private String getMessage(ConstraintDescriptor cd, Locale locale) { String msgKey = cd.getAttributes().get("message").toString(); String msg; ValidationContext validationContext = new ValidationContext(cd, null); if (null == locale) { msg = FACTORY.getMessageInterpolator().interpolate(msgKey, validationContext); } else { msg = FACTORY.getMessageInterpolator().interpolate(msgKey, validationContext, locale); } return msg.replaceAll("[{}]", ""); } /** * Retrieves complementary constraint attributes from a given {@link javax.validation.metadata.ConstraintDescriptor} * <tt>cd</tt> */ private Map<String, Object> getAttributes(ConstraintDescriptor cd) { Map<String, Object> attributes = new HashMap<String, Object>(cd.getAttributes()); attributes.remove("payload"); attributes.remove("groups"); attributes.remove("message"); return attributes; } }