Значение параметра ошибки преобразования для 'null Converter'

У меня проблемы с пониманием того, как эффективно использовать выбор в JSF 2 с POJO/сущностью. Например, я пытаюсь выбрать объект Warehouse через раскрывающееся меню ниже:

<h:selectOneMenu value="#{bean.selectedWarehouse}">
    <f:selectItem itemLabel="Choose one .." itemValue="#{null}" />
    <f:selectItems value="#{bean.availableWarehouses}" />
</h:selectOneMenu>

И ниже управляемый bean:

@Named
@ViewScoped
public class Bean {

    private Warehouse selectedWarehouse;
    private List<SelectItem> availableWarehouses;

    // ...

    @PostConstruct
    public void init() {
        // ...

        availableWarehouses = new ArrayList<>();

        for (Warehouse warehouse : warehouseService.listAll()) {
            availableWarehouses.add(new SelectItem(warehouse, warehouse.getName()));
        }
    }

    // ...
}

Обратите внимание, что я использую весь объект Warehouse как значение SelectItem.

Когда я отправляю форму, это происходит с сообщением следующих лиц:

Значение ошибки конверсии '[email protected]' для 'null Converter'.

Я надеялся, что JSF может просто установить правильный объект Warehouse для моего управляемого bean, когда я оберну его в SelectItem. Обертка моего объекта внутри SelectItem означала пропустить создание Converter для моего объекта.

Нужно ли мне использовать Converter, когда я хочу использовать объекты в моем <h:selectOneMenu>? Для JSF необходимо просто извлечь выбранный элемент из списка доступных элементов. Если мне действительно нужно использовать конвертер, какой практический способ это сделать? До сих пор я подошел к этому:

  • Создайте реализацию Converter для объекта.
  • Переопределение getAsString(). Я думаю, что мне это не нужно, так как свойство метки SelectItem будет использоваться для отображения метки выпадающего списка.
  • Переопределение getAsObject(). Я думаю, что это будет использоваться для возврата правильного SelectItem или объекта в зависимости от типа выбранного поля, определенного в управляемом bean.

getAsObject() меня смущает. Каков эффективный способ сделать это? Имея строковое значение, как мне получить связанный объект объекта? Должен ли я запрашивать объект объекта из объекта службы на основе значения строки и возвращать объект? Или, возможно, каким-то образом я могу получить доступ к списку объектов, которые формируют элементы выбора, зацикливать их, чтобы найти правильный объект, и вернуть объект?

Каков нормальный подход?

Ответ 1

Введение

JSF генерирует HTML. HTML в Java-терминах в основном один большой String. Чтобы представить объекты Java в HTML, их нужно преобразовать в String. Кроме того, при отправке HTML-формы представленные значения обрабатываются как String в параметрах запроса HTTP. Под обложками JSF извлекает их из HttpServletRequest#getParameter(), который возвращает String.

Для преобразования между нестандартным Java-объектом (т.е. не String, Number или Boolean, для которого EL имеет встроенные преобразования, или Date, для которого JSF предоставляет встроенный <f:convertDateTime>), вам действительно нужно предоставить пользовательский Converter. SelectItem не имеет никакой специальной цели. Это просто осталось от JSF 1.x, когда было невозможно подавать, например. List<Warehouse> непосредственно на <f:selectItems>. Он также не имеет особого отношения к методам и конверсии.

getAsString()

Вам необходимо реализовать метод getAsString() таким образом, чтобы желаемый объект Java был представлен в уникальном String, которое может использоваться как параметр запроса HTTP. Обычно здесь используется технический идентификатор (первичный ключ базы данных).

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"));
    }
}

Обратите внимание, что возвращение пустой строки в случае нулевого/пустого значения модели является значительным и требуется javadoc. См. Также Использование "Пожалуйста, выберите" f: selectItem с нулевым/пустым значением внутри p: selectOneMenu.

getAsObject()

Вам нужно реализовать getAsObject() таким образом, чтобы точно, что String представление возвращалось getAsString() можно преобразовать обратно в точно тот же Java-объект, который указан как modelValue в 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);
    }
}

Другими словами, вы должны иметь техническую возможность передать возвращаемый объект в качестве аргумента modelValue getAsString(), а затем передать полученную строку как аргумент submittedValue getAsObject() в бесконечном цикле.

Использование

Наконец, просто комментируйте Converter с помощью @FacesConverter, чтобы подключиться к типу объекта, о котором идет речь, JSF автоматически позаботится об обращении когда тип Warehouse появляется на картинке:

@FacesConverter(forClass=Warehouse.class)

Это был "канонический" подход JSF. В конце концов, это не очень эффективно, поскольку он действительно может просто захватить элемент из <f:selectItems>. Но наиболее важной точкой Converter является то, что он возвращает представление unique String, так что объект Java может быть идентифицирован с помощью простого String, подходящего для прохождения в HTTP и HTML,

Общий преобразователь на основе toString()

Библиотека утилиты JSF OmniFaces имеет SelectItemsConverter, который работает на основе результата toString() объекта. Таким образом, вам больше не нужно возиться с getAsObject() и более дорогостоящими операциями с бизнесом/базой данных. Для некоторых конкретных примеров использования см. Также showcase.

Чтобы использовать его, просто зарегистрируйте его, как показано ниже:

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

И убедитесь, что toString() вашего объекта Warehouse возвращает представление уникальное объекта. Например, вы можете напрямую вернуть идентификатор:

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

Или что-то более читаемое/многоразовое:

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

См. также:


Несвязанный к проблеме, поскольку JSF 2.0 явно не требует больше значения List<SelectItem> как <f:selectItem>. Достаточно просто List<Warehouse>.

<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;

Ответ 2

Пример универсального конвертера JSF с ABaseEntity и идентификатором:

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;
    }
}

И использование:

<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>