Spring as a Message Provider for Wicket

Hi, this one is about Wicket integration with Spring. Wicket has a built-in integration with Spring using the wicket-spring module, but this module doesn't (or at least I didn't find any) support the usage of Spring's Application context as a Wicket message provider (as we know Spring application context is a org.springframework.context.MessageSource). Probably some, if not the most, of you think "what do I need this for?" well…here is a real life situation I've encountered:
An existing application uses EJB3 (both entities and session beans), a Swing client, and it heavily usages Spring. The Spring's ApplicationContext is also the messages provider for the application, using the standard ResourceBundleMessageSource, and it aggregates few property files:

         
              
         module-a
         module-b
         module-c
         module-d
         module-e
      
   


And the requirement is: "we would like to add a Web interface to the application, and we choose Wicket. How can we reuse our resource bundles?", the most important nuance of the requirement is about labels. The application generates batch reports and the development group would like to have the same labels both in the web interface and the generated reports.

How does Wicket Resolve Resource Bundles

Wicket has a unique way of resource bundles resolving (implemented by org.apache.wicket.resource.loader.ComponentStringResourceLoader), the principle is as follows: assuming that we have the following components structure:
 
PageA extends BasePage (wicket:id="myPage")
   |
   + -- contains a -- Form extends MyBaseForm (wicket:id="myForm")
                                |
                                |
                                + -- contains a –- Label (key="myLabel")
 
When Wicket needs to resolve the label key (for example if we were using :message
 key="myLabel"/> ) it looks for the resource in the following search path
·        First it looks in the page (PageA.properties) then it goes up the classes hierarchy to BasePage (BasePage.properties), if it fails
·        Wicket will go one level down the components tree and looks in Form.properties and MyBaseForm.properties.
·        Not found yet? No problem Wicket will now look in our application class and its parent. (so if our application class was MyApplication.class it would looked in MyApplication.properties and Application.properties)
In each of the properties files Wicket will try to look for two keys – the key we were looking for and a path from the current searched component to the actual key we are looking for. Confused? Here it is:
 
PageA.properties ===> myForm.myLabel
PageA.properties ===> myLabel
    [Move up the class hierarchy] 
BasePage.properties ===> myForm.myLabel
BasePage.properties ===> myLabel
 
 
    [Move down the components hierarchy]
From.properies ===> myLabel
MyBaseForm.properties ===> myLabel
 
    
    [Look in the application class (assuming MyApplication is the class name of our application)]
MyApplication.properties ===> myPage.myForm.myLabel
MyApplication.properties ===> myLabel
    [Move up the class hierarchy]
Application.properties ===> myPage.myForm.myLabel
Application.properties ===> myLabel
 
It looks complicated but the Wicket documentation gives a good explanation for that: let's say that team A has developed the 'Form' component and now Team B uses it. Using this search path team A can provide a default value for the label, and still anyone (team B) that uses the component within his component can override the default provided by team A. This overridden feature is supported up to the top component. A global default can be provided by using the application's properties file. If Wicket fails to find the key translation in any of the properties files it returns the key itself.
Spring Message Source
We started with a request to integrate Spring resources into Wicket, there are several ways to do it. For example during the build process we could merge all of Spring's property files into one file MyApplication.properties and let Wicket fallback into this properties file. But I've chosen a different way: The Application object holds a reference to an instance of IResourceSettings (org.apache.wicket.settings.IResourceSettings). The resource setting manages all resource related behaviors and among these the one that interests us is the list of string resource loaders. The string resource loaders is a chain of IStringResourceLoader (org.apache.wicket.resource.loader.IStringResourceLoader), by default this chain is composed of two string resource loaders:
·        An instance of ComponentStringResourceLoader
·        And an instance of ClassStringResourceLoader (this is the one responsible for appending the Application properties file at the end of the search path from the previous section)
I added an additional resource loader - eyal.lupu.blog.resource.loader.SpringStringResourceLoader – to the end of the chain. When the two default resource loaders fails to resolve a key Wicket will delegate the task to this loader which accesses a Spring MessageSource to resolve the key. Below is the code of SpringStringResourceLoader
 
package eyal.lupu.blog.resource.loader;
 
import java.util.Locale;
 
import org.apache.wicket.Component;
import org.apache.wicket.resource.loader.IStringResourceLoader;
import org.apache.wicket.spring.ISpringContextLocator;
import org.springframework.context.MessageSource;
import org.springframework.context.NoSuchMessageException;
 
public class SpringStringResourceLoader implements IStringResourceLoader {
 
        private MessageSource messageSource;
 
        public SpringStringResourceLoader(ISpringContextLocator contextLocator) {
               messageSource = contextLocator.getSpringContext();
        }
 
        public String loadStringResource(Component component, String key) {
 
               if (component == null) {
                       return null;
               }
 
               Locale l = component.getLocale();
               return loadStringResource(component.getClass(), key, l, null);
        }
 
        public String loadStringResource(Class clazz, String key, Locale locale,
                       String style) {
 
               try {
                       //Actual work is done here
                       return messageSource.getMessage(key, new Object[0], locale);
               } catch (NoSuchMessageException e) {
                       return null;
               }
        }
 
}
 
Installing the Spring resource loader is done as part of the application's init:
 
 
public class MyApplication extends SpringWebApplication { //I use the wicket-spring extension 
 
 
    protected void init() {
        super.init();
 
        IResourceSettings resourceSettings = getResourceSettings();
        Collection stringLoaders = new ArrayList>(
resourceSettings.getStringResourceLoaders());
        for (IStringResourceLoader loader : stringLoaders) {
               resourceSettings.addStringResourceLoader(loader);
        }
        resourceSettings.addStringResourceLoader(new SpringStringResourceLoader(getSpringContextLocator()));
               
    }
}
 
Yes the code above looks little bit strange, why just invoking resourceSettings.addStringResourceLoader(..) isn't enough? The problem is with the way the addStringResourceLoader method is defined:
·        If this is the first time it is called clear the current list of string resource loaders
·        Add the new resource loader to the list
So, since this is the first time this method is invoked, I have to create a local copy of the string resource loaders list and reconstruct it. (Notice the way I do it in here is only for demonstration, but putting this code it in the init() method of your application it's reasonably safe).



Please notice: I had recover this blog post from my old blog at http://www.jroller.com/eyallupu since jroller.com is no longer available. As such the styling might be a bit wobbly ... If something seems 'too broken' please contact me and I'll adjust

Comments

Popular posts from this blog

New in Spring MVC 3.1: CSRF Protection using RequestDataValueProcessor

Hibernate Exception - Simultaneously Fetch Multiple Bags

Hibernate/JPA Identity Generators