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

Fix error message when introspection is missing and method has generic signature #443

Open
wants to merge 2 commits 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
Expand Up @@ -6,13 +6,12 @@
import jakarta.inject.Singleton;
import jakarta.validation.Valid;
import org.junit.jupiter.api.Test;
import spock.lang.Specification;

import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;

@Property(name = "spec.name", value = "PojoOptionalGetterSpec")
@MicronautTest
class PojoOptionalGetterTest extends Specification {
class PojoOptionalGetterSpec {

@Test
void pojoCanHaveGetterWhichReturnsAnOptional(MockService service) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package io.micronaut.docs.validation;

import io.micronaut.context.annotation.Property;
import io.micronaut.context.annotation.Requires;
import io.micronaut.core.annotation.Introspected;
import io.micronaut.test.extensions.junit5.annotation.MicronautTest;
import io.micronaut.validation.Validated;
import jakarta.inject.Inject;
import jakarta.inject.Singleton;
import jakarta.validation.ConstraintViolationException;
import jakarta.validation.Valid;
import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;

@Property(name = "spec.name", value = "WrongValidationTest")
@MicronautTest
class WrongValidationTest {

@Inject
ValidatedService validatedService;

@Test
void validateEvent() {
assertDoesNotThrow(() -> validatedService.send(new IntrospectedEvent()));

var ex = assertThrows(ConstraintViolationException.class, () -> validatedService.send(new NonIntrospectedEvent()));

assertTrue(ex.getMessage().contains("NonIntrospectedEvent"));
}

@Requires(property = "spec.name", value = "WrongValidationTest")
@Singleton
@Validated
static class ValidatedService {

<E> void send(@Valid E event) {

}
}

@Introspected
static class IntrospectedEvent {}

static class NonIntrospectedEvent {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
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,6 +55,7 @@ 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.
Expand All @@ -63,8 +66,10 @@ public class ValidatingInterceptor implements MethodInterceptor<Object, Object>
*/
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 +108,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 +121,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 +162,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 +176,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,16 @@
public class ConstraintExceptionHandler implements ExceptionHandler<ConstraintViolationException, HttpResponse<?>> {

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
Expand Down Expand Up @@ -85,38 +88,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 Expand Up @@ -1357,7 +1361,7 @@ private <R, E> void propagateValidation(DefaultConstraintValidatorContext<R> con
final BeanIntrospection<E> beanIntrospection = getBeanIntrospection(elementValue, elementType.getType());
if (beanIntrospection == null) {
// Error if not introspected
ConstraintDescriptor<Annotation> constraintDescriptor = notIntrospectedConstraint(elementType);
ConstraintDescriptor<Annotation> constraintDescriptor = notIntrospectedConstraint(elementType, elementValue);
DefaultConstraintViolation<R> violation = createConstraintViolation(context, leftBean, elementValue, constraintDescriptor);
context.addViolation(violation);
return;
Expand Down Expand Up @@ -1584,7 +1588,7 @@ public static String requireNonEmpty(String name, String value) {
return value;
}

private static ConstraintDescriptor<Annotation> notIntrospectedConstraint(Argument<?> notIntrospectedArgument) {
private static <E> ConstraintDescriptor<Annotation> notIntrospectedConstraint(Argument<E> notIntrospectedArgument, E elementValue) {
return new ConstraintDescriptor<>() {

@Override
Expand Down Expand Up @@ -1619,7 +1623,11 @@ public ConstraintTarget getValidationAppliesTo() {

@Override
public Map<String, Object> getAttributes() {
return Collections.singletonMap("type", notIntrospectedArgument.getType().getName());
var argType = notIntrospectedArgument.getType().getName();
if (notIntrospectedArgument.isTypeVariable()) {
argType = elementValue.getClass().getName();
}
return Collections.singletonMap("type", argType);
}

@Override
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
Loading