Search code examples
jsfprimefacesmojarra

How to use SelectManyCheckbox with ArrayList as HashMap value


Im trying to bind selected values of SelectManyCheckbox to ArrayList, which is the value of my HashMap.

If my options are inside an ArrayList and selected options are aswell. i have no issue.

BUT when i want to set these selected options as a value of a map. it will populate an array of Object[] no matter what i try.

im using Mojarra 4.0.5.

MyBean

@Component
@ViewScoped
@Getter
@Setter
public class TestView {
    Map<String, ArrayList<Dto>> map;
    ArrayList<String> strings;
    ArrayList<Dto> options;
    @PostConstruct
    public void init() {
        Dto dto1 = new Dto();
        Dto dto2 = new Dto();
        dto1.setAge(1);
        dto1.setName("OPTION1");
        dto2.setAge(2);
        dto2.setName("OPTION2");
        options = new ArrayList<Dto>();
        options.add(dto1);
        options.add(dto2);
        map = new HashMap<String, ArrayList<Dto>>();
        strings = new ArrayList<String>();
        strings.add(new String("TEST1"));
        strings.add(new String("TEST2"));
        
        strings.forEach(s -> map.put(s, new ArrayList<Dto>()));
    }
    public void debug() {
        System.out.println("debug");
        ArrayList<ArrayList<Dto>> testarr = new ArrayList<ArrayList<Dto>>();
        map.forEach((k,v)->testarr.add(v));
    }
    
}

Dto

public class Dto {
    private String name;
    private int age;
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public int getAge() {
        return age;
    }
    public void setAge(int age) {
        this.age = age;
    }
}

Converter

@FacesConverter(value="myConverter")
public class MyConverter implements Converter<Dto> {

    private Map<Dto, String> entities = new HashMap<Dto, String>();

    @Override
    public String getAsString(FacesContext context, UIComponent component, Dto value) {
        synchronized (entities) {
            if (!entities.containsKey(value)) {
                String uuid = UUID.randomUUID().toString();
                entities.put(value, uuid);
                return uuid;
            } else {
                return entities.get(value);
            }
        }
    }

    @Override
    public Dto getAsObject(FacesContext context, UIComponent component, String value) {
        for (Map.Entry<Dto, String> entry : entities.entrySet()) {
            if (entry.getValue().equals(value)) {
                return entry.getKey();
            }
        }
        return null;
    }
}

Xhtml

<ui:repeat var="str" value="#{testView.strings}" varStatus="strStatus">
    <ui:param name="indexStr" value="#{strStatus.index}" />
    <p:selectManyCheckbox value="#{testView.map[str]}"
        converter="myConverter" layout="responsive" columns="6">
        <p:ajax process="@this" />
        <f:selectItems value="#{testView.options}" var="item"
            itemLabel="#{item.name}" itemValue="#{item}" />
    </p:selectManyCheckbox>
</ui:repeat>
<p:commandButton value="debug" icon="pi pi-check" process="@this"
    action="#{testView.debug()}" />

Solution

  • You're victim of type erasure in generics.

    Basically, in raw Java source code you have Map<String, ArrayList<Dto>> map.

    But in the compiled output of that code it has become Map map and information about String and ArrayList<Dto> is lost.

    This is the same problem the EL expressions #{...} are facing. When it tries to inspect the type of value="#{testView.map[str]}" via ValueExpression#getType() it retrieves java.lang.Object not java.lang.ArrayList.

    Technically speaking, setting collectionType attribute as follows must work

    <p:selectManyCheckbox ... collectionType="java.util.ArrayList">
    

    But to my surprise it indeed doesn't have any effect. It would only work if the java.lang.Object was actually an instance of java.util.Collection. I consider this a bug in Mojarra. I've fixed this via issue 5427.

    By the way, your converter is not thread safe and it's not entirely memory efficient: Arguments against a generic JSF object converter with a static WeakHashMap. Take a look at OmniFaces SelectItemsConverter.