2016-05-15

Spring Boot internationalisation with database stored messages and IBM ICU

Motivation

In Java world there is a common approach to store localisation messages in property files. I want to have messages stored in database so users can manage them on runtime. I also want better plural forms handling provided by project ICU.

Introduction to Spring's localisation process

Following figure shows localisation processing in a Spring application.

  1. Requested locale (language and country codes) is passed to the Spring application as part of ServletRequest object. In ServletRequest there are several properties that can hold locale value. For example HTTP header Accept-Language, a cookie or query string parameter. Locale is resolved from the request object in DispatcherServlet.render method by instance of LocaleResolver class. There are several resolvers available in the Spring. You can check out org.springframework.web.servlet.i18n package for more details.
  2. Locale value is send by user's browser by default (as a Accept-Language header). To allow your application to change locale you have to configure LocaleChangeInterceptor. The interceptor reads locale value from query string and sets it to the request. Please read my older article Request mapping on demand in Spring MVC if you want to know more about request processing.
  3. Message code and resolved locale are passed to MessageSource via a view. Message source is responsible for loading message from storage, processing it and returning localised message back to the view. The view incorporates processed message into template.
  4. A message can be simple text or a pattern consisted of placeholders that will be replaced while pattern is processed. Placeholders are used to render text in a locale sensitive way. Patterns are processed by java.text.MessageFormat by default. In this article I will also describe enhanced version of message format (com.ibm.icu.text.MessageFormat) provided by ICU.

LocaleChangeInterceptor and LocaleResolver configuration

AcceptHeaderLocaleResolver is Spring Boot's default locale resolver. The resolver reads locale from Accept-Language header. Unfortunately it does not support locale change on runtime because it's setLocale method is not implemented.

No locale change interceptor is configured by default.

Locale resolver can be changed by creating LocaleResolver bean. I've decided to use CookieLocaleResolver which resolves locale stored in a cookie.

Locale change interceptor can be added by extending WebMvcConfigurerAdapter. Following code snippet shows configuration of locale change interceptor that reads new language code from query string. By adding lang parameter to a query string user can change requested locale.

Database aware MessageSource

To create message source you just need to implement MessageSource interface.

And then configure template engine so it will load messages from newly created message source. Thymeleaf template engine is used in this article.

Message patterns and plural forms handling

Plural forms handling is very common case in multilingual applications. Plural forms handling has to be robust because some languages have pretty complicated plural rules. Here is a example of three sentences that should be generated from a single message pattern:

  • There is 1 apple in 1 basket.
  • There are 2 apples in 2 baskets.
  • There are 0 apples in 2 baskets.
Standard MessageFormat

To deal with plurals Java offers MessageFormat formatter. This formatter can evaluate conditions in message pattern which can be used for plural forms handling. Let's revisit DatabaseMessageSource.resolveMessage method to incorporate the formatter into the application.

private String resolveMessage(String code, Object[] args, Locale locale) {
    String message = ""; // TODO Load message from database...
    MessageFormat messageFormat = new MessageFormat(message, locale)
    return messageFormat.format(args);
}

Then you need to create a message pattern with choice conditions. Curly braces are used as a placeholders for message parameters and can contain message formatting syntax.

There {0,choice,0#are|1#is|2#are} {0} {0,choice,0#apples|1#apple|2#apples} in {1} {1,choice,0#baskets|1#basket|2#baskets}.

To pass values into message you just need to add braces with values at the end of message code.

<p th:text="#{apple.message(1,1)}"></p>
<p th:text="#{apple.message(2,2)}"></p>
<p th:text="#{apple.message(0,2)}"></p>

Shown example is official Java recommended approach for plural forms handling. Unfortunately it's choice conditions are quite complicated even for English language which has only two plural forms. For languages like Polish it's almost unreadable.

IBM ICU MessageFormat

Project ICU focus on dealing with internationalisation. It offers its own implementation of MessageFormat that is more robust and easier to use. To incorporate this formatter to the application you need to slightly change DatabaseMessageSource.resolveMessage method.

private String resolveMessage(String code, Object[] args, Locale locale) {
    String message = ""; // TODO Load message from database...
    MessageFormat messageFormat = new MessageFormat(message, locale);
    StringBuffer formattedMessage = new StringBuffer();
    messageFormat.format(args, formattedMessage, null);
    return formattedMessage.toString();
}

For plural forms handling ICU formatter offers purpose built plural pattern argument.

There {0,plural,one{is # apple}other{are # apples}} in {1,plural,one{# basket}other{# baskets}}.

Localised URLs

In the Request Mapping on Demand article I've described possibilities of URL localisation on the fly.

Conclusion

There are several more internationalisation approaches available in Java world. For example there is possibility to use GNU gettext in Java. But it's not that easy to implement it in Spring. I've done some research and found out that combination of ICU and message codes seems to be the best choice.

2016-05-03

Request mapping on demand in Spring MVC

Motivation

I want to generate request mappings during execution time of my application. I want to have several URL paths that points to one controller method. Paths will be generated from e-shop product names. Each path should be bound to a language code send in a header. Of course paths can vary as user changes product names during an application's excution.

Here is an example list of mappings. Mapping for English language will match only if lang-header is set to en code. Same rule should apply for Czech lang-header.

[header: Accept-Language=en*]
/spinach
/carrot
/brocoli

[header: Accept-Language=cs*]
/spenat
/mrkev
/brokolice

These prerequisites disqualifies @RequestMapping annotation because:

  • Request mapping conditions are evaluated on application's init phase and cannot be changed on runtime.
  • It's not possible to attach a mapping path to a specific header value. There is many to many relation between paths and headers defined as a request mapping condition.
    @RequestMapping(value = {"/spinach", "/carrot", "/spenat", "/mrkev"}, headers = {"Accept-Language=en*", "Accept-Language=cs*"})
        

Theory of mapping a request to a controller method in Spring MVC

Following figure shows request's lifecycle in a Spring application.

  1. The first place where the request is processed by an application is DispatcherServlet. There is only one servlet of this kind which processes all incoming requests.
  2. Then DispatcherServlet in its doDispatch method tries to find handler capable of processing request. DispatcherServlet holds a list of HandlerMappings built on application start. These mappings tells the servlet which particular mapping is suitable for processing request. RequestMappingHandlerMapping is interesting in our case because it maps a controller method according to @RequestMapping annotation. If request matches conditions specified by @RequestMapping annotation then a request handler will be send back to the servlet. The handler is instance of HandlerMethod wrapped in HandlerExecutionChain object and actually it's kind of pointer to the controller method.
  3. Handler (HandlerMethod) is passed to the HandlerAdapter's specialization RequestMappingHandlerAdapter and then executed. Actually RequestMappingHandlerAdapter can be easily renamed as HandlerMethodHandlerAdapter because this adapter doesn't have almost no relation to @RequestMapping annotation. This will be useful in one of my solutions.
  4. The handler is executed in RequestMappingHandlerAdapter.invokeHandlerMethod method. Result is wrapped into ModelAndView object and then send back to the servlet.
  5. The servlet resolves a view, processes it, and so on.

Solution #1: Custom request condition

Spring allows to extend @RequestMapping with a custom condition. By overwriting RequestMappingHandlerMapping.getCustomMethodCondition or RequestMappingHandlerMapping.getCustomTypeCondition methods you can create your own condition that will (or will not) match requests. If request matches mapping's condition and your custom conditions then handler is returned by the handler mapping.

Custom conditions are created during application's runtime so it can also be changed on application's runtime.

To demonstrate this approach I've created a demo application Request Mapping On Demand (Custom Condition).

Pros:

  • Sort of Spring-way approach.
  • Less custom code.

Cons:

  • In Spring Boot it's not possible (in a sense of "clean way") to extend existing RequestMappingHandlerMapping. By adding new HandlerMapping to the dispatcher you can create pretty mess. Check the comment in CustomConditionRequestMappingHandlerMapping. Eventually this will be fixed soon.
  • You have to process raw HttpServletRequest object in your condition.

This solution was inspired by Rob Hinds's article Spring MVC & custom routing conditions.

Solution #2: Creating a new handler mapping

There is also demo application which demonstrates this approach: Request Mapping On Demand Demo.

In this solution the @RequestMapping annotation is replaced by custom @RequestMappingOnDemand. All conditions of this new annotation are created by application. So there's no conditions evaluated on application's init phase. Except for a pointer to a condition manager which creates these conditions.

The handler mapping RequestMappingHandlerMapping is replaced by RequestMappingOnDemandHandlerMapping. This custom handler mapping is capable of processing conditions attached to a controller method by @RequestMappingOnDemand annotation.

Because the custom handler mapping returns HandlerMethod there is no need to change anything in the adapter.

Pros:

  • More robust handling of requests.
  • Works flawlessly with current version of Spring Boot.

Cons:

  • More custom code.
  • Code duplication in RequestMappingOnDemandHandlerMapping and RequestMappingHandlerMapping.

Conclusion

I've used second solution in my project because of Spring Boot's inability to extend RequestMappingHandlerMapping. I will probably switch to first solution when it will be fixed.