<p:selectManyMenu id="colourList"
var="color"
value="#{testBean.selectedColours}"
converter="#{colourConverter}"
showCheckbox="true"
required="true"
label="Colour"
style="overflow: auto; width: 317px; background-color: white; max-height: 200px;">
<f:selectItems var="colour"
value="#{testBean.colours}"
itemLabel="#{colour.colourHex}"
itemValue="#{colour}"/>
<p:column>
<span style="display: inline-block; width: 275px; height: 20px; background-color:\##{color.colourHex}; border: 1px solid black;"
title="Name: #{color.colourName} | Hex: #{color.colourHex}" />
</p:column>
</p:selectManyMenu>
<p:commandButton value="Submit" actionListener="#{testBean.action}"/>
CSS is left intact, if someone may want to put the example into practice. It will display three basic colours (RGB) with check boxes in front of them as follows.
The managed bean :
@Named
@ViewScoped
public class TestBean implements Serializable {
@Inject
private DataStore dataStore;
private List<Colour> colours; //Getter & setter.
private List<Colour> selectedColours; //Getter & setter.
private static final long serialVersionUID = 1L;
public TestBean() {}
@PostConstruct
private void init() {
colours = dataStore.getColours();
}
public void action() {
for (Colour colour : selectedColours) {
System.out.println("colourName : "
+ colour.getColourName()
+ " : colourHex : "
+ colour.getColourHex());
}
}
}
If the converter="#{colourConverter}"
attribute is removed from <p:selectManyMenu>
, then it causes a java.lang.ClassCastException
to be thrown in the action()
method even though the converter is decorated with @FacesConverter(forClass = Colour.class)
.
java.lang.ClassCastException: java.lang.String cannot be cast to com.example.Colour
It appears that it is the generic type eraser problem (the generic type parameter of List<Colour>
is removed at run time so that it turns into an untyped List
).
Colour[]
should then work but the action()
method itself was not invoked, when attempted.
What is the exact reason why it requires an explicit mention of a converter?
Additional :
The converter :
@Named
@ApplicationScoped
@FacesConverter(forClass = Colour.class)
public class ColourConverter implements Converter {
@Inject
private DataStore dataStore;
@Override
public Object getAsObject(FacesContext context, UIComponent component, String value) {
if (value == null || value.isEmpty()) {
return null;
}
try {
long parsedValue = Long.parseLong(value);
if (parsedValue <= 0) {
throw new ConverterException("FacesMessage");
}
Colour entity = dataStore.findColourById(parsedValue);
if (entity == null) {
throw new ConverterException("FacesMessage");
}
return entity;
} catch (NumberFormatException e) {
throw new ConverterException("FacesMessage", e);
}
}
@Override
public String getAsString(FacesContext context, UIComponent component, Object value) {
if (value == null) {
return "";
}
if (!(value instanceof Colour)) {
throw new ConverterException("Message");
}
Long id = ((Colour) value).getColourId();
return id != null ? id.toString() : "";
}
}
The application scoped bean where the List<Colour>
is maintained.
@Named
@ApplicationScoped
public class DataStore {
private List<Colour> colours;
public DataStore() {}
@PostConstruct
private void init() {
colours = new ArrayList<>();
Colour colour = new Colour();
colour.setColourId(1L);
colour.setColourName("Red");
colour.setColourHex("FF0000");
colours.add(colour);
colour = new Colour();
colour.setColourId(3L);
colour.setColourName("Green");
colour.setColourHex("008000");
colours.add(colour);
colour = new Colour();
colour.setColourId(2L);
colour.setColourName("Blue");
colour.setColourHex("0000FF");
colours.add(colour);
}
public Colour findColourById(Long id) {
for (Colour colour : colours) {
if (colour.getColourId().equals(id)) {
return colour;
}
}
return null;
}
public List<Colour> getColours() {
return colours;
}
}
The domain model class :
public class Colour implements Serializable {
private Long colourId;
private String colourName;
private String colourHex;
private static final long serialVersionUID = 1L;
public Colour() {}
public Long getColourId() {
return colourId;
}
public void setColourId(Long colourId) {
this.colourId = colourId;
}
public String getColourName() {
return colourName;
}
public void setColourName(String colourName) {
this.colourName = colourName;
}
public String getColourHex() {
return colourHex;
}
public void setColourHex(String colourHex) {
this.colourHex = colourHex;
}
@Override
public int hashCode() {
int hash = 7;
hash = 47 * hash + Objects.hashCode(getColourId());
return hash;
}
@Override
public boolean equals(Object that) {
if (!(that instanceof Colour)) {
return false;
}
return this == that || Objects.equals(getColourId(), ((Colour) that).getColourId());
}
@Override
public String toString() {
return String.format("%s[colourId=%d]", getClass().getCanonicalName(), getColourId());
}
}
This problem is two-fold.
First problem is that EL can't determine the model value type, because the generic type information is lost during runtime. It basically becomes Object.class
. You basically need to replace List<Colour>
by Colour[]
. This is in detail answered in this question: UISelectMany on a List<T> causes java.lang.ClassCastException: java.lang.String cannot be cast to T.
Second problem is that PrimeFaces InputRenderer
has the bug that it doesn't take into account with array types before locating the converter. Line numbers below match 5.2
156 protected Converter findImplicitConverter(FacesContext context, UIComponent component) {
157 ValueExpression ve = component.getValueExpression("value");
158
159 if(ve != null) {
160 Class<?> valueType = ve.getType(context.getELContext());
161
162 if(valueType != null)
163 return context.getApplication().createConverter(valueType);
164 }
165
166 return null;
167 }
In your specific case, the valueType
is Colour[].class
instead of Colour.class
. This explians why it couldn't locate the converter associated with Colour.class
. Before creating the converter, it should have checked if the valueType
is an array type and if so then extract the component type from it.
if (valueType.isArray()) {
valueType = valueType.getComponentType();
}
You'd best report this as a bug to PrimeFaces guys along with the fact that it works fine in standard components like <h:selectManyMenu>
. In the meanwhile, your best bet is to just explicitly register the converter.