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}