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
binding
. You namely can't usebinding
on a render time variable which isnull
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<h:dataTable>
here. But won't the problem be the same with<c:forEach>
? I will still not be able to initialize myMap<Language, UIInput>
. – UndaInitialisation de Mojarra 2.2.6 ( 20140304-1537 https://svn.java.net/svn/mojarra~svn/tags/2.2.6@12949)
– Unda