jsfjsf-2enumsselectmanycheckbox

Use enum in h:selectManyCheckbox


I want to use enum values in a <h:selectManyCheckbox>. The checkboxes get populated correctly, however, when selecting some values and submitting them, their runtime type is String, and not enum. My code:

<h:selectManyCheckbox value="#{userController.roles}" layout="pageDirection">
     <f:selectItems value="#{userController.rolesSelectMany}" />
</h:selectManyCheckbox>

UserController class (SecurityRole is an enum type):

public SelectItem[] getRolesSelectMany() {
    SelectItem[] items = new SelectItem[SecurityRole.values().length];

    int i = 0;
    for (SecurityRole role : SecurityRole.values()) {
        items[i++] = new SelectItem(role, role.toString());
    }
    return items;
}     

public List<SecurityRole> getRoles() {
     getCurrent().getRoles();
}

public void setRoles(List<SecurityRole> roles) {
     getCurrent().setRoles(roles);
}

When JSF calls the setRoles method, it contains a list of type String, and not the enum type. Any ideas? Thanks!


Solution

  • This problem is not specifically related to enums. You would have the same problem with other List types for which JSF has builtin converters, e.g. List<Integer>, List<Double>, etcetera.

    The problem is that EL operates runtime and that generic type information is lost during runtime. So in essence, JSF/EL doesn't know anything about the parameterized type of the List and defaults to String unless otherwise specified by an explicit Converter. In theory, it would have been possible using nasty reflection hacks with help of ParameterizedType#getActualTypeArguments(), but the JSF/EL developers may have their reasons for not doing this.

    You really need to explicitly define a converter for this. Since JSF already ships with a builtin EnumConverter (which isn't useable standalone in this particular case because you have to specify the enum type during runtime), you could just extend it as follows:

    package com.example;
    
    import javax.faces.convert.EnumConverter;
    import javax.faces.convert.FacesConverter;
    
    @FacesConverter(value="securityRoleConverter")
    public class SecurityRoleConverter extends EnumConverter {
    
        public SecurityRoleConverter() {
            super(SecurityRole.class);
        }
    
    }
    

    And use it as follows:

    <h:selectManyCheckbox value="#{userController.roles}" converter="securityRoleConverter">
        <f:selectItems value="#{userController.rolesSelectMany}" />
    </h:selectManyCheckbox>
    

    or

    <h:selectManyCheckbox value="#{userController.roles}">
        <f:converter converterId="securityRoleConverter" />
        <f:selectItems value="#{userController.rolesSelectMany}" />
    </h:selectManyCheckbox>
    

    A bit more generic (and hacky) solution would be to storing the enum type as component attribute.

    package com.example;
    
    import javax.faces.application.FacesMessage;
    import javax.faces.component.UIComponent;
    import javax.faces.context.FacesContext;
    import javax.faces.convert.Converter;
    import javax.faces.convert.ConverterException;
    import javax.faces.convert.FacesConverter;
    
    @FacesConverter(value="genericEnumConverter")
    public class GenericEnumConverter implements Converter {
    
        private static final String ATTRIBUTE_ENUM_TYPE = "GenericEnumConverter.enumType";
    
        @Override
        public String getAsString(FacesContext context, UIComponent component, Object value) {
            if (value instanceof Enum) {
                component.getAttributes().put(ATTRIBUTE_ENUM_TYPE, value.getClass());
                return ((Enum<?>) value).name();
            } else {
                throw new ConverterException(new FacesMessage("Value is not an enum: " + value.getClass()));
            }
        }
    
        @Override
        @SuppressWarnings({"rawtypes", "unchecked"})
        public Object getAsObject(FacesContext context, UIComponent component, String value) {
            Class<Enum> enumType = (Class<Enum>) component.getAttributes().get(ATTRIBUTE_ENUM_TYPE);
            try {
                return Enum.valueOf(enumType, value);
            } catch (IllegalArgumentException e) {
                throw new ConverterException(new FacesMessage("Value is not an enum of type: " + enumType));
            }
        }
    
    }
    

    It's useable on all kinds of List<Enum> using converter ID genericEnumConverter. For List<Double>, List<Integer>, etc one would have used the builtin converters javax.faces.Double, javax.faces.Integer and so on. The builtin Enum converter is by the way unsuitable due to the inability to specify the target enum type (a Class<Enum>) from the view side on. The JSF utility library OmniFaces offers exactly this converter out the box.

    Note that for a normal Enum property, the builtin EnumConverter already suffices. JSF will instantiate it automagically with the right target enum type.