JSF and type safety
Asked Answered
G

1

5

As I struggled for hours I finally found where those annoying ClassCastExceptions came from, which I thought were produced by Hibernate and it's enum-mapping.

But they came from my JSF view, where I passed a List from

    <h:selectManyCheckbox value="#{createUserManager.user.roles}"  ... >
        <f:selectItems value="#{createUserManager.roles}"/>
    </h:selectManyCheckbox>

back into my backing bean.
My data simply consists of the values of an enum: public Role[] getRoles() { return Role.values(); } .
I was really shocked when I tested the setter of roles in the User-class and got this:

public void setRoles(List<Role> paramRoles) {

    System.out.println(paramRoles.get(0) instanceof Role); //output: false

    for(Role role : paramRoles){ ...} //crashes with ClassCastException
}

Changing List<Role> paramRoles to List<String> paramRoles worked perfectly.
How is this possible? Shouldn't those generics be type safe or is type erasure in connection with JSF killing the whole type safety thing?
Also shouldn't the return value of h:selectManyCheckbox be List<Role>, like I passed in via the f:selectItems?

Gerald answered 8/5, 2013 at 5:25 Comment(1)
I don't know JSF so I can't speculate too well, but JSF support for generics looks pretty bleak: #15242715Glennglenna
I
7

The behaviour you are experiencing is fully expected. Moreover, it is related to java generics in the same way as to how HTTP works.

The problem

  1. The HTTP part

    The problem is that you don't fully understand how HTTP works. When you submit data by pressing the submit button, parameters of your generated by JSF <h:selectManyCheckbox> tag, as a bunch of <input type="checkbox" name="..." value="userRoleAsString"> checkboxes, will be sent as strings and retrived ultimately as request.getParameter("checkboxName"); also as strings. Of course, JSF has no idea how to construct you model object class, Role.

  2. The generics part

    As you know due to the fact that java chose type erasure for generics to provide for backwards compatibility, information about generic types is basically a compile-type artifact and is lost at runtime. So at runtime your List<Role> erases to a plain, good old List. And as far as EL is a runtime language that uses Java Reflection API to deal with your expressions / call methods, at runtime no such information is available. Taking into account the HTTP part, JSF does its best and assigns string objects to your list, as it's all it can implicitly do. If you are willing to tell JSF to do otherwise, you need to do that explicitly, i.e. by specifying a converter to know what type of object to expect in an HTTP request.

  3. The JSF part: aftermath

    JSF has a provided javax.faces.Enum converter and in indeed works, if EL knew of the compile-time generic type of your list, that is Role. But it doesn't know of it. It would be not necessary to provide for a converter in case your multiple selection would be done on a Role[] userRoles object, or if you used the unique selection like in <h:selectOneMenu> with a value bound to Role userRole. In these examples the built-in enum converter will be called automatically.

    So, to get it work as expected you need to provide for a Converter that will 'explain' JSF what type of values does this list hold, and how to do the transformations from Role to String, and vice versa.

    It is worth noting that this will be the case with any bound List<...> values within the multiple choice JSF components.


Points of reference on Stack Overflow

After the problem was examined and resolved I was wondering if no one faced it in the past and searched for some previous answers here. Not surprisingly, it was asked before, and of course the problem was solved by BalusC. Below are two most valuable point of reference:


The test case and two examples of working converters

Below I provide for a test case entirely for your understanding: everything works as expected apart from the third <h:selectManyCheckbox> component. It's up to you to trace it fully to eliminate the issue altogether.

The view:

<h:form>
    Many with enum converter
    <!-- will be mapped correctly with Role object -->
    <h:selectManyCheckbox value="#{q16433250Bean.userRoles}" converter="roleEnumConverter">
        <f:selectItems value="#{q16433250Bean.allRoles}" var="role" itemLabel="#{role.name}" />
    </h:selectManyCheckbox>
    <br/>
    Many with plain converter
    <!-- will be mapped correctly with Role object -->
    <h:selectManyCheckbox value="#{q16433250Bean.userRoles2}" converter="roleConverter">
        <f:selectItems value="#{q16433250Bean.allRoles2}" var="role" itemLabel="#{role.name}" />
    </h:selectManyCheckbox>
    <br/>
    Without any converter
    <!-- will NOT be mapped correctly with Role object, but with a default String instead -->
    <h:selectManyCheckbox value="#{q16433250Bean.userRoles3}">
        <f:selectItems value="#{q16433250Bean.allRoles}" var="role" itemLabel="#{role.name}" />
    </h:selectManyCheckbox>
    <br/>
    Without any converter + array
    <!-- will be mapped correctly with Role object -->
    <h:selectManyCheckbox value="#{q16433250Bean.userRoles4}">
        <f:selectItems value="#{q16433250Bean.allRoles}" var="role" itemLabel="#{role.name}" />
    </h:selectManyCheckbox>
    <br/>
    <h:commandButton value="Submit" action="#{q16433250Bean.action}"/>
</h:form>

The bean:

@ManagedBean
@RequestScoped
public class Q16433250Bean {

    private List<Role> userRoles = new ArrayList<Role>();//getter + setter
    private List<Role> userRoles2 = new ArrayList<Role>();//getter + setter
    private List<Role> userRoles3 = new ArrayList<Role>();//getter + setter
    private Role[] userRoles4;//getter + setter

    public enum Role {

        ADMIN("Admin"),
        SUPER_USER("Super user"),
        USER("User");
        private final String name;

        private Role(String name) {
            this.name = name;
        }

        public String getName() {
            return this.name;
        }
    }

    public Role[] getAllRoles() {
        return Role.values();
    }

    public String action() {
        return null;
    }

}

The converters:

@FacesConverter("roleEnumConverter")
public class RoleEnumConverter extends EnumConverter {

    public RoleEnumConverter() {
        super(Role.class);
    }

}

and

@FacesConverter("roleConverter")
public class RoleConverter implements Converter {

    public Object getAsObject(FacesContext context, UIComponent component, String value) {
        if(value == null || value.equals("")) {
            return null;
        }
        Role role = Role.valueOf(value);
        return role;
    }

    public String getAsString(FacesContext context, UIComponent component, Object value) {
        if (!(value instanceof Role) || (value == null)) {
            return null;
        }
        return ((Role)value).toString();
    }

}
Iams answered 8/5, 2013 at 6:8 Comment(4)
Shouldn't the standard enum converter do that in JSF 2.0 without explicitly setting it?Gerald
No, it shouldn't in this particular case. See the extended answer above.Iams
Well I know how converters work in general(and how to write them as I already did), but I was mistaken by thinking the standard enum converter would do that for me without any extra work. However this answer is BalusC-like qualitity, thank you :)Gerald
You don't need a converter for Java Enum.Sidwell

© 2022 - 2024 — McMap. All rights reserved.