001/* 002 * Copyright 2002-2020 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; 027 028import javax.validation.ConstraintViolation; 029import javax.validation.ElementKind; 030import javax.validation.Path; 031import javax.validation.ValidationException; 032import javax.validation.executable.ExecutableValidator; 033import javax.validation.metadata.BeanDescriptor; 034import javax.validation.metadata.ConstraintDescriptor; 035 036import org.springframework.beans.NotReadablePropertyException; 037import org.springframework.context.MessageSourceResolvable; 038import org.springframework.context.support.DefaultMessageSourceResolvable; 039import org.springframework.lang.Nullable; 040import org.springframework.util.Assert; 041import org.springframework.util.ClassUtils; 042import org.springframework.validation.BindingResult; 043import org.springframework.validation.Errors; 044import org.springframework.validation.FieldError; 045import org.springframework.validation.ObjectError; 046import org.springframework.validation.SmartValidator; 047 048/** 049 * Adapter that takes a JSR-303 {@code javax.validator.Validator} and 050 * exposes it as a Spring {@link org.springframework.validation.Validator} 051 * while also exposing the original JSR-303 Validator interface itself. 052 * 053 * <p>Can be used as a programmatic wrapper. Also serves as base class for 054 * {@link CustomValidatorBean} and {@link LocalValidatorFactoryBean}, 055 * and as the primary implementation of the {@link SmartValidator} interface. 056 * 057 * <p>As of Spring Framework 5.0, this adapter is fully compatible with 058 * Bean Validation 1.1 as well as 2.0. 059 * 060 * @author Juergen Hoeller 061 * @since 3.0 062 * @see SmartValidator 063 * @see CustomValidatorBean 064 * @see LocalValidatorFactoryBean 065 */ 066public class SpringValidatorAdapter implements SmartValidator, javax.validation.Validator { 067 068 private static final Set<String> internalAnnotationAttributes = new HashSet<>(4); 069 070 static { 071 internalAnnotationAttributes.add("message"); 072 internalAnnotationAttributes.add("groups"); 073 internalAnnotationAttributes.add("payload"); 074 } 075 076 @Nullable 077 private javax.validation.Validator targetValidator; 078 079 080 /** 081 * Create a new SpringValidatorAdapter for the given JSR-303 Validator. 082 * @param targetValidator the JSR-303 Validator to wrap 083 */ 084 public SpringValidatorAdapter(javax.validation.Validator targetValidator) { 085 Assert.notNull(targetValidator, "Target Validator must not be null"); 086 this.targetValidator = targetValidator; 087 } 088 089 SpringValidatorAdapter() { 090 } 091 092 void setTargetValidator(javax.validation.Validator targetValidator) { 093 this.targetValidator = targetValidator; 094 } 095 096 097 //--------------------------------------------------------------------- 098 // Implementation of Spring Validator interface 099 //--------------------------------------------------------------------- 100 101 @Override 102 public boolean supports(Class<?> clazz) { 103 return (this.targetValidator != null); 104 } 105 106 @Override 107 public void validate(Object target, Errors errors) { 108 if (this.targetValidator != null) { 109 processConstraintViolations(this.targetValidator.validate(target), errors); 110 } 111 } 112 113 @Override 114 public void validate(Object target, Errors errors, Object... validationHints) { 115 if (this.targetValidator != null) { 116 processConstraintViolations( 117 this.targetValidator.validate(target, asValidationGroups(validationHints)), errors); 118 } 119 } 120 121 @SuppressWarnings({ "unchecked", "rawtypes" }) 122 @Override 123 public void validateValue( 124 Class<?> targetType, String fieldName, @Nullable Object value, Errors errors, Object... validationHints) { 125 126 if (this.targetValidator != null) { 127 processConstraintViolations(this.targetValidator.validateValue( 128 (Class) targetType, fieldName, value, asValidationGroups(validationHints)), errors); 129 } 130 } 131 132 /** 133 * Turn the specified validation hints into JSR-303 validation groups. 134 * @since 5.1 135 */ 136 private Class<?>[] asValidationGroups(Object... validationHints) { 137 Set<Class<?>> groups = new LinkedHashSet<>(4); 138 for (Object hint : validationHints) { 139 if (hint instanceof Class) { 140 groups.add((Class<?>) hint); 141 } 142 } 143 return ClassUtils.toClassArray(groups); 144 } 145 146 /** 147 * Process the given JSR-303 ConstraintViolations, adding corresponding errors to 148 * the provided Spring {@link Errors} object. 149 * @param violations the JSR-303 ConstraintViolation results 150 * @param errors the Spring errors object to register to 151 */ 152 @SuppressWarnings("serial") 153 protected void processConstraintViolations(Set<ConstraintViolation<Object>> violations, Errors errors) { 154 for (ConstraintViolation<Object> violation : violations) { 155 String field = determineField(violation); 156 FieldError fieldError = errors.getFieldError(field); 157 if (fieldError == null || !fieldError.isBindingFailure()) { 158 try { 159 ConstraintDescriptor<?> cd = violation.getConstraintDescriptor(); 160 String errorCode = determineErrorCode(cd); 161 Object[] errorArgs = getArgumentsForConstraint(errors.getObjectName(), field, cd); 162 if (errors instanceof BindingResult) { 163 // Can do custom FieldError registration with invalid value from ConstraintViolation, 164 // as necessary for Hibernate Validator compatibility (non-indexed set path in field) 165 BindingResult bindingResult = (BindingResult) errors; 166 String nestedField = bindingResult.getNestedPath() + field; 167 if (nestedField.isEmpty()) { 168 String[] errorCodes = bindingResult.resolveMessageCodes(errorCode); 169 ObjectError error = new ViolationObjectError( 170 errors.getObjectName(), errorCodes, errorArgs, violation, this); 171 bindingResult.addError(error); 172 } 173 else { 174 Object rejectedValue = getRejectedValue(field, violation, bindingResult); 175 String[] errorCodes = bindingResult.resolveMessageCodes(errorCode, field); 176 FieldError error = new ViolationFieldError(errors.getObjectName(), nestedField, 177 rejectedValue, errorCodes, errorArgs, violation, this); 178 bindingResult.addError(error); 179 } 180 } 181 else { 182 // got no BindingResult - can only do standard rejectValue call 183 // with automatic extraction of the current field value 184 errors.rejectValue(field, errorCode, errorArgs, violation.getMessage()); 185 } 186 } 187 catch (NotReadablePropertyException ex) { 188 throw new IllegalStateException("JSR-303 validated property '" + field + 189 "' does not have a corresponding accessor for Spring data binding - " + 190 "check your DataBinder's configuration (bean property versus direct field access)", ex); 191 } 192 } 193 } 194 } 195 196 /** 197 * Determine a field for the given constraint violation. 198 * <p>The default implementation returns the stringified property path. 199 * @param violation the current JSR-303 ConstraintViolation 200 * @return the Spring-reported field (for use with {@link Errors}) 201 * @since 4.2 202 * @see javax.validation.ConstraintViolation#getPropertyPath() 203 * @see org.springframework.validation.FieldError#getField() 204 */ 205 protected String determineField(ConstraintViolation<Object> violation) { 206 Path path = violation.getPropertyPath(); 207 StringBuilder sb = new StringBuilder(); 208 boolean first = true; 209 for (Path.Node node : path) { 210 if (node.isInIterable()) { 211 sb.append('['); 212 Object index = node.getIndex(); 213 if (index == null) { 214 index = node.getKey(); 215 } 216 if (index != null) { 217 sb.append(index); 218 } 219 sb.append(']'); 220 } 221 String name = node.getName(); 222 if (name != null && node.getKind() == ElementKind.PROPERTY && !name.startsWith("<")) { 223 if (!first) { 224 sb.append('.'); 225 } 226 first = false; 227 sb.append(name); 228 } 229 } 230 return sb.toString(); 231 } 232 233 /** 234 * Determine a Spring-reported error code for the given constraint descriptor. 235 * <p>The default implementation returns the simple class name of the descriptor's 236 * annotation type. Note that the configured 237 * {@link org.springframework.validation.MessageCodesResolver} will automatically 238 * generate error code variations which include the object name and the field name. 239 * @param descriptor the JSR-303 ConstraintDescriptor for the current violation 240 * @return a corresponding error code (for use with {@link Errors}) 241 * @since 4.2 242 * @see javax.validation.metadata.ConstraintDescriptor#getAnnotation() 243 * @see org.springframework.validation.MessageCodesResolver 244 */ 245 protected String determineErrorCode(ConstraintDescriptor<?> descriptor) { 246 return descriptor.getAnnotation().annotationType().getSimpleName(); 247 } 248 249 /** 250 * Return FieldError arguments for a validation error on the given field. 251 * Invoked for each violated constraint. 252 * <p>The default implementation returns a first argument indicating the field name 253 * (see {@link #getResolvableField}). Afterwards, it adds all actual constraint 254 * annotation attributes (i.e. excluding "message", "groups" and "payload") in 255 * alphabetical order of their attribute names. 256 * <p>Can be overridden to e.g. add further attributes from the constraint descriptor. 257 * @param objectName the name of the target object 258 * @param field the field that caused the binding error 259 * @param descriptor the JSR-303 constraint descriptor 260 * @return the Object array that represents the FieldError arguments 261 * @see org.springframework.validation.FieldError#getArguments 262 * @see org.springframework.context.support.DefaultMessageSourceResolvable 263 * @see org.springframework.validation.DefaultBindingErrorProcessor#getArgumentsForBindError 264 */ 265 protected Object[] getArgumentsForConstraint(String objectName, String field, ConstraintDescriptor<?> descriptor) { 266 List<Object> arguments = new ArrayList<>(); 267 arguments.add(getResolvableField(objectName, field)); 268 // Using a TreeMap for alphabetical ordering of attribute names 269 Map<String, Object> attributesToExpose = new TreeMap<>(); 270 descriptor.getAttributes().forEach((attributeName, attributeValue) -> { 271 if (!internalAnnotationAttributes.contains(attributeName)) { 272 if (attributeValue instanceof String) { 273 attributeValue = new ResolvableAttribute(attributeValue.toString()); 274 } 275 attributesToExpose.put(attributeName, attributeValue); 276 } 277 }); 278 arguments.addAll(attributesToExpose.values()); 279 return arguments.toArray(); 280 } 281 282 /** 283 * Build a resolvable wrapper for the specified field, allowing to resolve the field's 284 * name in a {@code MessageSource}. 285 * <p>The default implementation returns a first argument indicating the field: 286 * of type {@code DefaultMessageSourceResolvable}, with "objectName.field" and "field" 287 * as codes, and with the plain field name as default message. 288 * @param objectName the name of the target object 289 * @param field the field that caused the binding error 290 * @return a corresponding {@code MessageSourceResolvable} for the specified field 291 * @since 4.3 292 * @see #getArgumentsForConstraint 293 */ 294 protected MessageSourceResolvable getResolvableField(String objectName, String field) { 295 String[] codes = new String[] {objectName + Errors.NESTED_PATH_SEPARATOR + field, field}; 296 return new DefaultMessageSourceResolvable(codes, field); 297 } 298 299 /** 300 * Extract the rejected value behind the given constraint violation, 301 * for exposure through the Spring errors representation. 302 * @param field the field that caused the binding error 303 * @param violation the corresponding JSR-303 ConstraintViolation 304 * @param bindingResult a Spring BindingResult for the backing object 305 * which contains the current field's value 306 * @return the invalid value to expose as part of the field error 307 * @since 4.2 308 * @see javax.validation.ConstraintViolation#getInvalidValue() 309 * @see org.springframework.validation.FieldError#getRejectedValue() 310 */ 311 @Nullable 312 protected Object getRejectedValue(String field, ConstraintViolation<Object> violation, BindingResult bindingResult) { 313 Object invalidValue = violation.getInvalidValue(); 314 if (!field.isEmpty() && !field.contains("[]") && 315 (invalidValue == violation.getLeafBean() || field.contains("[") || field.contains("."))) { 316 // Possibly a bean constraint with property path: retrieve the actual property value. 317 // However, explicitly avoid this for "address[]" style paths that we can't handle. 318 invalidValue = bindingResult.getRawFieldValue(field); 319 } 320 return invalidValue; 321 } 322 323 /** 324 * Indicate whether this violation's interpolated message has remaining 325 * placeholders and therefore requires {@link java.text.MessageFormat} 326 * to be applied to it. Called for a Bean Validation defined message 327 * (coming out {@code ValidationMessages.properties}) when rendered 328 * as the default message in Spring's MessageSource. 329 * <p>The default implementation considers a Spring-style "{0}" placeholder 330 * for the field name as an indication for {@link java.text.MessageFormat}. 331 * Any other placeholder or escape syntax occurrences are typically a 332 * mismatch, coming out of regex pattern values or the like. Note that 333 * standard Bean Validation does not support "{0}" style placeholders at all; 334 * this is a feature typically used in Spring MessageSource resource bundles. 335 * @param violation the Bean Validation constraint violation, including 336 * BV-defined interpolation of named attribute references in its message 337 * @return {@code true} if {@code java.text.MessageFormat} is to be applied, 338 * or {@code false} if the violation's message should be used as-is 339 * @since 5.1.8 340 * @see #getArgumentsForConstraint 341 */ 342 protected boolean requiresMessageFormat(ConstraintViolation<?> violation) { 343 return containsSpringStylePlaceholder(violation.getMessage()); 344 } 345 346 private static boolean containsSpringStylePlaceholder(@Nullable String message) { 347 return (message != null && message.contains("{0}")); 348 } 349 350 351 //--------------------------------------------------------------------- 352 // Implementation of JSR-303 Validator interface 353 //--------------------------------------------------------------------- 354 355 @Override 356 public <T> Set<ConstraintViolation<T>> validate(T object, Class<?>... groups) { 357 Assert.state(this.targetValidator != null, "No target Validator set"); 358 return this.targetValidator.validate(object, groups); 359 } 360 361 @Override 362 public <T> Set<ConstraintViolation<T>> validateProperty(T object, String propertyName, Class<?>... groups) { 363 Assert.state(this.targetValidator != null, "No target Validator set"); 364 return this.targetValidator.validateProperty(object, propertyName, groups); 365 } 366 367 @Override 368 public <T> Set<ConstraintViolation<T>> validateValue( 369 Class<T> beanType, String propertyName, Object value, Class<?>... groups) { 370 371 Assert.state(this.targetValidator != null, "No target Validator set"); 372 return this.targetValidator.validateValue(beanType, propertyName, value, groups); 373 } 374 375 @Override 376 public BeanDescriptor getConstraintsForClass(Class<?> clazz) { 377 Assert.state(this.targetValidator != null, "No target Validator set"); 378 return this.targetValidator.getConstraintsForClass(clazz); 379 } 380 381 @Override 382 @SuppressWarnings("unchecked") 383 public <T> T unwrap(@Nullable Class<T> type) { 384 Assert.state(this.targetValidator != null, "No target Validator set"); 385 try { 386 return (type != null ? this.targetValidator.unwrap(type) : (T) this.targetValidator); 387 } 388 catch (ValidationException ex) { 389 // ignore if just being asked for plain Validator 390 if (javax.validation.Validator.class == type) { 391 return (T) this.targetValidator; 392 } 393 throw ex; 394 } 395 } 396 397 @Override 398 public ExecutableValidator forExecutables() { 399 Assert.state(this.targetValidator != null, "No target Validator set"); 400 return this.targetValidator.forExecutables(); 401 } 402 403 404 /** 405 * Wrapper for a String attribute which can be resolved via a {@code MessageSource}, 406 * falling back to the original attribute as a default value otherwise. 407 */ 408 @SuppressWarnings("serial") 409 private static class ResolvableAttribute implements MessageSourceResolvable, Serializable { 410 411 private final String resolvableString; 412 413 public ResolvableAttribute(String resolvableString) { 414 this.resolvableString = resolvableString; 415 } 416 417 @Override 418 public String[] getCodes() { 419 return new String[] {this.resolvableString}; 420 } 421 422 @Override 423 @Nullable 424 public Object[] getArguments() { 425 return null; 426 } 427 428 @Override 429 public String getDefaultMessage() { 430 return this.resolvableString; 431 } 432 433 @Override 434 public String toString() { 435 return this.resolvableString; 436 } 437 } 438 439 440 /** 441 * Subclass of {@code ObjectError} with Spring-style default message rendering. 442 */ 443 @SuppressWarnings("serial") 444 private static class ViolationObjectError extends ObjectError implements Serializable { 445 446 @Nullable 447 private transient SpringValidatorAdapter adapter; 448 449 @Nullable 450 private transient ConstraintViolation<?> violation; 451 452 public ViolationObjectError(String objectName, String[] codes, Object[] arguments, 453 ConstraintViolation<?> violation, SpringValidatorAdapter adapter) { 454 455 super(objectName, codes, arguments, violation.getMessage()); 456 this.adapter = adapter; 457 this.violation = violation; 458 wrap(violation); 459 } 460 461 @Override 462 public boolean shouldRenderDefaultMessage() { 463 return (this.adapter != null && this.violation != null ? 464 this.adapter.requiresMessageFormat(this.violation) : 465 containsSpringStylePlaceholder(getDefaultMessage())); 466 } 467 } 468 469 470 /** 471 * Subclass of {@code FieldError} with Spring-style default message rendering. 472 */ 473 @SuppressWarnings("serial") 474 private static class ViolationFieldError extends FieldError implements Serializable { 475 476 @Nullable 477 private transient SpringValidatorAdapter adapter; 478 479 @Nullable 480 private transient ConstraintViolation<?> violation; 481 482 public ViolationFieldError(String objectName, String field, @Nullable Object rejectedValue, String[] codes, 483 Object[] arguments, ConstraintViolation<?> violation, SpringValidatorAdapter adapter) { 484 485 super(objectName, field, rejectedValue, false, codes, arguments, violation.getMessage()); 486 this.adapter = adapter; 487 this.violation = violation; 488 wrap(violation); 489 } 490 491 @Override 492 public boolean shouldRenderDefaultMessage() { 493 return (this.adapter != null && this.violation != null ? 494 this.adapter.requiresMessageFormat(this.violation) : 495 containsSpringStylePlaceholder(getDefaultMessage())); 496 } 497 } 498 499}