3
votes

I'm writing a JSF application which needs to be internationalized. To do so, I created a MultilingualString :

public class MultilingualString {
    /* The Language class is basically a wrapper for a java.util.Locale */
    private Map<Language, String> strings;

    /* business methods, getters, setters */
}

Now, there are multiple forms which needs to fill a MultilingualString, and it's quite ugly to repeat a c:forEach loop each time I need to put such an object in a form. So I heard about JSF Composite Components, and I tried to write one for that purpose.

Here is my inputMultilingualString.xhtml :

<ui:component xmlns:h="http://xmlns.jcp.org/jsf/html"
              xmlns:composite="http://xmlns.jcp.org/jsf/composite"
              xmlns:f="http://xmlns.jcp.org/jsf/core"
              xmlns:ui="http://xmlns.jcp.org/jsf/facelets">
    <composite:interface componentType="inputMultilingualString">
        <composite:attribute name="value" required="true"
                             type="com.tob.entities.internationalization.MultilingualString"/>
        <composite:attribute name="languages" type="java.util.List" default="#{null}"/>
    </composite:interface>

    <composite:implementation>
        <f:event type="preRenderComponent" listener="#{cc.init}"/>
        <h:dataTable id="#{cc.clientId}" value="#{cc.languages}" var="language">
            <h:column>
                <h:outputLabel value="#{language}"/>
            </h:column>
            <h:column>
                <h:inputText binding="#{cc.inputs[language]}"/>
            </h:column>
        </h:dataTable>
    </composite:implementation>
</ui:component>

So I want to the value attribute to be an instance of MultilingualString and the languages attribute to be an instance of List of Language. If the languages attribute is null, I want te composite component to display a row in the dataTable for each entry in the map contained in the MultilingualString.

Now here is my "backing component" in InputMultilingualString.java :

@FacesComponent(value = "inputMultilingualString", createTag = true)
public class InputMultilingualString extends UIInput implements NamingContainer {

    private final Map<Language, UIInput> inputs = new HashMap();
    private List<Language> languages;

    @Override
    public String getFamily() {
        return (UINamingContainer.COMPONENT_FAMILY);
    }

    public void init() {
        List<Language> ls = (List<Language>) this.getAttributes().get("languages");
        MultilingualString ms = (MultilingualString) this.getValue();

        /* Setting languages */
        if (ls != null) {
            this.setLanguages(ls);
        } else {
            this.languages = new ArrayList();
            this.languages.addAll(ms.getStrings().keySet());
        }

        /* Initializing inputs */
        UIInput tmp;
        for (Language l : this.languages) {
            tmp = new UIInput();
            tmp.setValue(ms.getString(l));//
            this.inputs.put(l, tmp);
        }
    }

    @Override
    public String getSubmittedValue() {
        String ret = new String();

        for (Map.Entry<Language, UIInput> entry : this.inputs.entrySet()) {
            if (entry.getValue() != null) {
                if (!ret.isEmpty()) {
                    ret += ',';
                }
                ret += entry.getKey().getLanguageTag(); // NullPointerException here when the form is submitted
                ret += "=" + entry.getValue().getSubmittedValue();
            }
        }
        return (ret);
    }

    @Override
    protected Object getConvertedValue(FacesContext context, Object submittedValue) {
        MultilingualString ms = (MultilingualString) this.getValue();
        String[] entries = ((String) submittedValue).split(",");
        String[] pair;
        Language language;

        for (String entry : entries) {
            pair = entry.split("=");
            language = new Language();
            language.setLanguageTag(pair[0]);
            ms.addString(language, pair[1]);
        }
        return (ms);
    }

    public List<Language> getLanguages() {
        return (this.languages);
    }

    public void setLanguages(List<Language> languages) {
        this.languages = languages;
    }

    public Map<Language, UIInput> getInputs() {
        return (this.inputs);
    }
}

In order to implement the rule on which languages I want to display an input, I added a languages attribute to the backing component and initialized it in the init method which is called on the preRenderComponent event. The languages list is correctly initialized.

Here is how I use my composite component :

<ui:composition template="/Templates/Common.xhtml"
                xmlns="http://www.w3.org/1999/xhtml"
                xmlns:h="http://xmlns.jcp.org/jsf/html"
                xmlns:ui="http://xmlns.jcp.org/jsf/facelets"
                xmlns:tob="http://xmlns.jcp.org/jsf/composite/components"
                xmlns:p="http://primefaces.org/ui">
    <ui:define name="content">
        <h:form id="testForm">
            <tob:inputMultilingualString value="#{testBean.ms}" languages="#{testBean.languages}"/>
            <!-- The testBean.ms contains :
                     [English]  => string-English
                     [français] => string-français
                     [русский]  => string-русский

                 And the testBean.languages contains a list of Language objects for English, French, and Russian -->
            <p:commandButton value="Submit" action="#{testBean.submit()}"/>
        </h:form>
    </ui:define>
</ui:composition>

The problems are :

  • If the MultilingualString entered as value already contains some strings, they are not displayed in the inputText as it would be if you fill the inputText value attribute (I read the article of BalusC on the topic and he doesn't need to fill the value attribute to make his drop down lists have the correct value). I read on a BalusC's answer somewhere on stackoverflow that the UIInput referenced in the binding attribute of the inputText is created if it's evaluated to null, that's why I tried to initialized them in the init method, but so far no luck.
  • When I submit the form, I get an NullPointerException at the getKey() call in the getSubmittedValue() method. How is that possible ?

I hope that was clear and that someone can help me ! Thanks !

Edit : I'm using GlassFish 4 and I manually updated Mojarra to 2.2.6

1
The whole page shouldn't display at all but already fail during page load with an EL exception on binding. You namely can't use binding on a render time variable which is null during view build time. Did you really use <h:dataTable> in your code? Isn't that actually a <c:forEach>? I can point out the actual mistake as to the described failure during postback, but the given code already fails during display so I'm confused now.BalusC
@BalusC I copy-pasted the code I wrote in the question, and the page does display. So I understand it's not possible to use a <h:dataTable> here. But won't the problem be the same with <c:forEach> ? I will still not be able to initialize my Map<Language, UIInput>.Unda
What JSF impl/version? It fails on Mojarra 2.2.6. But anyway, the requirement is understood. This all is just possible without the need for a backing component, I'll bake an answer.BalusC
@BalusC Just this one : Mojarra 2.2.6. GlassFish starting output sample : Initialisation de Mojarra 2.2.6 ( 20140304-1537 https://svn.java.net/svn/mojarra~svn/tags/2.2.6@12949)Unda

1 Answers

4
votes

There are 2 technical problems in the code posted so far:

  1. You're using binding on a variable which is only available during view render time. The binding attribute runs during view build time, not during view render time. In this particular case, when the binding is executed, the #{language} is null. See also How does the 'binding' attribute work in JSF? When and how should it be used?. Also you seem to expect that multiple <h:inputText> components would be generated, but that's not true. There's only one which is reused multiple times during rendering the view. Only when you have used <c:forEach> instead of <h:dataTable>, then indeed physically multiple <h:inputText> components would be generated. See also JSTL in JSF2 Facelets... makes sense?

  2. You're not saving the state of the component for the postback. You should be removing the languages property and let the getter and setter delegate to getStateHelper(). See also How to save state when extending UIComponentBase.

However, the overall approach is clumsy. You don't need a backing component for the functional requirement. Just add a List<Languages> getter to the MultilingualString and use it directly as default of languages attribute.

So, if you add this to MultilingualString:

public List<Language> getLanguages() {
    return new ArrayList<>(strings.keySet());
}

And then just reference the attribtues via #{cc.attrs}:

<cc:interface>
    <cc:attribute name="value" required="true" type="com.tob.entities.internationalization.MultilingualString"/>
    <cc:attribute name="languages" type="java.util.List" default="#{cc.attrs.value.languages}" />
</cc:interface>

<cc:implementation>
    <h:dataTable value="#{cc.attrs.languages}" var="language">
        <h:column>
            <h:outputLabel value="#{language}"/>
        </h:column>
        <h:column>
            <h:inputText value="#{cc.attrs.value.strings[language]}" />
        </h:column>
    </h:dataTable>
</cc:implementation>

Then it should work as intented. Note the possibility to use the brace notation [] to reference a dynamic map key. That's perhaps the whole key to your solution (you seems to be not aware of that and hence working towards an overcomplicated solution).