From 1fe935083702d57567e728bc2a42bfd3646a31b3 Mon Sep 17 00:00:00 2001 From: Alexey Trofimov Date: Tue, 12 Nov 2024 17:17:47 +0700 Subject: [PATCH] Add ability to disable automatically prepend property path in exception message. Fixed #286 --- .../ConstraintViolationExceptionUtil.java | 49 ++++++++++++++++ .../validation/ValidatingInterceptor.java | 18 ++++-- .../ConstraintExceptionHandler.java | 57 +++++++++++-------- .../validator/DefaultValidator.java | 10 +++- .../DefaultValidatorConfiguration.java | 21 ++++++- .../validator/ValidatorConfiguration.java | 9 +++ .../micronaut/validation/ValidatedSpec.groovy | 3 +- .../DisabledPrependPropertyPathSpec.groovy | 42 ++++++++++++++ .../replacement/ReplaceInterceptorSpec.groovy | 5 +- 9 files changed, 177 insertions(+), 37 deletions(-) create mode 100644 validation/src/main/java/io/micronaut/validation/ConstraintViolationExceptionUtil.java create mode 100644 validation/src/test/groovy/io/micronaut/validation/validator/DisabledPrependPropertyPathSpec.groovy diff --git a/validation/src/main/java/io/micronaut/validation/ConstraintViolationExceptionUtil.java b/validation/src/main/java/io/micronaut/validation/ConstraintViolationExceptionUtil.java new file mode 100644 index 00000000..272a3ec2 --- /dev/null +++ b/validation/src/main/java/io/micronaut/validation/ConstraintViolationExceptionUtil.java @@ -0,0 +1,49 @@ +/* + * Copyright 2017-2024 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.validation; + +import jakarta.validation.ConstraintViolation; +import jakarta.validation.ConstraintViolationException; + +import java.util.Set; +import java.util.stream.Collectors; + +/** + * Utility class for ConstraintViolationException. + * + * @since 4.9.0 + */ +public final class ConstraintViolationExceptionUtil { + + private ConstraintViolationExceptionUtil() { + } + + /** + * Method to create ConstraintViolationException with custom message. + * + * @param prependPropertyPath prependPropertyPath configuration flag + * @param constraintViolations constraint violations + * @return ConstraintViolationException + */ + public static ConstraintViolationException createConstraintViolationException(boolean prependPropertyPath, Set> constraintViolations) { + var message = constraintViolations.stream() + .map(cv -> cv == null ? "null" : (prependPropertyPath ? cv.getPropertyPath() + ": " : "") + cv.getMessage()) + .collect(Collectors.joining(", ")); + + return new ConstraintViolationException(message, constraintViolations); + } + +} diff --git a/validation/src/main/java/io/micronaut/validation/ValidatingInterceptor.java b/validation/src/main/java/io/micronaut/validation/ValidatingInterceptor.java index c645280b..81015a6e 100644 --- a/validation/src/main/java/io/micronaut/validation/ValidatingInterceptor.java +++ b/validation/src/main/java/io/micronaut/validation/ValidatingInterceptor.java @@ -26,9 +26,9 @@ import io.micronaut.validation.validator.ExecutableMethodValidator; import io.micronaut.validation.validator.ReactiveValidator; import io.micronaut.validation.validator.Validator; +import io.micronaut.validation.validator.ValidatorConfiguration; import jakarta.inject.Singleton; import jakarta.validation.ConstraintViolation; -import jakarta.validation.ConstraintViolationException; import jakarta.validation.ValidatorFactory; import jakarta.validation.executable.ExecutableValidator; @@ -36,6 +36,8 @@ import java.util.Set; import java.util.concurrent.CompletionStage; +import static io.micronaut.validation.ConstraintViolationExceptionUtil.createConstraintViolationException; + /** * A {@link MethodInterceptor} that validates method invocations. * @@ -53,6 +55,7 @@ public class ValidatingInterceptor implements MethodInterceptor private final @Nullable ExecutableValidator executableValidator; private final @Nullable ExecutableMethodValidator micronautValidator; private final ConversionService conversionService; + private final boolean isPrependPropertyPath; /** * Creates ValidatingInterceptor from the validatorFactory. @@ -60,11 +63,14 @@ public class ValidatingInterceptor implements MethodInterceptor * @param micronautValidator The micronaut validator use if no factory is available * @param validatorFactory Factory returning initialized {@code Validator} instances * @param conversionService The conversion service + * @param validatorConfiguration validator configuration instance */ public ValidatingInterceptor(@Nullable Validator micronautValidator, @Nullable ValidatorFactory validatorFactory, - ConversionService conversionService) { + ConversionService conversionService, + ValidatorConfiguration validatorConfiguration) { this.conversionService = conversionService; + isPrependPropertyPath = validatorConfiguration.isPrependPropertyPath(); if (validatorFactory != null) { jakarta.validation.Validator validator = validatorFactory.getValidator(); @@ -103,7 +109,7 @@ public Object intercept(MethodInvocationContext context) { getValidationGroups(context) ); if (!constraintViolations.isEmpty()) { - throw new ConstraintViolationException(constraintViolations); + throw createConstraintViolationException(isPrependPropertyPath, constraintViolations); } } return validateReturnExecutableValidator(context, targetMethod); @@ -116,7 +122,7 @@ public Object intercept(MethodInvocationContext context) { context.getParameterValues(), getValidationGroups(context)); if (!constraintViolations.isEmpty()) { - throw new ConstraintViolationException(constraintViolations); + throw createConstraintViolationException(isPrependPropertyPath, constraintViolations); } } if (micronautValidator instanceof ReactiveValidator reactiveValidator) { @@ -157,7 +163,7 @@ private Object validateReturnMicronautValidator(MethodInvocationContext> { private final ErrorResponseProcessor responseProcessor; + private final ValidatorConfiguration validatorConfiguration; /** * Constructor. + * * @param responseProcessor Error Response Processor + * @param validatorConfiguration validator configuration bean */ @Inject - public ConstraintExceptionHandler(ErrorResponseProcessor responseProcessor) { + public ConstraintExceptionHandler(ErrorResponseProcessor responseProcessor, ValidatorConfiguration validatorConfiguration) { this.responseProcessor = responseProcessor; + this.validatorConfiguration = validatorConfiguration; } @Override @@ -85,38 +90,42 @@ public HttpResponse handle(HttpRequest request, ConstraintViolationException */ protected String buildMessage(ConstraintViolation violation) { Path propertyPath = violation.getPropertyPath(); - StringBuilder message = new StringBuilder(); - Iterator i = propertyPath.iterator(); + var message = new StringBuilder(); - boolean firstNode = true; + if (validatorConfiguration.isPrependPropertyPath()) { + Iterator i = propertyPath.iterator(); - while (i.hasNext()) { - Path.Node node = i.next(); + boolean firstNode = true; - if (node.getKind() == ElementKind.METHOD || node.getKind() == ElementKind.CONSTRUCTOR) { - continue; - } + while (i.hasNext()) { + Path.Node node = i.next(); - if (node.isInIterable()) { - message.append('['); - if (node.getKey() != null) { - message.append(node.getKey()); - } else if (node.getIndex() != null) { - message.append(node.getIndex()); + if (node.getKind() == ElementKind.METHOD || node.getKind() == ElementKind.CONSTRUCTOR) { + continue; } - message.append(']'); - } - if (node.getKind() != ElementKind.CONTAINER_ELEMENT && node.getName() != null) { - if (!firstNode) { - message.append('.'); + + if (node.isInIterable()) { + message.append('['); + if (node.getKey() != null) { + message.append(node.getKey()); + } else if (node.getIndex() != null) { + message.append(node.getIndex()); + } + message.append(']'); } - message.append(node.getName()); + if (node.getKind() != ElementKind.CONTAINER_ELEMENT && node.getName() != null) { + if (!firstNode) { + message.append('.'); + } + message.append(node.getName()); + } + + firstNode = false; } - firstNode = false; + message.append(": "); } - - message.append(": ").append(violation.getMessage()); + message.append(violation.getMessage()); return message.toString(); } diff --git a/validation/src/main/java/io/micronaut/validation/validator/DefaultValidator.java b/validation/src/main/java/io/micronaut/validation/validator/DefaultValidator.java index 8141e639..86df1565 100644 --- a/validation/src/main/java/io/micronaut/validation/validator/DefaultValidator.java +++ b/validation/src/main/java/io/micronaut/validation/validator/DefaultValidator.java @@ -92,6 +92,8 @@ import java.util.function.Function; import java.util.stream.Collectors; +import static io.micronaut.validation.ConstraintViolationExceptionUtil.createConstraintViolationException; + /** * Default implementation of the {@link Validator} interface. * @@ -122,6 +124,7 @@ public class DefaultValidator implements private final ConversionService conversionService; private final BeanIntrospector beanIntrospector; private final InternalConstraintValidatorFactory constraintValidatorFactory; + private final boolean isPrependPropertyPath; /** * Default constructor. @@ -139,6 +142,7 @@ public DefaultValidator(@NonNull ValidatorConfiguration configuration) { this.conversionService = configuration.getConversionService(); this.beanIntrospector = configuration.getBeanIntrospector(); this.constraintValidatorFactory = (InternalConstraintValidatorFactory) configuration.getConstraintValidatorFactory(); + this.isPrependPropertyPath = configuration.isPrependPropertyPath(); } /** @@ -339,7 +343,7 @@ public T createValid(@NonNull Class beanType, Object... arguments) throws final Set> constraintViolations = validateConstructorParameters(introspection, arguments); if (!constraintViolations.isEmpty()) { - throw new ConstraintViolationException(constraintViolations); + throw createConstraintViolationException(isPrependPropertyPath, constraintViolations); } final T instance = introspection.instantiate(arguments); @@ -347,7 +351,7 @@ public T createValid(@NonNull Class beanType, Object... arguments) throws if (errors.isEmpty()) { return instance; } - throw new ConstraintViolationException(errors); + throw createConstraintViolationException(isPrependPropertyPath, errors); } @Override @@ -912,7 +916,7 @@ private CompletionStage instrumentCompletionStage(DefaultConstraintVal } if (!context.getOverallViolations().isEmpty()) { - throw new ConstraintViolationException(context.getOverallViolations()); + throw createConstraintViolationException(isPrependPropertyPath, context.getOverallViolations()); } return value; diff --git a/validation/src/main/java/io/micronaut/validation/validator/DefaultValidatorConfiguration.java b/validation/src/main/java/io/micronaut/validation/validator/DefaultValidatorConfiguration.java index ac3d561d..4a653884 100644 --- a/validation/src/main/java/io/micronaut/validation/validator/DefaultValidatorConfiguration.java +++ b/validation/src/main/java/io/micronaut/validation/validator/DefaultValidatorConfiguration.java @@ -38,8 +38,8 @@ import io.micronaut.validation.validator.extractors.DefaultValueExtractors; import io.micronaut.validation.validator.extractors.ValueExtractorDefinition; import io.micronaut.validation.validator.extractors.ValueExtractorRegistry; -import io.micronaut.validation.validator.messages.DefaultMessages; import io.micronaut.validation.validator.messages.DefaultMessageInterpolator; +import io.micronaut.validation.validator.messages.DefaultMessages; import jakarta.inject.Inject; import jakarta.validation.ClockProvider; import jakarta.validation.ConstraintValidatorFactory; @@ -113,6 +113,7 @@ public class DefaultValidatorConfiguration implements ValidatorConfiguration, To private BeanIntrospector beanIntrospector = BeanIntrospector.SHARED; private boolean enabled = true; + private boolean prependPropertyPath = true; /** * Sets the conversion service. @@ -173,6 +174,24 @@ public DefaultValidatorConfiguration setEnabled(boolean enabled) { return this; } + @Override + public boolean isPrependPropertyPath() { + return prependPropertyPath; + } + + /** + * If true, then the path to the property will be automatically added to the error message. + *

+ * Default: true + * + * @param prependPropertyPath If true, then the path to the property will be automatically added to the error message. + * @return this configuration + */ + public DefaultValidatorConfiguration setPrependPropertyPath(boolean prependPropertyPath) { + this.prependPropertyPath = prependPropertyPath; + return this; + } + /** * Sets the constraint validator registry to use. * diff --git a/validation/src/main/java/io/micronaut/validation/validator/ValidatorConfiguration.java b/validation/src/main/java/io/micronaut/validation/validator/ValidatorConfiguration.java index bf5e8b4b..e4c7981a 100644 --- a/validation/src/main/java/io/micronaut/validation/validator/ValidatorConfiguration.java +++ b/validation/src/main/java/io/micronaut/validation/validator/ValidatorConfiguration.java @@ -106,6 +106,15 @@ public interface ValidatorConfiguration extends ConversionServiceProvider { @NonNull ExecutionHandleLocator getExecutionHandleLocator(); + /** + * If true, then the path to the property will be automatically added to the error message. + *

+ * Default: true + * + * @return prependPropertyPath flag value + */ + boolean isPrependPropertyPath(); + /** * The bean introspector. * @return The introspector diff --git a/validation/src/test/groovy/io/micronaut/validation/ValidatedSpec.groovy b/validation/src/test/groovy/io/micronaut/validation/ValidatedSpec.groovy index cdef4fc5..141e08be 100644 --- a/validation/src/test/groovy/io/micronaut/validation/ValidatedSpec.groovy +++ b/validation/src/test/groovy/io/micronaut/validation/ValidatedSpec.groovy @@ -39,6 +39,7 @@ import io.micronaut.http.client.HttpClient import io.micronaut.http.client.annotation.Client import io.micronaut.http.client.exceptions.HttpClientResponseException import io.micronaut.runtime.server.EmbeddedServer +import io.micronaut.validation.validator.DefaultValidatorConfiguration import reactor.core.publisher.Flux import spock.lang.Specification @@ -71,7 +72,7 @@ class ValidatedSpec extends Specification { Object intercept(InvocationContext context) { return null } - }, new ValidatingInterceptor(null, null, ConversionService.SHARED)] + }, new ValidatingInterceptor(null, null, ConversionService.SHARED, new DefaultValidatorConfiguration())] OrderUtil.sort(list) expect: diff --git a/validation/src/test/groovy/io/micronaut/validation/validator/DisabledPrependPropertyPathSpec.groovy b/validation/src/test/groovy/io/micronaut/validation/validator/DisabledPrependPropertyPathSpec.groovy new file mode 100644 index 00000000..00931f45 --- /dev/null +++ b/validation/src/test/groovy/io/micronaut/validation/validator/DisabledPrependPropertyPathSpec.groovy @@ -0,0 +1,42 @@ +package io.micronaut.validation.validator + +import io.micronaut.context.ApplicationContext +import io.micronaut.validation.Validated +import jakarta.inject.Singleton +import jakarta.validation.Valid +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Min +import spock.lang.AutoCleanup +import spock.lang.Shared +import spock.lang.Specification + +class DisabledPrependPropertyPathSpec extends Specification { + + @Shared + @AutoCleanup + ApplicationContext applicationContext = ApplicationContext.run(["micronaut.validator.prepend-property-path": false]) + + void "test disabled prependPropertyPath"() { + when: + def service = applicationContext.getBean(MyService3) + def bean = new MyBeanWithPrimitives() + bean.number = 100 + service.myMethod2(bean) + then: + Exception e = thrown() + e.message == 'must be less than or equal to 20' + } +} + +@Validated +@Singleton +class MyService3 { + + @Min(10) + int myMethod1(@Max(5) int a) { + return a + } + + void myMethod2(@Valid MyBeanWithPrimitives bean) { + } +} diff --git a/validation/src/test/groovy/io/micronaut/validation/validator/replacement/ReplaceInterceptorSpec.groovy b/validation/src/test/groovy/io/micronaut/validation/validator/replacement/ReplaceInterceptorSpec.groovy index 5da9fdf4..4b14eefb 100644 --- a/validation/src/test/groovy/io/micronaut/validation/validator/replacement/ReplaceInterceptorSpec.groovy +++ b/validation/src/test/groovy/io/micronaut/validation/validator/replacement/ReplaceInterceptorSpec.groovy @@ -12,6 +12,7 @@ import io.micronaut.validation.Validated import io.micronaut.validation.ValidatingInterceptor import io.micronaut.validation.validator.BeanValidationContext import io.micronaut.validation.validator.Validator +import io.micronaut.validation.validator.ValidatorConfiguration import jakarta.inject.Inject import jakarta.inject.Singleton import jakarta.validation.ConstraintViolation @@ -38,8 +39,8 @@ class ReplaceInterceptorSpec extends Specification { @InterceptorBean(Validated) static class MyInterceptor extends ValidatingInterceptor { Validator micronautValidator - MyInterceptor(Validator micronautValidator, ValidatorFactory validatorFactory, ConversionService conversionService) { - super(micronautValidator, validatorFactory, conversionService) + MyInterceptor(Validator micronautValidator, ValidatorFactory validatorFactory, ConversionService conversionService, ValidatorConfiguration validatorConfiguration) { + super(micronautValidator, validatorFactory, conversionService, validatorConfiguration) this.micronautValidator = micronautValidator; }