001/* 002 * Copyright 2002-2018 the original author or authors. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * https://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016 017package org.springframework.validation.beanvalidation; 018 019import java.io.Serializable; 020import java.util.ArrayList; 021import java.util.HashSet; 022import java.util.LinkedHashSet; 023import java.util.List; 024import java.util.Map; 025import java.util.Set; 026import java.util.TreeMap; 027import javax.validation.ConstraintViolation; 028import javax.validation.ValidationException; 029import javax.validation.Validator; 030import javax.validation.metadata.BeanDescriptor; 031import javax.validation.metadata.ConstraintDescriptor; 032 033import org.springframework.beans.NotReadablePropertyException; 034import org.springframework.context.MessageSourceResolvable; 035import org.springframework.context.support.DefaultMessageSourceResolvable; 036import org.springframework.util.Assert; 037import org.springframework.util.ClassUtils; 038import org.springframework.validation.BindingResult; 039import org.springframework.validation.Errors; 040import org.springframework.validation.FieldError; 041import org.springframework.validation.ObjectError; 042import org.springframework.validation.SmartValidator; 043 044/** 045 * Adapter that takes a JSR-303 {@code javax.validator.Validator} and 046 * exposes it as a Spring {@link org.springframework.validation.Validator} 047 * while also exposing the original JSR-303 Validator interface itself. 048 * 049 * <p>Can be used as a programmatic wrapper. Also serves as base class for 050 * {@link CustomValidatorBean} and {@link LocalValidatorFactoryBean}. 051 * 052 * <p>Note that Bean Validation 1.1's {@code #forExecutables} method isn't supported 053 * on this adapter: We do not expect that method to be called by application code; 054 * consider {@link MethodValidationInterceptor} instead. If you really need programmatic 055 * {@code #forExecutables} access, call {@code #unwrap(Validator.class)} which will 056 * provide the native {@link Validator} object with {@code #forExecutables} support. 057 * 058 * @author Juergen Hoeller 059 * @since 3.0 060 */ 061public class SpringValidatorAdapter implements SmartValidator, javax.validation.Validator { 062 063 private static final Set<String> internalAnnotationAttributes = new HashSet<String>(3); 064 065 static { 066 internalAnnotationAttributes.add("message"); 067 internalAnnotationAttributes.add("groups"); 068 internalAnnotationAttributes.add("payload"); 069 } 070 071 private javax.validation.Validator targetValidator; 072 073 074 /** 075 * Create a new SpringValidatorAdapter for the given JSR-303 Validator. 076 * @param targetValidator the JSR-303 Validator to wrap 077 */ 078 public SpringValidatorAdapter(javax.validation.Validator targetValidator) { 079 Assert.notNull(targetValidator, "Target Validator must not be null"); 080 this.targetValidator = targetValidator; 081 } 082 083 SpringValidatorAdapter() { 084 } 085 086 void setTargetValidator(javax.validation.Validator targetValidator) { 087 this.targetValidator = targetValidator; 088 } 089 090 091 //--------------------------------------------------------------------- 092 // Implementation of Spring Validator interface 093 //--------------------------------------------------------------------- 094 095 @Override 096 public boolean supports(Class<?> clazz) { 097 return (this.targetValidator != null); 098 } 099 100 @Override 101 public void validate(Object target, Errors errors) { 102 if (this.targetValidator != null) { 103 processConstraintViolations(this.targetValidator.validate(target), errors); 104 } 105 } 106 107 @Override 108 public void validate(Object target, Errors errors, Object... validationHints) { 109 if (this.targetValidator != null) { 110 Set<Class<?>> groups = new LinkedHashSet<Class<?>>(); 111 if (validationHints != null) { 112 for (Object hint : validationHints) { 113 if (hint instanceof Class) { 114 groups.add((Class<?>) hint); 115 } 116 } 117 } 118 processConstraintViolations( 119 this.targetValidator.validate(target, ClassUtils.toClassArray(groups)), errors); 120 } 121 } 122 123 /** 124 * Process the given JSR-303 ConstraintViolations, adding corresponding errors to 125 * the provided Spring {@link Errors} object. 126 * @param violations the JSR-303 ConstraintViolation results 127 * @param errors the Spring errors object to register to 128 */ 129 protected void processConstraintViolations(Set<ConstraintViolation<Object>> violations, Errors errors) { 130 for (ConstraintViolation<Object> violation : violations) { 131 String field = determineField(violation); 132 FieldError fieldError = errors.getFieldError(field); 133 if (fieldError == null || !fieldError.isBindingFailure()) { 134 try { 135 ConstraintDescriptor<?> cd = violation.getConstraintDescriptor(); 136 String errorCode = determineErrorCode(cd); 137 Object[] errorArgs = getArgumentsForConstraint(errors.getObjectName(), field, cd); 138 if (errors instanceof BindingResult) { 139 // Can do custom FieldError registration with invalid value from ConstraintViolation, 140 // as necessary for Hibernate Validator compatibility (non-indexed set path in field) 141 BindingResult bindingResult = (BindingResult) errors; 142 String nestedField = bindingResult.getNestedPath() + field; 143 if ("".equals(nestedField)) { 144 String[] errorCodes = bindingResult.resolveMessageCodes(errorCode); 145 bindingResult.addError(new ObjectError( 146 errors.getObjectName(), errorCodes, errorArgs, violation.getMessage())); 147 } 148 else { 149 Object rejectedValue = getRejectedValue(field, violation, bindingResult); 150 String[] errorCodes = bindingResult.resolveMessageCodes(errorCode, field); 151 bindingResult.addError(new FieldError( 152 errors.getObjectName(), nestedField, rejectedValue, false, 153 errorCodes, errorArgs, violation.getMessage())); 154 } 155 } 156 else { 157 // got no BindingResult - can only do standard rejectValue call 158 // with automatic extraction of the current field value 159 errors.rejectValue(field, errorCode, errorArgs, violation.getMessage()); 160 } 161 } 162 catch (NotReadablePropertyException ex) { 163 throw new IllegalStateException("JSR-303 validated property '" + field + 164 "' does not have a corresponding accessor for Spring data binding - " + 165 "check your DataBinder's configuration (bean property versus direct field access)", ex); 166 } 167 } 168 } 169 } 170 171 /** 172 * Determine a field for the given constraint violation. 173 * <p>The default implementation returns the stringified property path. 174 * @param violation the current JSR-303 ConstraintViolation 175 * @return the Spring-reported field (for use with {@link Errors}) 176 * @since 4.2 177 * @see javax.validation.ConstraintViolation#getPropertyPath() 178 * @see org.springframework.validation.FieldError#getField() 179 */ 180 protected String determineField(ConstraintViolation<Object> violation) { 181 String path = violation.getPropertyPath().toString(); 182 int elementIndex = path.indexOf(".<"); 183 return (elementIndex >= 0 ? path.substring(0, elementIndex) : path); 184 } 185 186 /** 187 * Determine a Spring-reported error code for the given constraint descriptor. 188 * <p>The default implementation returns the simple class name of the descriptor's 189 * annotation type. Note that the configured 190 * {@link org.springframework.validation.MessageCodesResolver} will automatically 191 * generate error code variations which include the object name and the field name. 192 * @param descriptor the JSR-303 ConstraintDescriptor for the current violation 193 * @return a corresponding error code (for use with {@link Errors}) 194 * @since 4.2 195 * @see javax.validation.metadata.ConstraintDescriptor#getAnnotation() 196 * @see org.springframework.validation.MessageCodesResolver 197 */ 198 protected String determineErrorCode(ConstraintDescriptor<?> descriptor) { 199 return descriptor.getAnnotation().annotationType().getSimpleName(); 200 } 201 202 /** 203 * Return FieldError arguments for a validation error on the given field. 204 * Invoked for each violated constraint. 205 * <p>The default implementation returns a first argument indicating the field name 206 * (see {@link #getResolvableField}). Afterwards, it adds all actual constraint 207 * annotation attributes (i.e. excluding "message", "groups" and "payload") in 208 * alphabetical order of their attribute names. 209 * <p>Can be overridden to e.g. add further attributes from the constraint descriptor. 210 * @param objectName the name of the target object 211 * @param field the field that caused the binding error 212 * @param descriptor the JSR-303 constraint descriptor 213 * @return the Object array that represents the FieldError arguments 214 * @see org.springframework.validation.FieldError#getArguments 215 * @see org.springframework.context.support.DefaultMessageSourceResolvable 216 * @see org.springframework.validation.DefaultBindingErrorProcessor#getArgumentsForBindError 217 */ 218 protected Object[] getArgumentsForConstraint(String objectName, String field, ConstraintDescriptor<?> descriptor) { 219 List<Object> arguments = new ArrayList<Object>(); 220 arguments.add(getResolvableField(objectName, field)); 221 // Using a TreeMap for alphabetical ordering of attribute names 222 Map<String, Object> attributesToExpose = new TreeMap<String, Object>(); 223 for (Map.Entry<String, Object> entry : descriptor.getAttributes().entrySet()) { 224 String attributeName = entry.getKey(); 225 Object attributeValue = entry.getValue(); 226 if (!internalAnnotationAttributes.contains(attributeName)) { 227 if (attributeValue instanceof String) { 228 attributeValue = new ResolvableAttribute(attributeValue.toString()); 229 } 230 attributesToExpose.put(attributeName, attributeValue); 231 } 232 } 233 arguments.addAll(attributesToExpose.values()); 234 return arguments.toArray(); 235 } 236 237 /** 238 * Build a resolvable wrapper for the specified field, allowing to resolve the field's 239 * name in a {@code MessageSource}. 240 * <p>The default implementation returns a first argument indicating the field: 241 * of type {@code DefaultMessageSourceResolvable}, with "objectName.field" and "field" 242 * as codes, and with the plain field name as default message. 243 * @param objectName the name of the target object 244 * @param field the field that caused the binding error 245 * @return a corresponding {@code MessageSourceResolvable} for the specified field 246 * @since 4.3 247 */ 248 protected MessageSourceResolvable getResolvableField(String objectName, String field) { 249 String[] codes = new String[] {objectName + Errors.NESTED_PATH_SEPARATOR + field, field}; 250 return new DefaultMessageSourceResolvable(codes, field); 251 } 252 253 /** 254 * Extract the rejected value behind the given constraint violation, 255 * for exposure through the Spring errors representation. 256 * @param field the field that caused the binding error 257 * @param violation the corresponding JSR-303 ConstraintViolation 258 * @param bindingResult a Spring BindingResult for the backing object 259 * which contains the current field's value 260 * @return the invalid value to expose as part of the field error 261 * @since 4.2 262 * @see javax.validation.ConstraintViolation#getInvalidValue() 263 * @see org.springframework.validation.FieldError#getRejectedValue() 264 */ 265 protected Object getRejectedValue(String field, ConstraintViolation<Object> violation, BindingResult bindingResult) { 266 Object invalidValue = violation.getInvalidValue(); 267 if (!"".equals(field) && !field.contains("[]") && 268 (invalidValue == violation.getLeafBean() || field.contains("[") || field.contains("."))) { 269 // Possibly a bean constraint with property path: retrieve the actual property value. 270 // However, explicitly avoid this for "address[]" style paths that we can't handle. 271 invalidValue = bindingResult.getRawFieldValue(field); 272 } 273 return invalidValue; 274 } 275 276 277 //--------------------------------------------------------------------- 278 // Implementation of JSR-303 Validator interface 279 //--------------------------------------------------------------------- 280 281 @Override 282 public <T> Set<ConstraintViolation<T>> validate(T object, Class<?>... groups) { 283 Assert.state(this.targetValidator != null, "No target Validator set"); 284 return this.targetValidator.validate(object, groups); 285 } 286 287 @Override 288 public <T> Set<ConstraintViolation<T>> validateProperty(T object, String propertyName, Class<?>... groups) { 289 Assert.state(this.targetValidator != null, "No target Validator set"); 290 return this.targetValidator.validateProperty(object, propertyName, groups); 291 } 292 293 @Override 294 public <T> Set<ConstraintViolation<T>> validateValue( 295 Class<T> beanType, String propertyName, Object value, Class<?>... groups) { 296 297 Assert.state(this.targetValidator != null, "No target Validator set"); 298 return this.targetValidator.validateValue(beanType, propertyName, value, groups); 299 } 300 301 @Override 302 public BeanDescriptor getConstraintsForClass(Class<?> clazz) { 303 Assert.state(this.targetValidator != null, "No target Validator set"); 304 return this.targetValidator.getConstraintsForClass(clazz); 305 } 306 307 @Override 308 @SuppressWarnings("unchecked") 309 public <T> T unwrap(Class<T> type) { 310 Assert.state(this.targetValidator != null, "No target Validator set"); 311 try { 312 return (type != null ? this.targetValidator.unwrap(type) : (T) this.targetValidator); 313 } 314 catch (ValidationException ex) { 315 // ignore if just being asked for plain Validator 316 if (javax.validation.Validator.class == type) { 317 return (T) this.targetValidator; 318 } 319 throw ex; 320 } 321 } 322 323 324 /** 325 * Wrapper for a String attribute which can be resolved via a {@code MessageSource}, 326 * falling back to the original attribute as a default value otherwise. 327 */ 328 @SuppressWarnings("serial") 329 private static class ResolvableAttribute implements MessageSourceResolvable, Serializable { 330 331 private final String resolvableString; 332 333 public ResolvableAttribute(String resolvableString) { 334 this.resolvableString = resolvableString; 335 } 336 337 @Override 338 public String[] getCodes() { 339 return new String[] {this.resolvableString}; 340 } 341 342 @Override 343 public Object[] getArguments() { 344 return null; 345 } 346 347 @Override 348 public String getDefaultMessage() { 349 return this.resolvableString; 350 } 351 } 352 353}