Use enum in h:selectManyCheckbox
Asked Answered
D

2

20

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!

Drambuie answered 29/9, 2010 at 13:30 Comment(0)
M
37

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.

Menken answered 29/9, 2010 at 16:36 Comment(4)
I wouldn't say that's a nasty reflection hack. frameworks do that, more type info, more happiness. JSF guys are probably overwhelmed by the complexity of their creation, they don't have time for this.Easement
@BalusC, thank you for yet another informative and helpful answer. If I may be so bold, may I ask why you chose to extend EnumConverter instead of delegating to an instance of it?Lugworm
@Nick: in order to set the desired enum class via super.Menken
With OmniFaces 3.0 the GenericEnumConverter has gone, as this has been solved in JSF 2.3. See github.com/omnifaces/omnifaces/issues/286 for details.Goodrich
F
1

In some cases the List could just as well be an array SomeType[], and in this case no explicit converter is needed.

Generic erasure was a clever way of putting generics into the language without breaking the old stuff, but now we live forever with the consequences of that decision...

Faunia answered 13/8, 2012 at 18:15 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.