UISelectMany в пользовательском интерфейсе: повтор вызывает java.lang.ClassCastException: [Ljava.lang.Object; нельзя привести к java.util.List

Я использовал HashMap метод для привязки списка флажков к Map<String, Boolean> с успехом. Это хорошо, так как позволяет вам иметь динамическое количество флажков.

Я пытаюсь расширить это до списка переменной длины selectManyMenu, Учитывая, что они являются избранными, я хотел бы иметь возможность связать с Map<String, List<MyObject>>, У меня есть один пример работы, где я могу связать один selectManyMenu к List<MyObject> и все работает нормально, но почему я положил динамический номер selectManyMenus внутри ui:repeat и попытка привязать к карте, я получаю странные результаты. Значения правильно хранятся на карте, что проверяется отладчиком и вызывает toString(), но среда выполнения думает, что значения карты имеют тип Object и не List<MyObject> и выбрасывает ClassCastException, когда я пытаюсь получить доступ к ключам карты.

Я предполагаю, что это как-то связано с тем, как JSF определяет тип времени выполнения цели вашей привязки, и так как я связываюсь со значением в Map, он не знает, чтобы получить тип из параметра типа значения карты. Есть ли какое-нибудь решение этой проблемы, кроме исправления Мохарры?

В общем, как я могу иметь страницу с динамическим номером selectManyMenus? Без, конечно, использования Primefaces <p:solveThisProblemForMe> составная часть. (На самом деле, Primefaces здесь не вариант, из-за факторов, не зависящих от меня.)

Вопрос UISelectMany для List вызывает java.lang.ClassCastException: java.lang.String не может быть приведен к T, имел некоторую полезную информацию, о которой я не знал, но у меня все еще есть проблемы с этим SSCE:

JSF:

  <ui:define name="content">
    <h:form>
      <ui:repeat value="#{testBean.itemCategories}" var="category">
        <h:selectManyMenu value="#{testBean.selectedItemMap[category]}">
          <f:selectItems value="#{testBean.availableItems}" var="item" itemValue="#{item}" itemLabel="#{item.name}"></f:selectItems>
          <f:converter binding="#{itemConverter}"></f:converter>
          <f:validator validatorId="test.itemValidator"></f:validator>
        </h:selectManyMenu>
      </ui:repeat>
      <h:commandButton value="Submit">
        <f:ajax listener="#{testBean.submitSelections}" execute="@form"></f:ajax>
      </h:commandButton>
    </h:form>
  </ui:define>

Преобразователь:

@Named
public class ItemConverter implements Converter {

  @Inject
  ItemStore itemStore;

  @Override
  public Object getAsObject(FacesContext context, UIComponent component, String value) {
    return itemStore.getById(value);
  }

  @Override
  public String getAsString(FacesContext context, UIComponent component, Object value) {
    return Optional.of(value)
                   .filter(v -> Item.class.isInstance(v))
                   .map(v -> ((Item) v).getId())
                   .orElse(null);
  }
}

Бэк бин:

@Data
@Slf4j
@Named
@ViewScoped
public class TestBean implements Serializable {

  private static final long serialVersionUID = 1L;

  @Inject
  ItemStore itemStore;

  List<Item> availableItems;

  List<String> itemCategories;

  Map<String, List<Item>> selectedItemMap = new HashMap<>();

  public void initialize() {
    log.debug("Initialized TestBean");

    availableItems = itemStore.getAllItems();

    itemCategories = new ArrayList<>();
    itemCategories.add("First Category");
    itemCategories.add("Second Category");
    itemCategories.add("Third Category");
  }

  public void submitSelections(AjaxBehaviorEvent event) {
    log.debug("Submitted Selections");

    selectedItemMap.entrySet().forEach(entry -> {
      String key = entry.getKey();
      List<Item> items = entry.getValue();

      log.debug("Key: {}", key);

      items.forEach(item -> {
        log.debug("   Value: {}", item);
      });

    });

  }

}

ItemStore просто содержит HashMap и делегирует методы для доступа к Предметам по их полю ID.

Вещь:

@Data
@Builder
public class Item {
  private String id;
  private String name;
  private String value;
}

ItemListValidator:

@FacesValidator("test.itemValidator")
public class ItemListValidator implements Validator {

  @Override
  public void validate(FacesContext context, UIComponent component, Object value) throws ValidatorException {
    if (List.class.isInstance(value)) {

      if (((List) value).size() < 1) {
        throw new ValidatorException(new FacesMessage(FacesMessage.SEVERITY_FATAL, "You must select at least 1 Admin Area", "You must select at least 1 Admin Area"));
      }
    }
  }

}

Ошибка:

java.lang.ClassCastException: [Ljava.lang.Object; cannot be cast to java.util.List

Stacktrace перерезан, но встречается в этой строке:

List<Item> items = entry.getValue();

Что мне здесь не хватает?

1 ответ

Решение

Как указано в связанном вопросе, UISelectMany для List вызывает java.lang.ClassCastException: java.lang.String не может быть приведен к T, аргументы универсального типа недоступны во время выполнения. Другими словами, EL не знает, что у вас есть Map<String, List<Item>>, Все, что знает EL, это то, что у вас есть Map, поэтому, если вы явно не укажете конвертер для выбранных значений и тип коллекции для коллекции, JSF по умолчанию будет String для выбранных значений и массива объектов Object[] для коллекции. Обратите внимание, что [ в [Ljava.lang.Object указывает на массив.

Учитывая, что вы хотите, чтобы тип коллекции был экземпляром java.util.Listнеобходимо указать collectionType атрибут с FQN желаемой конкретной реализации.

<h:selectManyMenu ... collectionType="java.util.ArrayList">

Затем JSF удостоверится, что создается правильный тип коллекции, чтобы заполнить выбранные элементы и вставить в модель. Вот связанный вопрос, где такое решение используется, но затем по другой причине: org.hibernate.LazyInitializationException в com.sun.faces.renderkit.html_basic.MenuRenderer.convertSelectManyValuesForModel.


Обновление: я должен был проверить вышеупомянутую теорию. Это не работает в Мохарре, когда коллекция позади collectionType в свою очередь оборачивается в другую общую коллекцию / карту. Мохарра только проверяет collectionType если UISelectMany само значение уже представляет собой экземпляр java.util.Collection, Тем не менее, из-за того, что он был завернут в Mapего (необработанный) тип становится java.lang.Object и тогда Мохарра пропустит проверку на любой collectionType,

MyFaces проделал лучшую работу в этом в своем UISelectMany рендер, он работает там.

Насколько я проверял исходный код Mojarra, нет способа обойти этот другой способ, кроме замены Map<String, List<Long>> по List<Category> где Category пользовательский объект, имеющий String name а также List<MyObject> selectedItems свойства. Правда, это действительно убивает преимущество Map иметь динамические ключи в EL, но это то, что есть.

Вот MCVE с помощью Long как тип элемента (просто замените его MyObject):

private List<Category> categories;
private List<Long> availableItems;

@PostConstruct
public void init() {
    categories = Arrays.asList(new Category("one"), new Category("two"), new Category("three"));
    availableItems = Arrays.asList(1L, 2L, 3L, 4L, 5L);
}

public void submit() {
    categories.forEach(c -> {
        System.out.println("Name: " + c.getName());

        for (Long selectedItem : c.getSelectedItems()) {
            System.out.println("Selected item: " + selectedItem);
        }
    });

    // ...
}

public class Category {

    private String name;
    private List<Long> selectedItems;

    public Category(String name) {
        this.name = name;
    }

    // ...
}

<h:form>
    <ui:repeat value="#{bean.categories}" var="category">
        <h:selectManyMenu value="#{category.selectedItems}" converter="javax.faces.Long">
            <f:selectItems value="#{bean.availableItems}" />
        </h:selectManyMenu>
    </ui:repeat>
    <h:commandButton value="submit" action="#{bean.submit}">
        <f:ajax execute="@form" />
    </h:commandButton>
</h:form>

Обратите внимание, что collectionType здесь не нужно Только converter все еще необходимо.

Вне зависимости от конкретной проблемы, я хотел бы отметить, что selectedItemMap.entrySet().forEach(entry -> { String key ...; List<Item> items ...;}) можно упростить до selectedItemMap.forEach((key, items) -> {}) и это ItemListValidator не нужно, если вы просто используете required="true" на входной компонент.

Другие вопросы по тегам