How to resolve a message with named parameters from Spring's MessageSource

2.5k Views Asked by At

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

3

There are 3 best solutions below

0
dsp_user On

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.

5
hicnar On

@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.

0
PJ_Finnegan On

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.