Search code examples
jsfprimefacesconvertersselectmanymenu

p:selectManyMenu and @FacesConverter(forClass = Clazz.class)


<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.

enter image description here

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());
    }
}

Solution

  • 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.