001    package org.springframework.binding.validation.support;
002    
003    import java.beans.IntrospectionException;
004    import java.beans.Introspector;
005    import java.beans.PropertyDescriptor;
006    import java.util.ArrayList;
007    import java.util.HashSet;
008    import java.util.List;
009    import java.util.Set;
010    
011    import org.hibernate.validator.AssertFalse;
012    import org.hibernate.validator.AssertTrue;
013    import org.hibernate.validator.ClassValidator;
014    import org.hibernate.validator.InvalidValue;
015    import org.springframework.binding.form.ValidatingFormModel;
016    import org.springframework.binding.validation.RichValidator;
017    import org.springframework.binding.validation.ValidationMessage;
018    import org.springframework.binding.validation.ValidationResults;
019    import org.springframework.binding.validation.Validator;
020    import org.springframework.richclient.core.Severity;
021    import org.springframework.richclient.util.Assert;
022    import org.springframework.rules.reporting.ObjectNameResolver;
023    
024    /**
025     * <p>
026     * Validator which uses the {@link ClassValidator} of Hibernate to discover
027     * {@link InvalidValue}. These are then translated to {@link ValidationMessage}s
028     * and added to the {@link ValidationResults} as usual.
029     * </p>
030     *
031     * <p>
032     * Usage in a {@link ValidatingFormModel} where <code>SomeClass</code> has
033     * annotations for Hibernate Validator:
034     * </p>
035     *
036     * <pre>
037     * formModel.setValidator(new HibernateRulesValidator(formModel, SomeClass.class));
038     * </pre>
039     *
040     * <p>
041     * This can be used in combination with other {@link Validator}s as well by
042     * creating a {@link CompositeRichValidator}:
043     * </p>
044     *
045     * <pre>
046     * HibernateRulesValidator hibernateRulesValidator = new HibernateRulesValidator(getFormModel(), SomeClass.class);
047     * hibernateRulesValidator.addIgnoredHibernateProperty(&quot;ignoredProperty&quot;);
048     * RulesValidator rulesValidator = new RulesValidator(getFormModel(), myRulesSource);
049     * getFormModel().setValidator(new CompositeRichValidator(rulesValidator, hibernateRulesValidator));
050     * </pre>
051     *
052     * <p>
053     * Note that we're adding one property to the {@link HibernateRulesValidator}
054     * that will be ignored. This property will only be checked at your back-end by
055     * Hibernate. The {@link RulesValidator} adds additional rules that are only
056     * used at the front-end or/and may contain the equivalent of the
057     * {@link AssertTrue} or {@link AssertFalse} methods on <code>SomeClass</code>.
058     * </p>
059     *
060     * @author Andy DuPue
061     * @author Lieven Doclo
062     * @author Jan Hoskens
063     */
064    @SuppressWarnings("unchecked")
065    public class HibernateRulesValidator implements RichValidator, ObjectNameResolver {
066    
067            private final ClassValidator hibernateValidator;
068    
069            private ValidatingFormModel formModel;
070    
071            private DefaultValidationResults results = new DefaultValidationResults();
072    
073            private Set<String> ignoredHibernateProperties;
074    
075            /**
076             * Creates a new HibernateRulesValidator without ignoring any properties.
077             *
078             * @param formModel The {@link ValidatingFormModel} on which validation
079             * needs to occur
080             * @param clazz The class of the object this validator needs to check
081             */
082            public HibernateRulesValidator(ValidatingFormModel formModel, Class clazz) {
083                    this(formModel, clazz, new HashSet<String>());
084            }
085    
086            /**
087             * Creates a new HibernateRulesValidator with additionally a set of
088             * properties that should not be validated.
089             *
090             * @param formModel The {@link ValidatingFormModel} on which validation
091             * needs to occur
092             * @param clazz The class of the object this validator needs to check
093             * @param ignoredHibernateProperties properties that should not be checked
094             * though are
095             */
096            public HibernateRulesValidator(ValidatingFormModel formModel, Class clazz, Set<String> ignoredHibernateProperties) {
097                    this.formModel = formModel;
098                    this.hibernateValidator = new ClassValidator(clazz, new HibernateRulesMessageInterpolator());
099                    this.ignoredHibernateProperties = ignoredHibernateProperties;
100            }
101    
102            /**
103             * {@inheritDoc}
104             */
105            public ValidationResults validate(Object object) {
106                    return validate(object, null);
107            }
108    
109            /**
110             * {@inheritDoc}
111             */
112            public ValidationResults validate(Object object, String propertyName) {
113                    // hibernate will return InvalidValues per propertyName, remove any
114                    // previous validationMessages.
115                    if (propertyName == null) {
116                            results.clearMessages();
117                    }
118                    else {
119                            results.clearMessages(propertyName);
120                    }
121    
122                    addInvalidValues(doHibernateValidate(object, propertyName));
123                    return results;
124            }
125    
126            /**
127             * Add all {@link InvalidValue}s to the {@link ValidationResults}.
128             */
129            protected void addInvalidValues(InvalidValue[] invalidValues) {
130                    if (invalidValues != null) {
131                            for (InvalidValue invalidValue : invalidValues) {
132                                    results.addMessage(translateMessage(invalidValue));
133                            }
134                    }
135            }
136    
137            /**
138             * Translate a single {@link InvalidValue} to a {@link ValidationMessage}.
139             */
140            protected ValidationMessage translateMessage(InvalidValue invalidValue) {
141                    return new DefaultValidationMessage(invalidValue.getPropertyName(), Severity.ERROR,
142                                    resolveObjectName(invalidValue.getPropertyName()) + " " + invalidValue.getMessage());
143            }
144    
145            /**
146             * Validates the object through Hibernate Validator
147             *
148             * @param object The object that needs to be validated
149             * @param property The properties that needs to be validated
150             * @return An array of {@link InvalidValue}, containing all validation
151             * errors
152             */
153            protected InvalidValue[] doHibernateValidate(final Object object, final String property) {
154                    if (property == null) {
155                            final List<InvalidValue> ret = new ArrayList<InvalidValue>();
156                            PropertyDescriptor[] propertyDescriptors;
157                            try {
158                                    propertyDescriptors = Introspector.getBeanInfo(object.getClass()).getPropertyDescriptors();
159                            }
160                            catch (IntrospectionException e) {
161                                    throw new IllegalStateException("Could not retrieve property information");
162                            }
163                            for (final PropertyDescriptor prop : propertyDescriptors) {
164                                    String propertyName = prop.getName();
165                                    if (formModel.hasValueModel(propertyName) && !ignoredHibernateProperties.contains(propertyName)) {
166                                            final InvalidValue[] result = hibernateValidator.getPotentialInvalidValues(propertyName, formModel
167                                                            .getValueModel(propertyName).getValue());
168                                            if (result != null) {
169                                                    for (final InvalidValue r : result) {
170                                                            ret.add(r);
171                                                    }
172                                            }
173                                    }
174                            }
175                            return ret.toArray(new InvalidValue[ret.size()]);
176                    }
177                    else if (!ignoredHibernateProperties.contains(property) && formModel.hasValueModel(property)) {
178                            return hibernateValidator.getPotentialInvalidValues(property, formModel.getValueModel(property).getValue());
179                    }
180                    else {
181                            return null;
182                    }
183            }
184    
185            /**
186             * Clear the current validationMessages and the errors.
187             *
188             * @see #validate(Object, String)
189             */
190            public void clearMessages() {
191                    this.results.clearMessages();
192            }
193    
194            /**
195             * Add a property for the Hibernate validator to ignore.
196             *
197             * @param propertyName Name of the property to ignore. Cannot be null.
198             */
199            public void addIgnoredHibernateProperty(String propertyName) {
200                    Assert.notNull(propertyName);
201                    ignoredHibernateProperties.add(propertyName);
202            }
203    
204            /**
205             * Remove a property for the Hibernate validator to ignore.
206             *
207             * @param propertyName Name of the property to be removed. Cannot be null.
208             */
209            public void removeIgnoredHibernateProperty(String propertyName) {
210                    Assert.notNull(propertyName);
211                    ignoredHibernateProperties.remove(propertyName);
212            }
213    
214            /**
215             * {@inheritDoc}
216             */
217            public String resolveObjectName(String objectName) {
218                    return formModel.getFieldFace(objectName).getDisplayName();
219            }
220    }