3
votes

This is a subject that has been asked in many forums but I can't find any precise and concrete answer for it. Even the accepted answers are incomplete in my point of view, so I will try to post my complete attempt to solve this problem in hope to build a precise question + answer on the subject.

I'm trying to get Resource Bundles working in JSF. The Resource Bundles come from a Spring bean, which should be loaded from an arbitrary external system (ie a Database).

I will bypass the Database querying for now and use a mocked up Resource Bundle to keep things clear.

This is my Resource Bundle business implementation, which I managed to gather from other post on this forum:

public class TesteResBundle extends ReloadableResourceBundleMessageSource {

    private final Map<String, Map<String, String>> properties = new HashMap<String, Map<String, String>>();

    public TesteResBundle() {
        reload();
    }

    @Override
    protected MessageFormat resolveCode(String code, Locale locale) {
        String msg = getText(code, locale);
        MessageFormat result = createMessageFormat(msg, locale);
        return result;
    }

    @Override
    protected String resolveCodeWithoutArguments(String code, Locale locale) {
        return getText(code, locale);
    }

    private String getText(String code, Locale locale) {
        Map<String, String> localized = properties.get(code);
        String textForCurrentLanguage = null;
        if (localized != null) {
            textForCurrentLanguage = localized.get(locale.getLanguage());
            if (textForCurrentLanguage == null) {
                textForCurrentLanguage = localized.get(Locale.ENGLISH.getLanguage());
            }
        }
        return textForCurrentLanguage != null ? textForCurrentLanguage : code;
    }

    public void reload() {
        properties.clear();
        properties.putAll(loadTexts());
    }

    protected Map<String, Map<String, String>> loadTexts() {

        Map<String, Map<String, String>> m = new HashMap<String, Map<String, String>>();
        Map<String, String> v = new HashMap<String, String>();
        v.put("en", "good");
        v.put("pt", "bom");
        v.put("en_US", "bom");
        m.put("prop", v);

        v = new HashMap<String, String>();
        v.put("en", "bad");
        v.put("pt", "mau");
        v.put("en_US", "bom");
        m.put("pror", v);
        return m;
    }
}

This is a custom EL resolver that I also grabbed around in a forum. It tries to gather the message if the base is an instance of MessageSource. If not it passes the resolution to the default Spring EL resolver:

public class MessageSourcePropertyResolver extends SpringBeanFacesELResolver {

    public Object getValue(ELContext elContext, Object base, Object property)
        throws ELException {

        if (base instanceof MessageSource && property instanceof String) {
            String result = ((MessageSource) base).getMessage(
                (String) property, null, getLocale());

            if (null != result) {
                elContext.setPropertyResolved(true);
            }

            return result;
        }

        return super.getValue(elContext, base, property);
    }

     private Locale getLocale() {
        FacesContext context = FacesContext.getCurrentInstance();
        return context.getExternalContext().getRequestLocale();
     }

}

The custom EL resolver is defined in faces-config.xml:

<el-resolver>pt.teste.pojo.MessageSourcePropertyResolver</el-resolver>

Finally in Spring configuration I have the messageSource bean defined as:

<bean id="messageSource" class="pt.teste.pojo.TesteResBundle">
</bean>

I can confirm that the messageSource bean is instantiated correctly and the HashMap is correctly loaded upon the application startup. I can confirm that the custom handler is being called and is passing to the default Spring resolver all the EL's that are not Resource Messages and is resolving correctly.

When I use a Resource Bundle in a xhtml JSF 2.0 page I'm doing it this way:

<h:outputText value="#{messageSource.prop}" />

During EL resolving, the custom resolver correctly detects the base as a MessageSource instance but fails at:

String result = ((MessageSource) base).getMessage((String) property, null, getLocale());

With the following exception:

org.springframework.context.NoSuchMessageException: No message found under code 'prop' for locale 'en_US'.
org.springframework.context.support.DelegatingMessageSource.getMessage(DelegatingMessageSource.java:65)
pt.teste.pojo.MessageSourcePropertyResolver.getValue(MessageSourcePropertyResolver.java:18)
com.sun.faces.el.DemuxCompositeELResolver._getValue(DemuxCompositeELResolver.java:176)
com.sun.faces.el.DemuxCompositeELResolver.getValue(DemuxCompositeELResolver.java:203)
org.apache.el.parser.AstValue.getValue(AstValue.java:169)
org.apache.el.ValueExpressionImpl.getValue(ValueExpressionImpl.java:189)
com.sun.faces.facelets.el.TagValueExpression.getValue(TagValueExpression.java:109)
javax.faces.component.ComponentStateHelper.eval(ComponentStateHelper.java:194)
javax.faces.component.ComponentStateHelper.eval(ComponentStateHelper.java:182)
javax.faces.component.UIOutput.getValue(UIOutput.java:169)
com.sun.faces.renderkit.html_basic.HtmlBasicInputRenderer.getValue(HtmlBasicInputRenderer.java:205)
com.sun.faces.renderkit.html_basic.HtmlBasicRenderer.getCurrentValue(HtmlBasicRenderer.java:355)
com.sun.faces.renderkit.html_basic.HtmlBasicRenderer.encodeEnd(HtmlBasicRenderer.java:164)
javax.faces.component.UIComponentBase.encodeEnd(UIComponentBase.java:875)
com.sun.faces.renderkit.html_basic.HtmlBasicRenderer.encodeRecursive(HtmlBasicRenderer.java:312)
com.sun.faces.renderkit.html_basic.GroupRenderer.encodeChildren(GroupRenderer.java:105)
javax.faces.component.UIComponentBase.encodeChildren(UIComponentBase.java:845)
javax.faces.component.UIComponent.encodeAll(UIComponent.java:1779)
com.sun.faces.renderkit.html_basic.CompositeRenderer.encodeChildren(CompositeRenderer.java:78)
javax.faces.component.UIComponentBase.encodeChildren(UIComponentBase.java:845)
javax.faces.component.UIComponent.encodeAll(UIComponent.java:1779)
javax.faces.render.Renderer.encodeChildren(Renderer.java:168)
javax.faces.component.UIComponentBase.encodeChildren(UIComponentBase.java:845)
javax.faces.component.UIComponent.encodeAll(UIComponent.java:1779)
javax.faces.component.UIComponent.encodeAll(UIComponent.java:1782)
com.sun.faces.application.view.FaceletViewHandlingStrategy.renderView(FaceletViewHandlingStrategy.java:402)
com.sun.faces.application.view.MultiViewHandler.renderView(MultiViewHandler.java:125)
org.springframework.faces.webflow.FlowViewHandler.renderView(FlowViewHandler.java:99)
com.sun.faces.lifecycle.RenderResponsePhase.execute(RenderResponsePhase.java:121)
com.sun.faces.lifecycle.Phase.doPhase(Phase.java:101)
com.sun.faces.lifecycle.LifecycleImpl.render(LifecycleImpl.java:139)
org.springframework.faces.mvc.JsfView.renderMergedOutputModel(JsfView.java:85)
org.springframework.web.servlet.view.AbstractView.render(AbstractView.java:262)
org.springframework.web.servlet.DispatcherServlet.render(DispatcherServlet.java:1180)
org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:950)
org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:852)
org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:882)
org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:778)
javax.servlet.http.HttpServlet.service(HttpServlet.java:734)
javax.servlet.http.HttpServlet.service(HttpServlet.java:847)

I think that I may be missing something here, specifically in the messageSource bean definition in Spring config. I suspect of this because no method of TesteResBundle gets called when resolving the Resource Bundle.

Thank you for any help provided on this subject.

1

1 Answers

1
votes

I actually managed to craft a workaround for the problem. Since I'm doing my first baby-steps in Spring it would be nice if a Spring expert could review this approach as I don't think it's done in a "Spring way". But if everything else is failing I will stick with this not-so-beautiful workaround.

I usually have a singleton in my web applications that holds configuration artifacts. Right now it's actually just holding a reference to the Spring application context:

public class ApplicationConfig {

    private static ApplicationConfig instance = new ApplicationConfig();

    private ApplicationContext context = new ClassPathXmlApplicationContext("spring.xml");

    private ApplicationConfig(){

    }

    public static ApplicationConfig instance(){
        return instance;
    }

    public ApplicationContext getApplicationContext(){
        return context;
    }
}

I discarded the previous Resource Bundle implementation and instead extended AbstractMessageSource:

public class TesteMessageSource extends AbstractMessageSource {

    @Override
    protected MessageFormat resolveCode(String key, Locale locale) {
        // This is just a dummy method.
        // It should lookup in a Map created in this same 
        // class for the correct key/locale resource value
        return createMessageFormat(key, Locale.US);
    }

    @Override
    protected String resolveCodeWithoutArguments(String key, Locale locale){
        // This is just a dummy method.
        // It should lookup in a Map created in this same 
        // class for the correct key/locale resource value
        return "dummyString";
    }
}

Then I noticed that the base parameter passed to the custom EL resolver is in fact an instance of DelegatingMessageSource. According to the Spring documentation: "Empty MessageSource that delegates all calls to the parent MessageSource". So I modified the custom EL resolver to get the Spring application context from the singleton, then get the messageResource bean and set it as the parent MessageSource to the DelegatingMessageSource instance:

public class MessageSourcePropertyResolver extends SpringBeanFacesELResolver /*implements MessageSourceAware */{

    public Object getValue(ELContext elContext, Object base, Object property)
        throws ELException {

        if (base instanceof MessageSource && property instanceof String) {
            DelegatingMessageSource delegatingMessageSource = (DelegatingMessageSource) base;
            BeanFactory factory = ApplicationConfig.instance().getApplicationContext();
            MessageSource messageSource = (MessageSource) factory.getBean("messageSource");
            delegatingMessageSource.setParentMessageSource(messageSource);

            String result = delegatingMessageSource.getMessage((String) property, new Object[] {}, getLocale());
            if (result != null) {
                elContext.setPropertyResolved(true);
            }

            return result;
        }

        return super.getValue(elContext, base, property);
    }

    private Locale getLocale() {
        FacesContext context = FacesContext.getCurrentInstance();
        return context.getExternalContext().getRequestLocale();
    }
}

Spring messageSource bean configuration became:

<bean id="messageSource" class="pt.teste.pojo.TesteMessageSource">
</bean>

Message resource access in Facelets xtml component became:

<h:outputText value="${messageSource['prop.aaa']}" />

Where prop.aaa is passed as the "property" parameter to the custom EL resolver and we just have to lookup for that "property" and the Locale that is also passed to the resolver.

This way everything is working, but I'm almost certain that this can be done in a better and correct way, like passing an already configured MessageSource to the custom EL resolver. With this I mean passing an instance of the custom MessageSource, or at least a default MessageSource having the parent already set as the custom MessageSource.