BeansBindingとDocumentFiler

List <-> JTable <-> JTable#Column <-> JTextField

JTextFieldにはDocumentFilterを介して入力文字と長さのチェックを行っている。
この状態でJTableの特定のセルをクリックすると、以下のスタックで再帰呼び出しになってしまう。(関係の無いトレースは端折っている)

BeansBindingのソースコードを調べてみたが、どうやらこいつが自らJTextFieldに設定されたDocumentのDocumentListenerと、バインドされているJavaBeanのPropertyChangeListenerそれぞれから通知を受けるようにリスナを追加しているのが原因のようだ。

org.jdesktop.swingbinding.adapters.JTextComponentAdapterProvider.javaより

private void installDocumentListener() {
    if (property != PROPERTY_BASE) {
        return;
    }

    boolean useDocumentFilter = !(component instanceof JFormattedTextField);
    
    if (useDocumentFilter && (document instanceof AbstractDocument) &&
            ((AbstractDocument)document).getDocumentFilter() == null) {
        ((AbstractDocument)document).setDocumentFilter(handler);
        installedFilter = true;
    } else {
        document.addDocumentListener(handler);
        installedFilter = false;
    }
}

org.jdesktop.beansbinding.ELProperty.javaより

private void registerListener(ResolvedProperty resolved, SourceEntry entry) {
    Object source = resolved.getSource();
    Object property = resolved.getProperty();
    if (source != null && property instanceof String) {
        String sProp = (String)property;

        if (source instanceof ObservableMap) {
            RegisteredListener rl = new RegisteredListener(source, sProp);

            if (!entry.registeredListeners.contains(rl)) {
                if (!entry.lastRegisteredListeners.remove(rl)) {
                    *1 {
            source = getAdapter(source, sProp);

            RegisteredListener rl = new RegisteredListener(source, sProp);

            if (!entry.registeredListeners.contains(rl)) {
                if (!entry.lastRegisteredListeners.remove(rl)) {
                    addPropertyChangeListener(source, entry);
                }
                
                entry.registeredListeners.add(rl);
            }
        }
    }
}

これらの結果として、

1.JTable#changeSelection 
  -> 2. JTableAdapterProvider$Adapter$Handler#valueChanged 
    -> 3. JTextField#setText 
      -> 4. JTextField.DocumentLister#replaceによる変更通知 
        -> 5.JavaBeansのプロパティ変更 
          -> 6.AbstractBean#PropertyChangeListenerによる変更通知 
            -> 7.ELProperty$SourceEntry#propertyChange 
              -> 8. JTextField#setText ->ここで 例外 (Attempt to mutate in notification)

とイベントが伝搬、再起することを確認している。

さて、この問題の回避方法だが根治させるにはイベント再起の輪をどこかで断ち切らなくてはならない。

・JTextFieldのバリデーションにDocumentFilter(DocumentLister)を使わない
・JavaBeansのPropertyChangeSupportを止める

どちらも一長一短だが、BeansBindingの場合.NETとは違いGUIの更新の更新にPropertyChangeSupportは必須ではないので、後者を使うのが無難な気がする。
しかし、今回のように、あるライブラリィの特定の問題を回避するために一般的ではない:=トリッキーなコーディングを始めると、それが次の一般的ではないコーディングを呼び、最終的に担当者以外には難解なコードになってしまう。最悪だ。

*1:ObservableMap)source).addObservableMapListener(entry); } entry.registeredListeners.add(rl); } } else if (!(source instanceof Map