001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.wicket.bean.validation;
018
019import java.util.Arrays;
020import java.util.HashSet;
021import java.util.Iterator;
022import java.util.Set;
023
024import javax.validation.ConstraintViolation;
025import javax.validation.Validator;
026import javax.validation.groups.Default;
027import javax.validation.metadata.ConstraintDescriptor;
028
029import org.apache.wicket.Component;
030import org.apache.wicket.behavior.Behavior;
031import org.apache.wicket.markup.ComponentTag;
032import org.apache.wicket.markup.html.form.FormComponent;
033import org.apache.wicket.model.IModel;
034import org.apache.wicket.model.PropertyModel;
035import org.apache.wicket.validation.INullAcceptingValidator;
036import org.apache.wicket.validation.IValidatable;
037
038/**
039 * Validator that delegates to the bean validation framework. The integration has to be first
040 * configured using {@link BeanValidationConfiguration}.
041 * 
042 * <p>
043 * The validator must be provided a {@link Property}, unless one can be resolved from the component
044 * implicitly. By default the configuration contains the {@link DefaultPropertyResolver} so
045 * {@link PropertyModel}s are supported out of the box - when attached to a component with a
046 * property model the property does not need to be specified explicitly.
047 * </p>
048 * 
049 * <p>
050 * The validator will set the required flag on the form component it is attached to based on the
051 * presence of the @NotNull annotation, see {@link BeanValidationContext#isRequiredConstraint(ConstraintDescriptor)}
052 * for details. Notice, the required flag will only be set to {@code true},
053 * components with the required flag already set to {@code true} will not have the flag set to
054 * {@code false} by this validator.
055 * </p>
056 * 
057 * <p>
058 * The validator will allow {@link ITagModifier}s registered on {@link BeanValidationContext}
059 * to mutate the markup tag of the component it is attached to, e.g. add a <code>maxlength</code> attribute.
060 * </p>
061 * 
062 * <p>
063 * The validator specifies default error messages in the {@code PropertyValidator.properties} file.
064 * These values can be overridden in the application subclass' property files globally or in the
065 * page or panel properties locally. See this file for the default messages supported.
066 * </p>
067 * 
068 * @author igor
069 * 
070 * @param <T>
071 */
072public class PropertyValidator<T> extends Behavior implements INullAcceptingValidator<T>
073{
074        private static final Class<?>[] EMPTY = new Class<?>[0];
075
076        private FormComponent<T> component;
077
078        // the trailing underscore means that these members should not be used
079        // directly. ALWAYS use the respective getter instead.
080        private Property property_;
081
082        private final IModel<Class<?>[]> groups_;
083
084        /**
085         * A flag indicating whether the component has been configured at least once.
086         */
087        private boolean requiredFlagSet;
088
089        public PropertyValidator(Class<?>... groups)
090        {
091                this(null, groups);
092        }
093
094        public PropertyValidator(IModel<Class<?>[]> groups)
095        {
096                this(null, groups);
097        }
098
099        public PropertyValidator(Property property, Class<?>... groups)
100        {
101                this(property, new GroupsModel(groups));
102        }
103
104        public PropertyValidator(Property property, IModel<Class<?>[]> groups)
105        {
106                this.property_ = property;
107                this.groups_ = groups;
108        }
109
110        /**
111         * To support debugging, trying to provide useful information where possible
112         * 
113         * @return
114         */
115        private String createUnresolvablePropertyMessage(FormComponent<T> component)
116        {
117                String baseMessage = "Could not resolve Bean Property from component: " + component
118                        + ". (Hints:) Possible causes are a typo in the PropertyExpression, a null reference or a model that does not work in combination with a "
119                        + IPropertyResolver.class.getSimpleName() + ".";
120                IModel<?> model = ValidationModelResolver.resolvePropertyModelFrom(component);
121                if (model != null)
122                {
123                        baseMessage += " Model : " + model;
124                }
125                return baseMessage;
126        }
127
128        private Property getProperty()
129        {
130                if (property_ == null)
131                {
132                        BeanValidationContext config = BeanValidationConfiguration.get();
133                        property_ = config.resolveProperty(component);
134                        if (property_ == null)
135                        {
136                                throw new IllegalStateException(createUnresolvablePropertyMessage(component));
137                        }
138                }
139                return property_;
140        }
141
142        private Class<?>[] getGroups()
143        {
144                if (groups_ == null)
145                {
146                        return EMPTY;
147                }
148                return groups_.getObject();
149        }
150
151        @SuppressWarnings("unchecked")
152        @Override
153        public void bind(Component component)
154        {
155                if (this.component != null)
156                {
157                        throw new IllegalStateException( //
158                                "This validator has already been added to component: " + this.component
159                                        + ". This validator does not support reusing instances, please create a new one");
160                }
161
162                if (!(component instanceof FormComponent))
163                {
164                        throw new IllegalStateException(
165                                getClass().getSimpleName() + " can only be added to FormComponents");
166                }
167
168                // TODO add a validation key that appends the type so we can have
169                // different messages for
170                // @Size on String vs Collection - done but need to add a key for each
171                // superclass/interface
172
173                this.component = (FormComponent<T>)component;
174        }
175
176        @Override
177        public void onConfigure(Component component)
178        {
179                super.onConfigure(component);
180                if (requiredFlagSet == false)
181                {
182                        // "Required" flag is calculated upon component's model property, so
183                        // we must ensure,
184                        // that model object is accessible (i.e. component is already added
185                        // in a page).
186                        requiredFlagSet = true;
187
188                        if (isRequired())
189                        {
190                                this.component.setRequired(true);
191                        }
192                }
193        }
194
195        @Override
196        public void detach(Component component)
197        {
198                super.detach(component);
199                if (groups_ != null)
200                {
201                        groups_.detach();
202                }
203        }
204
205        /**
206         * Should this property make the owning component required.
207         * 
208         * @return <code>true</code> if required
209         * 
210         * @see BeanValidationContext#isRequiredConstraint(ConstraintDescriptor)
211         */
212        protected boolean isRequired()
213        {
214                BeanValidationContext config = BeanValidationConfiguration.get();
215
216                HashSet<Class<?>> groups = new HashSet<Class<?>>(Arrays.asList(getGroups()));
217
218                Iterator<ConstraintDescriptor<?>> it = new ConstraintIterator(config.getValidator(), getProperty());
219                while (it.hasNext())
220                {
221                        ConstraintDescriptor<?> constraint = it.next();
222                        
223                        if (config.isRequiredConstraint(constraint))
224                        {
225                                if (canApplyToDefaultGroup(constraint) && groups.size() == 0)
226                                {
227                                        return true;
228                                }
229                
230                                for (Class<?> constraintGroup : constraint.getGroups())
231                                {
232                                        if (groups.contains(constraintGroup))
233                                        {
234                                                return true;
235                                        }
236                                }
237                        }
238                }
239
240                return false;
241        }
242
243        private boolean canApplyToDefaultGroup(ConstraintDescriptor<?> constraint)
244        {
245                Set<Class<?>> groups = constraint.getGroups();
246                //the constraint can be applied to default group either if its group array is empty
247                //or if it contains javax.validation.groups.Default
248                return groups.size() == 0 || groups.contains(Default.class);
249        }
250
251        @Override
252        @SuppressWarnings({ "rawtypes", "unchecked" })
253        public void onComponentTag(Component component, ComponentTag tag)
254        {
255                super.onComponentTag(component, tag);
256
257                BeanValidationContext config = BeanValidationConfiguration.get();
258                Validator validator = config.getValidator();
259                Property property = getProperty();
260
261                // find any tag modifiers that apply to the constraints of the property
262                // being validated
263                // and allow them to modify the component tag
264
265                Iterator<ConstraintDescriptor<?>> it = new ConstraintIterator(validator, property,
266                        getGroups());
267
268                while (it.hasNext())
269                {
270                        ConstraintDescriptor<?> desc = it.next();
271
272                        ITagModifier modifier = config.getTagModifier(desc.getAnnotation().annotationType());
273
274                        if (modifier != null)
275                        {
276                                modifier.modify((FormComponent<?>)component, tag, desc.getAnnotation());
277                        }
278                }
279        }
280
281        @SuppressWarnings("unchecked")
282        @Override
283        public void validate(IValidatable<T> validatable)
284        {
285                BeanValidationContext config = BeanValidationConfiguration.get();
286                Validator validator = config.getValidator();
287
288                Property property = getProperty();
289
290                // validate the value using the bean validator
291
292                Set<?> violations = validator.validateValue(property.getOwner(), property.getName(),
293                        validatable.getValue(), getGroups());
294
295                // iterate over violations and report them
296
297                for (ConstraintViolation<?> violation : (Set<ConstraintViolation<?>>)violations)
298                {
299                        validatable.error(config.getViolationTranslator().convert(violation));
300                }
301        }
302
303}