Conversion Error setting value for 'null Converter' - Why do I need a Converter in JSF?

后端 未结 2 548
南笙
南笙 2020-11-21 15:32

I have problems understanding how to use selection in JSF 2 with POJO/entity effectively. For example, I\'m trying to select a Warehouse entity via the below dr

相关标签:
2条回答
  • 2020-11-21 16:11

    Example of JSF generic converter with ABaseEntity and identifier:

    ABaseEntity.java

    public abstract class ABaseEntity implements Serializable {
    
        private static final long serialVersionUID = 1L;
    
        public abstract Long getIdentifier();
    }
    

    SelectItemToEntityConverter.java

    @FacesConverter(value = "SelectItemToEntityConverter")
    public class SelectItemToEntityConverter implements Converter {
    
        @Override
        public Object getAsObject(FacesContext ctx, UIComponent comp, String value) {
            Object o = null;
            if (!(value == null || value.isEmpty())) {
                o = this.getSelectedItemAsEntity(comp, value);
            }
            return o;
        }
    
        @Override
        public String getAsString(FacesContext ctx, UIComponent comp, Object value) {
            String s = "";
            if (value != null) {
                s = ((ABaseEntity) value).getIdentifier().toString();
            }
            return s;
        }
    
        private ABaseEntity getSelectedItemAsEntity(UIComponent comp, String value) {
            ABaseEntity item = null;
    
            List<ABaseEntity> selectItems = null;
            for (UIComponent uic : comp.getChildren()) {
                if (uic instanceof UISelectItems) {
                    Long itemId = Long.valueOf(value);
                    selectItems = (List<ABaseEntity>) ((UISelectItems) uic).getValue();
    
                    if (itemId != null && selectItems != null && !selectItems.isEmpty()) {
                        Predicate<ABaseEntity> predicate = i -> i.getIdentifier().equals(itemId);
                        item = selectItems.stream().filter(predicate).findFirst().orElse(null);
                    }
                }
            }
    
            return item;
        }
    }
    

    And usage:

    <p:selectOneMenu id="somItems" value="#{exampleBean.selectedItem}" converter="SelectItemToEntityConverter">
        <f:selectItem itemLabel="< select item >" itemValue="#{null}"/>
        <f:selectItems value="#{exampleBean.availableItems}" var="item" itemLabel="${item.identifier}" itemValue="#{item}"/>
    </p:selectOneMenu>
    
    0 讨论(0)
  • 2020-11-21 16:20

    Introduction

    JSF generates HTML. HTML is in Java terms basically one large String. To represent Java objects in HTML, they have to be converted to String. Also, when a HTML form is submitted, the submitted values are treated as String in the HTTP request parameters. Under the covers, JSF extracts them from the HttpServletRequest#getParameter() which returns String.

    To convert between a non-standard Java object (i.e. not a String, Number or Boolean for which EL has builtin conversions, or Date for which JSF provides builtin <f:convertDateTime> tag), you really have to supply a custom Converter. The SelectItem has no special purpose at all. It's just a leftover from JSF 1.x when it wasn't possible to supply e.g. List<Warehouse> directly to <f:selectItems>. It has also no special treatment as to labels and conversion.

    getAsString()

    You need to implement getAsString() method in such way that the desired Java object is been represented in an unique String representation which can be used as HTTP request parameter. Normally, the technical ID (the database primary key) is used here.

    public String getAsString(FacesContext context, UIComponent component, Object modelValue) {
        if (modelValue == null) {
            return "";
        }
    
        if (modelValue instanceof Warehouse) {
            return String.valueOf(((Warehouse) modelValue).getId());
        } else {
            throw new ConverterException(new FacesMessage(modelValue + " is not a valid Warehouse"));
        }
    }
    

    Note that returning an empty string in case of a null/empty model value is significant and required by the javadoc. See also Using a "Please select" f:selectItem with null/empty value inside a p:selectOneMenu.

    getAsObject()

    You need to implement getAsObject() in such way that exactly that String representation as returned by getAsString() can be converted back to exactly the same Java object specified as modelValue in getAsString().

    public Object getAsObject(FacesContext context, UIComponent component, String submittedValue) {
        if (submittedValue == null || submittedValue.isEmpty()) {
            return null;
        }
    
        try {
            return warehouseService.find(Long.valueOf(submittedValue));
        } catch (NumberFormatException e) {
            throw new ConverterException(new FacesMessage(submittedValue + " is not a valid Warehouse ID"), e);
        }
    }
    

    In other words, you must be technically able to pass back the returned object as modelValue argument of getAsString() and then pass back the obtained string as submittedValue argument of getAsObject() in an infinite loop.

    Usage

    Finally just annotate the Converter with @FacesConverter to hook on the object type in question, JSF will then automatically take care of conversion when Warehouse type ever comes into the picture:

    @FacesConverter(forClass=Warehouse.class)
    

    That was the "canonical" JSF approach. It's after all not very effective as it could indeed also just have grabbed the item from the <f:selectItems>. But the most important point of a Converter is that it returns an unique String representation, so that the Java object could be identified by a simple String suitable for passing around in HTTP and HTML.

    Generic converter based on toString()

    JSF utility library OmniFaces has a SelectItemsConverter which works based on toString() outcome of the entity. This way you do not need to fiddle with getAsObject() and expensive business/database operations anymore. For some concrete use examples, see also the showcase.

    To use it, just register it as below:

    <h:selectOneMenu ... converter="omnifaces.SelectItemsConverter">
    

    And make sure that the toString() of your Warehouse entity returns an unique representation of the entity. You could for instance directly return the ID:

    @Override
    public String toString() {
        return String.valueOf(id);
    }
    

    Or something more readable/reusable:

    @Override
    public String toString() {
        return "Warehouse[id=" + id + "]";
    }
    

    See also:

    • How to populate options of h:selectOneMenu from database?
    • Generic JSF entity converter - so that you don't need to write a converter for every entity.
    • Using enums in JSF selectitems - enums needs to be treated a bit differently
    • How to inject @EJB, @PersistenceContext, @Inject, @Autowired, etc in @FacesConverter?

    Unrelated to the problem, since JSF 2.0 it's not explicitly required anymore to have a List<SelectItem> as <f:selectItem> value. Just a List<Warehouse> would also suffice.

    <h:selectOneMenu value="#{bean.selectedWarehouse}">
        <f:selectItem itemLabel="Choose one .." itemValue="#{null}" />
        <f:selectItems value="#{bean.availableWarehouses}" var="warehouse"
            itemLabel="#{warehouse.name}" itemValue="#{warehouse}" />
    </h:selectOneMenu>
    
    private Warehouse selectedWarehouse;
    private List<Warehouse> availableWarehouses;
    
    0 讨论(0)
提交回复
热议问题