diff --git a/validation/src/main/java/io/micronaut/validation/validator/DefaultConstraintValidatorContext.java b/validation/src/main/java/io/micronaut/validation/validator/DefaultConstraintValidatorContext.java index ab2f69cf..80fcf822 100644 --- a/validation/src/main/java/io/micronaut/validation/validator/DefaultConstraintValidatorContext.java +++ b/validation/src/main/java/io/micronaut/validation/validator/DefaultConstraintValidatorContext.java @@ -56,7 +56,7 @@ * @param The root bean type */ @Internal -final class DefaultConstraintValidatorContext implements ConstraintValidatorContext { +public final class DefaultConstraintValidatorContext implements ConstraintValidatorContext { private static final Map, List>> GROUP_SEQUENCES = new ConcurrentHashMap<>(); private static final List> DEFAULT_GROUPS = Collections.singletonList(Default.class); diff --git a/validation/src/main/java/io/micronaut/validation/validator/DefaultConstraintViolationBuilder.java b/validation/src/main/java/io/micronaut/validation/validator/DefaultConstraintViolationBuilder.java index eb262ef8..85666c34 100644 --- a/validation/src/main/java/io/micronaut/validation/validator/DefaultConstraintViolationBuilder.java +++ b/validation/src/main/java/io/micronaut/validation/validator/DefaultConstraintViolationBuilder.java @@ -17,12 +17,11 @@ import io.micronaut.core.annotation.Internal; import io.micronaut.core.annotation.Nullable; +import io.micronaut.validation.validator.messages.DefaultMessageInterpolatorContext; import jakarta.validation.ConstraintValidatorContext; import jakarta.validation.ElementKind; import jakarta.validation.MessageInterpolator; import jakarta.validation.Path; -import jakarta.validation.ValidationException; -import jakarta.validation.metadata.ConstraintDescriptor; /** * The default implementation {@link ConstraintValidatorContext.ConstraintViolationBuilder}. @@ -122,22 +121,11 @@ public ConstraintValidatorContext addConstraintViolation() { null, null, messageTemplate, - messageInterpolator.interpolate(messageTemplate, new MessageInterpolator.Context() { - @Override - public ConstraintDescriptor getConstraintDescriptor() { - return constraintValidatorContext.constraint; - } - - @Override - public Object getValidatedValue() { - return null; - } - - @Override - public T unwrap(Class type) { - throw new ValidationException("Not supported!"); - } - }), + messageInterpolator.interpolate(messageTemplate, new DefaultMessageInterpolatorContext( + constraintValidatorContext, + constraintValidatorContext.constraint, + null + )), validationPath.iterator().hasNext() ? validationPath : new ValidationPath(constraintValidatorContext.getCurrentPath()), constraintValidatorContext.constraint, null, 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..26e3f9c8 100644 --- a/validation/src/main/java/io/micronaut/validation/validator/DefaultValidator.java +++ b/validation/src/main/java/io/micronaut/validation/validator/DefaultValidator.java @@ -56,6 +56,7 @@ import io.micronaut.validation.validator.constraints.InternalConstraintValidatorFactory; import io.micronaut.validation.validator.extractors.ValueExtractorDefinition; import io.micronaut.validation.validator.extractors.ValueExtractorRegistry; +import io.micronaut.validation.validator.messages.DefaultMessageInterpolatorContext; import jakarta.inject.Singleton; import jakarta.validation.ClockProvider; import jakarta.validation.Constraint; @@ -1472,22 +1473,7 @@ private DefaultConstraintViolation createConstraintViolation(DefaultConst Object elementValue, ConstraintDescriptor constraint) { final String messageTemplate = buildMessageTemplate(context, constraint); - final String message = messageInterpolator.interpolate(messageTemplate, new MessageInterpolator.Context() { - @Override - public ConstraintDescriptor getConstraintDescriptor() { - return constraint; - } - - @Override - public Object getValidatedValue() { - return elementValue; - } - - @Override - public T unwrap(Class type) { - throw new ValidationException("Not supported!"); - } - }); + final String message = messageInterpolator.interpolate(messageTemplate, new DefaultMessageInterpolatorContext(context, constraint, elementValue)); return new DefaultConstraintViolation<>( context.getRootBean(), diff --git a/validation/src/main/java/io/micronaut/validation/validator/messages/DefaultMessageInterpolator.java b/validation/src/main/java/io/micronaut/validation/validator/messages/DefaultMessageInterpolator.java index 4b2f3822..986ea4da 100644 --- a/validation/src/main/java/io/micronaut/validation/validator/messages/DefaultMessageInterpolator.java +++ b/validation/src/main/java/io/micronaut/validation/validator/messages/DefaultMessageInterpolator.java @@ -23,7 +23,6 @@ import java.util.HashMap; import java.util.Locale; -import java.util.Map; /** * The default error messages. @@ -49,8 +48,8 @@ private String interpolate(@NonNull String template, @NonNull MessageSource.Mess ArgumentUtils.requireNonNull("template", template); ArgumentUtils.requireNonNull("context", context); - StringBuilder messageBuilder = new StringBuilder(); - StringBuilder variableBuilder = new StringBuilder(); + var messageBuilder = new StringBuilder(); + var variableBuilder = new StringBuilder(); StringBuilder builder = messageBuilder; boolean isVariable = false; for (int i = 0; i < template.length(); i++) { @@ -108,8 +107,11 @@ public String interpolate(String messageTemplate, Context context) { @Override public String interpolate(String messageTemplate, Context context, Locale locale) { - Map attributes = new HashMap<>(context.getConstraintDescriptor().getAttributes()); + var attributes = new HashMap<>(context.getConstraintDescriptor().getAttributes()); attributes.put("validatedValue", context.getValidatedValue()); + if (context instanceof DefaultMessageInterpolatorContext interpolatorContext) { + attributes.put("validatedPath", interpolatorContext.getValidatorContext().getCurrentPath()); + } return interpolate(messageTemplate, MessageSource.MessageContext.of(locale, attributes)); } } diff --git a/validation/src/main/java/io/micronaut/validation/validator/messages/DefaultMessageInterpolatorContext.java b/validation/src/main/java/io/micronaut/validation/validator/messages/DefaultMessageInterpolatorContext.java new file mode 100644 index 00000000..ac2acac7 --- /dev/null +++ b/validation/src/main/java/io/micronaut/validation/validator/messages/DefaultMessageInterpolatorContext.java @@ -0,0 +1,61 @@ +/* + * 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.validator.messages; + +import io.micronaut.core.annotation.Nullable; +import io.micronaut.validation.validator.DefaultConstraintValidatorContext; +import jakarta.validation.MessageInterpolator; +import jakarta.validation.ValidationException; +import jakarta.validation.metadata.ConstraintDescriptor; + +import java.lang.annotation.Annotation; + +public class DefaultMessageInterpolatorContext implements MessageInterpolator.Context { + + private final DefaultConstraintValidatorContext validatorContext; + private final ConstraintDescriptor constraintDescriptor; + @Nullable + private final Object validatedValue; + + public DefaultMessageInterpolatorContext( + DefaultConstraintValidatorContext validatorContext, + ConstraintDescriptor constraintDescriptor, + @Nullable Object validatedValue + ) { + this.validatorContext = validatorContext; + this.constraintDescriptor = constraintDescriptor; + this.validatedValue = validatedValue; + } + + public DefaultConstraintValidatorContext getValidatorContext() { + return validatorContext; + } + + @Override + public ConstraintDescriptor getConstraintDescriptor() { + return constraintDescriptor; + } + + @Override + public Object getValidatedValue() { + return validatedValue; + } + + @Override + public T unwrap(Class type) { + throw new ValidationException("Not supported!"); + } +} diff --git a/validation/src/test/groovy/io/micronaut/validation/validator/MyBook.java b/validation/src/test/groovy/io/micronaut/validation/validator/MyBook.java new file mode 100644 index 00000000..bd96fb20 --- /dev/null +++ b/validation/src/test/groovy/io/micronaut/validation/validator/MyBook.java @@ -0,0 +1,20 @@ +package io.micronaut.validation.validator; + +import io.micronaut.core.annotation.Introspected; +import io.micronaut.validation.Validated; +import jakarta.validation.constraints.Size; + +@Validated +@Introspected +class MyBook { + @Size(max = 2, message = "Check path: {validatedPath} with value: {validatedValue}") + private String title; + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } +} diff --git a/validation/src/test/groovy/io/micronaut/validation/validator/ValidatorSpec.groovy b/validation/src/test/groovy/io/micronaut/validation/validator/ValidatorSpec.groovy index c689ff6e..9d3c6faa 100644 --- a/validation/src/test/groovy/io/micronaut/validation/validator/ValidatorSpec.groovy +++ b/validation/src/test/groovy/io/micronaut/validation/validator/ValidatorSpec.groovy @@ -920,6 +920,15 @@ class ValidatorSpec extends Specification { Exception e = thrown() e.message.contains('''myMethod2.bean.number: must be less than or equal to 20''') } + + void "test message interpolator context attributes"() { + when: + def service = applicationContext.getBean(MyBookService) + def book = service.saveBook(new MyBook(title: "too long name")) + then: + Exception e = thrown() + e.message.contains('''saveBook.book.title: Check path: saveBook.book.title with value: too long name''') + } } class Bean extends AbstractMap { @@ -1115,3 +1124,11 @@ class BookService { } } +@Validated +@Singleton +class MyBookService { + + MyBook saveBook(@Valid MyBook book) { + book + } +}