Skip to content

Commit

Permalink
Add ability to disable automatically prepend property path in excepti…
Browse files Browse the repository at this point in the history
…on message.

Fixed micronaut-projects#286
  • Loading branch information
altro3 committed Nov 12, 2024
1 parent 06be39e commit 1fe9350
Show file tree
Hide file tree
Showing 9 changed files with 177 additions and 37 deletions.
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

0 comments on commit 1fe9350

Please sign in to comment.