2
<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());
    }
}
Tiny
  • 27,221
  • 105
  • 339
  • 599
  • Is this acceptable as dupe? http://stackoverflow.com/questions/19872633/pselectcheckboxmenu-on-a-listlong-causes-java-lang-classcastexception-java/ – BalusC Aug 06 '15 at 11:57
  • Yes. Except using `Colour[]` did not work for me. The `action()` method itself was not invoked. – Tiny Aug 06 '15 at 12:03
  • So a conversion/validation error occurred? Note that `@Named` and `@FacesConverter` are mutually exclusive (i.e. 2 separate instances are created, one managed by CDI and other managed by JSF). Unless you're using OmniFaces, `@Inject` won't work in the JSF-managed `@FacesConverter` instance. – BalusC Aug 06 '15 at 12:17
  • `@Inject` works thanks to OmniFaces on the class-path. I removed the CDI designation from the converter. Upon submit, it passes through the converter. There is no conversion error (`required="true"` was also dropped) but it does not reach the `action()` method, when `List` is turned into `Colour[]`. – Tiny Aug 06 '15 at 13:02
  • The `getAsObject()` method receives a String value like `com.example.Colour[colourId=3]`, when `Colour[]` is used in the bean. It should instead receive `3`. Hence, it causes a `java.lang.NumberFormatException`. Since this exception was being caught and I removed `FacesMessage`s as well, It was not directly visible from the browser. Why does it do `Colour#toString()` directly, when `Colour[]` is used? – Tiny Aug 06 '15 at 13:34
  • This is not the expected behavior. I'd have to take a look myself. – BalusC Aug 06 '15 at 13:35
  • Indeed, the `` doesn't locate/invoke the converter by class during rendering. The `` however does. Must be a bug in its renderer. – BalusC Aug 06 '15 at 13:41
  • 1
    Who is the daemon/demon downvoter? First time in more than three years, saying something about a negative vote. Many amateur and immature people (even though they achieved a university level) everyday cast fraud votes to their own posts using clone accounts and/or on behalf of their colleagues. Go cast your negative votes to such poor quality posts as many as you can. Please don't disturb me without any sufficient cause. I am already disturbed enough. – Tiny Aug 06 '15 at 14:55

1 Answers1

4

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.

Community
  • 1
  • 1
BalusC
  • 1,082,665
  • 372
  • 3,610
  • 3,555