JSF and type safety

后端 未结 1 1047
南方客
南方客 2020-12-06 13:31

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

相关标签:
1条回答
  • 2020-12-06 14:28

    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:

    • JSF 2.0 use enum in selectMany menu;
    • How to make a dropdown menu of a enum in JSF;
    • How to create and use a generic bean for enums in f:selectItems?.

    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();
        }
    
    }
    
    0 讨论(0)
提交回复
热议问题