JSR 295: Beans Bindingの参照実装公開

本コラムの第6回では、「JSR 295: Beans Binding」の概要を紹介した。JSR 295は、複数のJavaBeansのプロパティを同期させるためのバインディングAPIである。第6回ではその参考としていくつかのバインディングフレームワークの使い方を解説した。

4月に入り、java.net内にあるプロジェクトサイトにおいてJSR 295の参照実装とAPIドキュメントが公開された。今回はこの参照実装を使って、Beansバインディングを用いたSwingプログラムを作ってみようと思う。なおJSR 295はJCPのステージ上ではまだ早期ドラフトすら公開されていない段階であり、今回公開されたのはあくまでも開発版である。今後大きな変更が加えられる可能性もあるので注意していただきたい。

JTextFieldとJLabelを使ったバインディング

JSR 295のプロジェクトサイトのドキュメントセクションからダウンロードすることができるAPIドキュメントを見てもわかるように、この実装にはJSR 295のベースとなる以下の4つのパッケージと、それに関連したcom.sun.*パッケージが含まれている。

  • javax.beans.binding
    2つのBeanのプロパティをバインドするためのベースとなるクラス群
  • javax.beans.binding.ext
    バインディングAPIを独自に拡張するための機能を提供する
  • javax.el
    「Unified Expression Language(Unified EL)」を使用するためのクラス群
  • javax.swing.binding
    Swingコンポーネントのバインディングをサポート

このうちのjavax.elは、JSP 2.1やJSF 1.2などで利用されてるUnified ELのためのAPIである。JSR 295ではBeanどうしのバインディング定義にUnified ELを使用できる。javax.elパッケージはそのためのもので、これはGlassfishプロジェクトの成果物を元にしてBeans Binding用に多少の修正を加えたものであるとのことだ。

それでは、実際にBeanのプロパティを同期させてみよう。リスト1はJTextFieldのtextプロパティとJLabelのtextプロパティをバインディングする例だ。

バインディングの定義はjavax.beans.binding.Bindingクラスによって行う。Bindingクラスのコンストラクタには、第1 - 第4引数でバインド対象のBeanオブジェクトとプロパティを指定する。5番目以降は可変長の引数で、Beanに応じたオプションを指定できるようになっている。javax.swing.binding.SwingBindingSupportクラスには各種Swingコンポーネントのためのパラメータが定義されており、ここではテキストフィールドのテキストが変更された際にパラメータの値の同期を実行するよう指定している。

リスト1 TextfieldBindingSample.java - テキストフィールドとラベルのバインディング

package apisample;

import java.awt.GridLayout;
import javax.beans.binding.Binding;
import javax.swing.*;
import javax.swing.binding.SwingBindingSupport;

public class TextfieldBindingSample {       
    public TextfieldBindingSample() {
        JFrame frame = new JFrame("TextfieldBindingSample");
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        JPanel panel = new JPanel(new GridLayout(2,1));        
        JTextField jTextField = new JTextField();
        JLabel jLabel = new JLabel("Please input.");

        // テキストフィールドとラベルのプロパティをバインディング
        Binding binding = new Binding(jLabel, "${text}", jTextField, "text", 
                SwingBindingSupport.TextChangeStrategyParameter, 
                SwingBindingSupport.TextChangeStrategy.CHANGE_ON_TYPE);
        binding.bind();

        panel.add(jLabel);
        panel.add(jTextField);
        frame.getContentPane().add(panel);
        frame.setSize(250, 100);
        frame.setVisible(true);
    }

    public static void main(String[] args) {
        TextfieldBindingSample sampple = new TextfieldBindingSample();
    }
}

このプログラムを実行すると図1のように表示される。テキストフィールドのテキストを変更すると即座にラベルに反映され、両者が同期していることがわかる。

図1 TextfieldBindingSampleの実行例

JTableを使ったバインディング

SwingBindingSupportクラスでは、JTextFieldとJLabel以外にもJCheckBox、JComboBox、JList、JSlider、JTable、JTree、その他のJTextComponentなどのバインディングをサポートしている。リスト2に示すのは、JavaBeanオブジェクトのプロパティとJTableに表示する値をバインドした例だ。

リスト2 TableBindingSample.java - Personオブジェクトのリストとテーブルのバインディング

package apisample;

import java.util.*;
import javax.beans.binding.Binding;
import javax.swing.*;
import javax.swing.binding.SwingBindingSupport;

public class TableBindingSample {       
    public TableBindingSample() {
        JFrame frame = new JFrame("TableBindingSample");
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

        List persons = new ArrayList();
        persons.add(new Person("Takaaki", "SUGIYAMA"));
        persons.add(new Person("Daichi", "GOTO"));
        JTable jTable = new JTable();

        // Personオブジェクトのリストとテーブルをバインディング
        Binding binding = new Binding(persons, null, jTable, "elements");
        binding.addBinding("${firstName}", null, 
                      SwingBindingSupport.TableColumnParameter, 0);
        binding.addBinding("${lastName}", null, 
                      SwingBindingSupport.TableColumnParameter, 1);
        binding.bind();

        frame.getContentPane().add(jTable);
        frame.setSize(250, 150);
        frame.setVisible(true);
    }

    public static void main(String[] args) {
        TableBindingSample sampple = new TableBindingSample();
    }
}

ここで使用しているPersonクラスはリスト3のようなもので、はじめにそのオブジェクトを要素に持つLitsを作成している。そして、まずはこのListオブジェクトとJTableのバインディングを定義したBindingを作成する。続いてListの要素であるPersonの各要素と、JTableの各カラムとのバインディングの定義を、addBinding()メソッドによって行っている。

リスト3 Person.java

package apisample;

public class Person {
    private String firstName;
    private String lastName;

    public Person(String firstName, String lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
    }

    /** SetterおよびGetterは省略 **/
}

このプログラムを実行すると図2のように表示される。

図2 TableBindingSampleの実行例

Java Persistence APIとの組み合わせ

最後に、JSR 295をJava Persistence API(JPA)と組み合わせて使用する例を紹介する。JPAはJavaオブジェクトの永続化のためのAPIであり、Swingコンポーネントを永続化オブジェクトと同期させれば、ユーザからの入力を容易にデータベースに反映させることができる。JPAの使用方法については特集記事を参照してほしい。ここではJPAの実装としてGlassfishに付属するToplink Essentialsを、データベースエンジンとしてH2 Database Engineを使用する。

まず、永続化エンティティとしてリスト4のようなJavaBeanを用意する。PropertyChangeEventをサポートするためにフィールドにPropertyChangeSupportを宣言してあるが、これは永続化の対象とならないように@Transientを指定している。

リスト4 PersonEntity.java

package apisample;

import java.beans.*;
import java.io.Serializable;
import javax.persistence.*;

@Entity
public class PersonEntity implements Serializable {
    @Transient
    private PropertyChangeSupport changes = new PropertyChangeSupport(this);

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    private String firstName;
    private String lastName;

    public PersonEntity() {
    }

    public Long getId() {
        return this.id;
    }
    public void setId(Long id) {
        this.id = id;
    }
    public void setFirstName(String firstName) {
        String oldFirstName = this.firstName;
        this.firstName = firstName;
        changes.firePropertyChange("firstName", oldFirstName, this.firstName);
    } 
    public String getFirstName() {
        return this.firstName;
    }
    public void setLastName(String lastName) {
        String oldLastName = this.lastName;
        this.lastName = lastName;
        changes.firePropertyChange("lastName", oldLastName, this.lastName);
    } 
    public String getLastName() {
        return this.lastName;
    }

    public void addPropertyChangeListener(PropertyChangeListener l) {
        changes.addPropertyChangeListener(l);
    }
    public void removePropertyChangeListener(PropertyChangeListener l) {
        changes.removePropertyChangeListener(l);
    }

    @Override
    public String toString() {
        return "PersonEntity[id:" + this.id + ", " + this.firstName + ", " + this.lastName + "]";
    }
}

persistence.xmlはリスト5のようになる。データベース名などは各自の環境に応じて設定すること。

リスト5 persistence.xml

<?xml version="1.0" encoding="UTF-8"?>
<persistence version="1.0" xmlns="http://java.sun.com/xml/ns/persistence" 
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
        xsi:schemaLocation="http://java.sun.com/xml/ns/persistence 
                http://java.sun.com/xml/ns/persistence/persistence_1_0.xsd">
  <persistence-unit name="BeansBindingSamplePU" transaction-type="RESOURCE_LOCAL">
    <provider>oracle.toplink.essentials.ejb.cmp3.EntityManagerFactoryProvider</provider>
    <class>apisample.PersonEntity</class>
    <properties>
      <property name="toplink.jdbc.user" value="sa"/>
      <property name="toplink.jdbc.password" value=""/>
      <property name="toplink.jdbc.url" value="jdbc:h2:tcp://localhost/bindingsample"/>
      <property name="toplink.jdbc.driver" value="org.h2.Driver"/>
      <property name="toplink.ddl-generation" value="create-tables"/>
    </properties>
  </persistence-unit>
</persistence>

PersonEntityを使ったSwingプログラムはリスト6のようにした。ここでは、まず最初にデータベースからすべてのPersonEntityオブジェクトをListとして取得し、それをBindingクラスを使用してJTableに関連付けている。各PersonEntityオブジェクトにはPropertyChangeListenerが追加してある。PropertyChangeListenerでは、プロパティが変更されたEntityManagerのmerge()メソッドを利用してデータベースに反映させるようになっている。

リスト6 BindingWithJPA.java - JPAとの組み合わせ

package apisample;

import java.beans.*;
import java.util.List;
import javax.beans.binding.Binding;
import javax.persistence.*;
import javax.swing.*;
import javax.swing.binding.SwingBindingSupport;

public class BindingWithJPA {
    private EntityManagerFactory factory;
    private EntityManager manager;

    public BindingWithJPA() {
        // エンティティマネージャを取得
        factory = Persistence.createEntityManagerFactory("BeansBindingSamplePU");
        manager = factory.createEntityManager();

        JFrame frame = new JFrame("BindingWithJPA");
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        JTable jTable = new JTable();

        // データベースからデータを取得してリストを初期化
        Query query = manager.createNativeQuery("select * from PersonEntity p order by p.id", PersonEntity.class);
        List results = (List)query.getResultList();
        PropertyChangeListener listener = new PersonPropertyChangeListener();
        for(PersonEntity person : results) {
            person.addPropertyChangeListener(listener);
        }

        // リストのPersonEntityオブジェクトとテーブルをバインディング
        Binding binding = new Binding(results, null, jTable, "elements");
        binding.addBinding("${id}", null, 
                      SwingBindingSupport.TableColumnParameter, 0);
        binding.addBinding("${firstName}", null, 
                      SwingBindingSupport.TableColumnParameter, 1);
        binding.addBinding("${lastName}", null, 
                      SwingBindingSupport.TableColumnParameter, 2);
        binding.bind();

        frame.getContentPane().add(jTable);
        frame.setSize(250, 150);
        frame.setVisible(true);
    }

    class PersonPropertyChangeListener implements PropertyChangeListener {
        public void propertyChange(PropertyChangeEvent e) {
            PersonEntity person = (PersonEntity)e.getSource();
            EntityTransaction transaction = manager.getTransaction();
            transaction.begin();
            manager.merge(person);
            transaction.commit();
        }
    }

    public static void main(String[] args) {
        BindingWithJPA sample = new BindingWithJPA();
    }
}

準備ができたら実際に実行してみよう。データベースにはPersonEntityクラスに対応した"PERSONENTITY"というテーブルを用意し、初期状態は図3のようにしておく。

図3 データベースの初期状態

この状態でBindingWithJPAを実行すると、最初は図4のように表示される。ここから、たとえば図5のようにデータを修正すれば、それがPersonEntityオブジェクトのプロパティに同期され、PropertyChangeListenerが呼び出されて図6のようにデータベースに反映される。

図4 BindingWithJPAの実行例1

図5 BindingWithJPAの実行例2 - データを修正する

図6 修正結果がデータベースに反映されている

JSR 295のエキスパートグループは現在JCPレビューのためのドラフトの作成を進めている。今回ひと足早く参照実装が公開されたのは、開発者からのフィードバックをドラフトに反映させるためだという。プロジェクトサイトではそのためのメーリングリストも設置されているため、何か気づいた点があればコンタクトを取ってみるといいだろう。