ValueChangeListener для значений BigDecimal, не вызывается, если изменить только масштаб

У меня есть приложение JSF 2.2, где пользователь должен вводить значения BigDecimal в <h:inputText>. Для такого входного поля a valueChangeListener сконфигурирован для вызова изменений ввода. Вот код XHTML:

<h:form id="theForm">
  <h:inputText id="bd" value="#{bean.bd}"
               valueChangeListener="#{bean.bdChangedListener()}"/>
  <h:commandButton id="submit" value="Submit" />
</h:form>

В большинстве случаев это нормально. Вызывается метод bdChangedListener(), когда значение изменено и нажата кнопка отправки. Значение верно передано модели.

Однако, если я ввел 1.1 и изменил его на 1.10, новое значение будет привязано к модели, но valueChangeListener никогда не называется! Отладка показала, что причина этого в javax.faces.component.UIInput#compareValues(). JavaDoc из  этот метод говорит:

Возвращает true, если новое значение отличается от предыдущего. Сначала сравните два значения, передав значение методу equals на аргумент предыдущий. Если этот метод возвращает true, верните true. Если это метод возвращает false, и оба аргумента реализуют java.lang.Comparable, сравните два значения, передав значение в compareTo метод по аргументу previous. Возвращает true, если этот метод возвращает 0, false в противном случае.

Мне кажется, это намеренно. Но почему?

Пользовательский ввод изменился, и есть приложения, в которых важна шкала BigDecimal. Поэтому JSF не должен просто игнорировать измененный ввод, но уведомлять меня! Он обновляет модель с новым значением, почему она пропускает метод valueChangeListener?

Как я мог избежать этого поведения и получать уведомление в чистом виде? (Я знаю, что могу подключиться к сеттерам, но это не то, что я называю чистым способом!)

Любые идеи?

Дальнейшее чтение и комментарии

В дополнение к вышеизложенному я хочу упомянуть, что я уже читал такие вопросы, как BigDecimal equals() и compareTo(). Я понимаю, почему BigDecimal equals() и compareTo() ведут себя так, как они, и я бы сказал, что это правильно. Поведение BigDecimal не является проблемой, проблема UIInput.compareValues().

Также конвертер (как предлагается в комментариях или уже удаленных ответах) не сохранит мой день. Вход пользователя правильно преобразован, и мне нужен BidDecimal, включая точный масштаб в моем приложении. Любой конвертер, возвращающий BigDecimal, не изменит наблюдаемое поведение.

Класс оболочки вокруг BigDecimal может решить мою проблему, но это не то, что я считаю чистым решением. Я действительно хочу знать, почему UIInput ведет себя так, как это делает.

Ответ 1

Это действительно так, как указано. Что есть, то есть. Вы очень правильно прикололи основную причину до javax.faces.component.UIInput#compareValues(). Решение состояло бы в том, чтобы пропустить дополнительную проверку Comparable#compareTo().

Лучше всего расширить HtmlInputText и соответственно заменить compareValues().

@FacesComponent(createTag=true)
public class InputBigDecimal extends HtmlInputText {

    @Override
    protected boolean compareValues(Object previous, Object value) {
        return !Objects.equals(previous, value);
    }

}

Затем просто замените <h:inputText> на <x:inputBigDecimal>, как показано ниже.

<... xmlns:x="http://xmlns.jcp.org/jsf/component">
...
<h:form id="theForm">
    <x:inputBigDecimal id="bd" value="#{bean.bd}"
                       valueChangeListener="#{bean.bdChangedListener()}" />
    <h:commandButton id="submit" value="Submit" />
</h:form>

Примечание: код завершен как есть. Никаких дополнительных файлов конфигурации XML не требуется благодаря JSF 2.2 createTag=true. Вы пропустите только IDE XML intellisense, но это другая проблема.

Ответ 2

В ответе BalusC описывается общий подход к исправлению проблемы чистым способом без изменения поведения приложения. Antoher, возможное обходное решение, которое работает, но немного изменяет поведение вашего приложения, заключается в использовании AJAX- listener вместо valueChangeListener:

<h:form id="theForm">
  <h:inputText id="bd" value="#{bean.bd}">
    <f:ajax listener="#{bean.ajaxListener}"/>
  </h:inputText>
  <h:commandButton id="submit" value="Submit" />
</h:form>

Метод listener должен иметь другую подпись:

public void ajaxListener(AjaxBehaviorEvent event)
{
    // Do whatever you like here.
    // The new value is already in the model.
}

Обратите внимание, что AJAX-прослушиватель вызывается, как только происходит событие изменения DOM (а не при нажатии кнопки sumbit) и не проверяет, изменилось ли фактическое значение, как valueChangeListener.

Подробности о различиях в двух подходах можно найти в превосходном ответе BalusC на Когда использовать valueChangeListener или f: ajax listener?.