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..093ef7a8 --- /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..aa10a8ab 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. @@ -63,8 +66,10 @@ public class ValidatingInterceptor implements MethodInterceptor */ 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 +108,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 +121,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 +162,7 @@ private Object validateReturnMicronautValidator(MethodInvocationContext> { private final ErrorResponseProcessor responseProcessor; + private final ValidatorConfiguration validatorConfiguration; /** * Constructor. * @param responseProcessor Error Response Processor */ @Inject - public ConstraintExceptionHandler(ErrorResponseProcessor responseProcessor) { + public ConstraintExceptionHandler(ErrorResponseProcessor responseProcessor, ValidatorConfiguration validatorConfiguration) { this.responseProcessor = responseProcessor; + this.validatorConfiguration = validatorConfiguration; } @Override @@ -85,38 +88,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/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) { + } +}