2

I have resource bundle with one of the key/value pairs looking like:

my.key=A message specifying min: {min} and max: {max} parameters

It all works well when used with @Length annotation, Thymeleaf and validators. If error conditions are met the message gets resolved and displayed, however if I try to resolve the same message using the MessageSource.getMessage(...) method there's now way I can get this to work.

I tried the following;

messageSource.getMessage("my.key", new Object[] {Map.of("min", 4, "max", 16)}, validLocale);

also 

messageSource.getMessage("my.key", new Object[]{"{min:4}", "max:16"}, validLocale);


And a few more things but every time I get IllegalArgumentException caused by NumberFormatException with the message: "can't parse argument number: min"

Will appreciate any suggestions

hicnar
  • 163
  • 3
  • 9

3 Answers3

0

Your code should work, but you have to modify it slightly

 messageSource.getMessage("my.key", new Object[] { 4, 16 }, validLocale);

And change your key to something like

my.key=A message specifying min: {0} and max: {1} parameters

MessageSource doesn't support named arguments (e.g. {min}). min and max in @Length are just attributes -- they have nothing to with the message.properties' placeholders.

dsp_user
  • 2,061
  • 2
  • 16
  • 23
0

@dsp_user

I know very well that if modified as you proposed it the code would work, but the following code has to work too:

    @Length(max = 4, min = 16, message = "{my.key}") 
    private String username;

and it would cease to work if changed to what you suggested.

hicnar
  • 163
  • 3
  • 9
  • I suggest then that you ask a different question regarding the Length annotation (or perhaps the Size annotation because that's what's usually recommended to use with Spring boot). Your question was about getting `MessageSource.getMessage` to work with named parameters and I addressed that question. – dsp_user Feb 19 '23 at 20:36
  • or you can add separate keys for your annotation and MessageSource. Sure, it's redunandant but like I said MessageSource doesn't support named parameters. – dsp_user Feb 19 '23 at 20:47
  • Are you suggesting that there must be some external code (external to Spring, but present in SpringBoot or Thymeleaf) that makes it work in combination with named variables? To give more context, yes it is a SpringBoot app and it uses Thymeleaf and the code snippet with annotation I posted 100% works. – hicnar Feb 19 '23 at 20:56
  • the classes annotated with Entity are normally under control of JPA/Hibernate, not Spring Boot or Thymeleaf. MessageSource, on the other hand, is purely Spring. That's why I think you're wrong in thinking there's a connection between the two. – dsp_user Feb 19 '23 at 21:04
  • 1
    That's correct and it indeed could be that I quietly assumed MessageSource is capable of resolving messages with named parameters, but in fact there could be a different mechanism that implements that. Thanks for your suggestion and I will have another look. – hicnar Feb 19 '23 at 21:14
0

Confirmed that MessageSource doesn't support named parameters, only numbered ones, at least in the Spring version I'm using (from the pom.xml):

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.3.12.RELEASE</version>
    <relativePath />
</parent>

I've debugged a test using a ReloadableResourceBundleMessageSource messageSource and here's the result.

Java code line causing the exception:

String imsg = messageSource.getMessage("constraints.Size.message", new Object[] { 0, 255 }, Locale.getDefault());

Content of messages_en.properties:

constraints.Size.message=A message specifying min: {min} and max: {max} parameters

Exception Stack trace:

java.lang.IllegalArgumentException: can't parse argument number: min
    at java.base/java.text.MessageFormat.makeFormat(MessageFormat.java:1451)
    at java.base/java.text.MessageFormat.applyPattern(MessageFormat.java:491)
    at java.base/java.text.MessageFormat.<init>(MessageFormat.java:390)
    at org.springframework.context.support.MessageSourceSupport.createMessageFormat(MessageSourceSupport.java:159)
    at org.springframework.context.support.ReloadableResourceBundleMessageSource$PropertiesHolder.getMessageFormat(ReloadableResourceBundleMessageSource.java:627)
    at org.springframework.context.support.ReloadableResourceBundleMessageSource.resolveCode(ReloadableResourceBundleMessageSource.java:206)
    at org.springframework.context.support.AbstractMessageSource.getMessageInternal(AbstractMessageSource.java:224)
    at org.springframework.context.support.AbstractMessageSource.getMessage(AbstractMessageSource.java:153)

Delving into org.springframework.context.support.AbstractMessageSource.getMessageInternal():

    /**
     * Resolve the given code and arguments as message in the given Locale,
     * returning {@code null} if not found. Does <i>not</i> fall back to
     * the code as default message. Invoked by {@code getMessage} methods.
     * @param code the code to lookup up, such as 'calculator.noRateSet'
     * @param args array of arguments that will be filled in for params
     * within the message
     * @param locale the locale in which to do the lookup
     * @return the resolved message, or {@code null} if not found
     * @see #getMessage(String, Object[], String, Locale)
     * @see #getMessage(String, Object[], Locale)
     * @see #getMessage(MessageSourceResolvable, Locale)
     * @see #setUseCodeAsDefaultMessage
     */
    @Nullable
    protected String getMessageInternal(@Nullable String code, @Nullable Object[] args, @Nullable Locale locale) {
        if (code == null) {
            return null;
        }
        if (locale == null) {
            locale = Locale.getDefault();
        }
        Object[] argsToUse = args;

        if (!isAlwaysUseMessageFormat() && ObjectUtils.isEmpty(args)) {
            // Optimized resolution: no arguments to apply,
            // therefore no MessageFormat needs to be involved.
            // Note that the default implementation still uses MessageFormat;
            // this can be overridden in specific subclasses.
            String message = resolveCodeWithoutArguments(code, locale);
            if (message != null) {
                return message;
            }
        }

        else {
            // Resolve arguments eagerly, for the case where the message
            // is defined in a parent MessageSource but resolvable arguments
            // are defined in the child MessageSource.
            argsToUse = resolveArguments(args, locale);

            MessageFormat messageFormat = resolveCode(code, locale);
            if (messageFormat != null) {
                synchronized (messageFormat) {
                    return messageFormat.format(argsToUse);
                }
            }
        }

        // Check locale-independent common messages for the given message code.
        Properties commonMessages = getCommonMessages();
        if (commonMessages != null) {
            String commonMessage = commonMessages.getProperty(code);
            if (commonMessage != null) {
                return formatMessage(commonMessage, args, locale);
            }
        }

        // Not found -> check parent, if any.
        return getMessageFromParent(code, argsToUse, locale);
    }

you can see that when the code fails deep into resolveCode(code, locale), the arguments argsToUse have not even been passed to it. So there's no way that by changing them for instance to a map, you can force a different behaviour from the MessageResource object.

I think you might want to use a custom message interpolator, the same one used by whatever library is implementing the javax.validation, which is patently able to resolve named parameters.

PJ_Finnegan
  • 1,981
  • 1
  • 20
  • 17