Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ability to disable automatically prepend property path in exception message. #449

Open
wants to merge 1 commit into
base: 4.9.x
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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<? extends ConstraintViolation<?>> constraintViolations) {
var message = constraintViolations.stream()
.map(cv -> cv == null ? "null" : (prependPropertyPath ? cv.getPropertyPath() + ": " : "") + cv.getMessage())
.collect(Collectors.joining(", "));

return new ConstraintViolationException(message, constraintViolations);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -26,16 +26,18 @@
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;

import java.lang.reflect.Method;
import java.util.Set;
import java.util.concurrent.CompletionStage;

import static io.micronaut.validation.ConstraintViolationExceptionUtil.createConstraintViolationException;

/**
* A {@link MethodInterceptor} that validates method invocations.
*
Expand All @@ -53,18 +55,22 @@ public class ValidatingInterceptor implements MethodInterceptor<Object, Object>
private final @Nullable ExecutableValidator executableValidator;
private final @Nullable ExecutableMethodValidator micronautValidator;
private final ConversionService conversionService;
private final boolean isPrependPropertyPath;

/**
* Creates ValidatingInterceptor from the validatorFactory.
*
* @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();
Expand Down Expand Up @@ -103,7 +109,7 @@ public Object intercept(MethodInvocationContext<Object, Object> context) {
getValidationGroups(context)
);
if (!constraintViolations.isEmpty()) {
throw new ConstraintViolationException(constraintViolations);
throw createConstraintViolationException(isPrependPropertyPath, constraintViolations);
}
}
return validateReturnExecutableValidator(context, targetMethod);
Expand All @@ -116,7 +122,7 @@ public Object intercept(MethodInvocationContext<Object, Object> context) {
context.getParameterValues(),
getValidationGroups(context));
if (!constraintViolations.isEmpty()) {
throw new ConstraintViolationException(constraintViolations);
throw createConstraintViolationException(isPrependPropertyPath, constraintViolations);
}
}
if (micronautValidator instanceof ReactiveValidator reactiveValidator) {
Expand Down Expand Up @@ -157,7 +163,7 @@ private Object validateReturnMicronautValidator(MethodInvocationContext<Object,
result,
getValidationGroups(context));
if (!constraintViolations.isEmpty()) {
throw new ConstraintViolationException(constraintViolations);
throw createConstraintViolationException(isPrependPropertyPath, constraintViolations);
}
return result;
}
Expand All @@ -171,7 +177,7 @@ private Object validateReturnExecutableValidator(MethodInvocationContext<Object,
getValidationGroups(context)
);
if (!constraintViolations.isEmpty()) {
throw new ConstraintViolationException(constraintViolations);
throw createConstraintViolationException(isPrependPropertyPath, constraintViolations);
}
return result;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import io.micronaut.http.server.exceptions.ExceptionHandler;
import io.micronaut.http.server.exceptions.response.ErrorContext;
import io.micronaut.http.server.exceptions.response.ErrorResponseProcessor;
import io.micronaut.validation.validator.ValidatorConfiguration;
import jakarta.inject.Inject;
import jakarta.inject.Singleton;

Expand All @@ -47,14 +48,18 @@
public class ConstraintExceptionHandler implements ExceptionHandler<ConstraintViolationException, HttpResponse<?>> {

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
Expand Down Expand Up @@ -85,38 +90,42 @@ public HttpResponse<?> handle(HttpRequest request, ConstraintViolationException
*/
protected String buildMessage(ConstraintViolation<?> violation) {
Path propertyPath = violation.getPropertyPath();
StringBuilder message = new StringBuilder();
Iterator<Path.Node> i = propertyPath.iterator();
var message = new StringBuilder();

boolean firstNode = true;
if (validatorConfiguration.isPrependPropertyPath()) {
Iterator<Path.Node> 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();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down Expand Up @@ -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.
Expand All @@ -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();
}

/**
Expand Down Expand Up @@ -339,15 +343,15 @@ public <T> T createValid(@NonNull Class<T> beanType, Object... arguments) throws

final Set<ConstraintViolation<T>> constraintViolations = validateConstructorParameters(introspection, arguments);
if (!constraintViolations.isEmpty()) {
throw new ConstraintViolationException(constraintViolations);
throw createConstraintViolationException(isPrependPropertyPath, constraintViolations);
}

final T instance = introspection.instantiate(arguments);
final Set<ConstraintViolation<T>> errors = validate(introspection, instance);
if (errors.isEmpty()) {
return instance;
}
throw new ConstraintViolationException(errors);
throw createConstraintViolationException(isPrependPropertyPath, errors);
}

@Override
Expand Down Expand Up @@ -912,7 +916,7 @@ private <T, E> CompletionStage<E> instrumentCompletionStage(DefaultConstraintVal
}

if (!context.getOverallViolations().isEmpty()) {
throw new ConstraintViolationException(context.getOverallViolations());
throw createConstraintViolationException(isPrependPropertyPath, context.getOverallViolations());
}

return value;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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.
* <p>
* 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.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
* <p>
* Default: true
*
* @return prependPropertyPath flag value
*/
boolean isPrependPropertyPath();

/**
* The bean introspector.
* @return The introspector
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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:
Expand Down
Original file line number Diff line number Diff line change
@@ -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) {
}
}
Loading
Loading