Fiverwoks Homepageへ

Java Web StartとXoneでリッチクライアントアプリを作る 1/3

初版 2004-09-15
沼倉 均
Numakura Hitoshi
|Page1|Page2|Page3|

■ はじめに

最近は企業内の業務アプリもWebアプリとして作ることが多いようです。たしかにアプリの配布の問題やバージョン管理の面倒はないし、クライアント側でもWebブラウザさえあればよいのでインストール作業も必要ありません。しかし、その反面ユーザインターフェイスはさみしいもので、レスポンスも悪く、たくさんのデータを入力するには向きません。開発する側もセッションの管理や「戻る」ボタンの対策など、面倒なことはたくさんあります。

これなら、以前のRDB+VBでクライアントサーバのシステムの方がましだった、ということもしばしばです。Webアプリでもブラウザをクライアントにするなら、入力項目の少ないアプリとか、検索系のアプリなどに限定し、それ以外はリッチクライアントを利用することを考えた方がよいのではと思います。

■ リッチクライアントアプリを作る

リッチクライアントアプリを実現するにはいくつか手段がありますが、サーバからクライアントすべてをJavaだけで開発できるというメリットから、ここではJava Web Start(以降JWS)を使って簡単なGUIアプリを作ってみたいと思います。JWSを使ったアプリは、配布やバージョン管理の面倒がなく、開発する側にとってはほとんど通常のJavaアプリを作るようなものです。ただし、クライアントにはJavaの実行環境をインストールしておいてもらう必要があります。

用意するのは、次のソフトウェアです。

  1. Java1.4以上
  2. Xone1.0.3以降(パーソナル版でも可)
  3. tomcat(ここでは4.1.30を使用)
  4. Apache-AXIS(ここでは1.1を使用)
  5. Xoneに対応するRDB(MySQL,PostgreSQL,oracle,SQL Server,DB2)
最初はXoneとRDBだけで作ります。これらをそれぞれインストールします。特にRDBは必ず日本語が使えるように設定しておいてください。RDB用のJDBCドライバも必要です。また、Xoneのルータ設定はデフォルトであるDirectで行います。

GUIアプリとしては、クライアント側のテキストファイルをRDBに保存したり、読み込みができ、テキストの編集ができるものにします。手順は次のようになります。

1.Xoneのクラスを定義する

今回は、テキストファイルをXoneのオブジェクトとして格納します。そのために、まずXoneのクラスを定義します(Anyインスタンスでもよいのですが、今回はクラスとインスタンスで作ります)。

その前に、クラスを入れるフォルダと、インスタンスを入れるフォルダを作っておきます。これらはライフサイクルも違うし、アクセス制御をすることもあるので別のフォルダにしておきます。フォルダの作成はXoneに附属するモデルマネージャで、rootの下にclassesとinstancesという名前で作っておきます。

オブジェクトには、ファイルの本体だけでなく、ファイルのメタ情報(ファイル名など)も格納します。メタ情報はjava.io.Fileで取得できるものを使います。クラスを定義するには、次の方法があります。

  1. モデルマネージャに附属のオブジェクトエディタを使う
  2. プログラムで定義する
  3. XMLファイルで定義しておき、それをモデルマネージャでインポートするか、あるいはプログラムで書き込む
aの方法はもっとも手軽ですが、GUIで一つ一つ要素を定義していくので、定義内容が簡単な場合に便利でしょう。bの方法は、クラスをダイナミックに生成したいときに利用します。cの方法は、たくさんのクラスを定義したり、複数の人がクラス定義をレビューするときなど、最初からXML形式で作ってしまう方がよいでしょう。DTDファイルが添付されていますから、DTDを利用できるXMLエディタではより簡単に編集できると思います。

今回はモデルマネージャ(オブジェクトエディタ)で定義しましたが、それをXML形式でエクスポートとしたものが次のリストです。

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE xoneObjectList SYSTEM "http://www.fiverworks.com/xone/dtd/xoneObjectList.dtd">
<xoneObjectList xmlns="http://www.fiverworks.com/xoneObjectList" ver="1.0">
  <xoneObject name="TextFile" type="XoneClass">
    <property>
      <hint />
    </property>
    <xoneElement name="name" type="string">~null;</xoneElement>
    <xoneElement name="parent" type="string">~null;</xoneElement>
    <xoneElement name="lastModified" type="timestamp">~null;</xoneElement>
    <xoneElement name="length" type="long">~null;</xoneElement>
    <xoneElement name="body" type="string">~null;</xoneElement>
    <xoneElementList name="_restrictionList" />
    <xoneElementList name="_restrictionMessageList" />
  </xoneObject>
</xoneObjectList>
上記のリスト中の「~null;」は値がnullであることを示しています。上記をコピーしてXMLファイルにし、そのファイルをモデルマネージャでclassesフォルダにインポートします。これで、TextFileというクラスが定義できます。

2.アプリを作る

GUIアプリですから、SwingをビジュアルにデザインできるIDEで作りました(IDEはNetBeansを使用。そのため変数宣言は後ろにあります)。プログラムは次のようになります(ソースはこちら)。

  1  /*
  2   * TextEditor.java
  3   *
  4   * 
  5   */
  6  
  7  package xone_sample.jws;
  8  
  9  import com.fiverworks.xone.model.XoneClass;
 10  import com.fiverworks.xone.model.XoneInstance;
 11  import com.fiverworks.xone.model.XoneModel;
 12  import com.fiverworks.xone.model.XoneNode;
 13  import com.fiverworks.xone.model.XoneObject;
 14  import com.fiverworks.xone.model.XoneUser;
 15  
 16  import com.fiverworks.xone.mw.MwMain;
 17  import com.fiverworks.xone.tools.man.ObjectChooser;
 18  
 19  import java.io.BufferedReader;
 20  import java.io.BufferedWriter;
 21  import java.io.File;
 22  import java.io.FileReader;
 23  import java.io.FileWriter;
 24  
 25  import java.sql.Timestamp;
 26  
 27  import javax.swing.JFileChooser;
 28  import javax.swing.JFrame;
 29  
 30  
 31  public class TextEditor extends JFrame {
 32     
 33      /** Creates new form TextEditor */
 34      public TextEditor() {
 35          initComponents();
 36      }
 37     
 38    /** This method is called from within the constructor to
 39     * initialize the form.
 40     * WARNING: Do NOT modify this code. The content of this method is
 41     * always regenerated by the Form Editor.
 42     */
 43      private void initComponents() {
 44          jScrollPane1 = new javax.swing.JScrollPane();
 45          textArea = new javax.swing.JTextArea();
 46          menuBar = new javax.swing.JMenuBar();
 47          fileMenu = new javax.swing.JMenu();
 48          openMenuItem = new javax.swing.JMenuItem();
 49          saveMenuItem = new javax.swing.JMenuItem();
 50          loadDBMenuItem = new javax.swing.JMenuItem();
 51          saveDBMenuItem = new javax.swing.JMenuItem();
 52          exitMenuItem = new javax.swing.JMenuItem();
 53  
 54          setTitle("TextEditor");
 55          addWindowListener(new java.awt.event.WindowAdapter() {
 56              public void windowClosing(java.awt.event.WindowEvent evt) {
 57                  exitForm(evt);
 58              }
 59          });
 60  
 61          jScrollPane1.setViewportView(textArea);
 62  
 63          getContentPane().add(jScrollPane1, java.awt.BorderLayout.CENTER);
 64  
 65          fileMenu.setText("\u30d5\u30a1\u30a4\u30eb");
 66          fileMenu.setFont(new java.awt.Font("Dialog", 0, 12));
 67          openMenuItem.setFont(new java.awt.Font("Dialog", 0, 12));
 68          openMenuItem.setText("\u958b\u304f");
 69          openMenuItem.addActionListener(new java.awt.event.ActionListener() {
 70              public void actionPerformed(java.awt.event.ActionEvent evt) {
 71                  openMenuItemActionPerformed(evt);
 72              }
 73          });
 74  
 75          fileMenu.add(openMenuItem);
 76  
 77          saveMenuItem.setFont(new java.awt.Font("Dialog", 0, 12));
 78          saveMenuItem.setText("\u4fdd\u5b58");
 79          saveMenuItem.addActionListener(new java.awt.event.ActionListener() {
 80              public void actionPerformed(java.awt.event.ActionEvent evt) {
 81                  saveMenuItemActionPerformed(evt);
 82              }
 83          });
 84  
 85          fileMenu.add(saveMenuItem);
 86  
 87          loadDBMenuItem.setFont(new java.awt.Font("Dialog", 0, 12));
 88          loadDBMenuItem.setText("\u30b5\u30fc\u30d0\u304b\u3089\u8aad\u307f\u8fbc\u307f");
 89          loadDBMenuItem.addActionListener(new java.awt.event.ActionListener() {
 90              public void actionPerformed(java.awt.event.ActionEvent evt) {
 91                  loadDBMenuItemActionPerformed(evt);
 92              }
 93          });
 94  
 95          fileMenu.add(loadDBMenuItem);
 96  
 97          saveDBMenuItem.setFont(new java.awt.Font("Dialog", 0, 12));
 98          saveDBMenuItem.setText("\u30b5\u30fc\u30d0\u306b\u4fdd\u5b58");
 99          saveDBMenuItem.addActionListener(new java.awt.event.ActionListener() {
100              public void actionPerformed(java.awt.event.ActionEvent evt) {
101                  saveDBMenuItemActionPerformed(evt);
102              }
103          });
104  
105          fileMenu.add(saveDBMenuItem);
106  
107          exitMenuItem.setFont(new java.awt.Font("Dialog", 0, 12));
108          exitMenuItem.setText("\u7d42\u4e86");
109          exitMenuItem.addActionListener(new java.awt.event.ActionListener() {
110              public void actionPerformed(java.awt.event.ActionEvent evt) {
111                  exitMenuItemActionPerformed(evt);
112              }
113          });
114  
115          fileMenu.add(exitMenuItem);
116  
117          menuBar.add(fileMenu);
118  
119          setJMenuBar(menuBar);
120  
121          pack();
122      }
123  
124    private void saveDBMenuItemActionPerformed(java.awt.event.ActionEvent evt) {
125      // 処理コードをここに追加します:
126      try {
127        int result = this.oc.showSaveDialog();
128        if (result == ObjectChooser.APPROVE) {
129          String objPath = oc.getSelectedObject();
130          String objName = XoneNode.getLastName(objPath);
131          String objParent = XoneNode.getParent(objPath);
132          // Xoneのインスタンスを作る
133          XoneInstance xi = XoneModel.newXoneInstance(this.textFileClass, objName);
134          xi.setElementValue("name", this.currentFileName);
135          xi.setElementValue("parent", this.currentFileParent);
136          Timestamp ts = new Timestamp(System.currentTimeMillis());
137          xi.setElementValue("lastModified", ts.toString());
138          xi.setElementValue("length", Integer.toString(this.textArea.getText().length()));
139          xi.setElementValue("body", this.textArea.getText());
140          
141          int res = mwMain.save(objParent, new XoneObject[]{xi}, oc.getWriteMode());
142        }
143      } catch (Exception ex) {
144        ex.printStackTrace();
145        return;
146      }
147    }
148  
149    private void saveMenuItemActionPerformed(java.awt.event.ActionEvent evt) {
150      // 処理コードをここに追加します:
151      int result = jfc.showSaveDialog(this);
152      if (result == JFileChooser.APPROVE_OPTION) {
153        File file = jfc.getSelectedFile();
154        BufferedWriter out = null;
155        try {
156          out = new BufferedWriter(new FileWriter(file));
157          out.write(this.textArea.getText());
158          this.currentFileName = file.getName();
159          this.currentFileParent = file.getParent();
160        } catch (Exception ex) {
161          ex.printStackTrace();
162          return;
163        } finally {
164          if (out != null) {
165            try {
166              out.close();
167            } catch (Exception ex) {}
168          }
169        }
170      }
171    }
172  
173    private void openMenuItemActionPerformed(java.awt.event.ActionEvent evt) {
174      // 処理コードをここに追加します:
175      int result = jfc.showOpenDialog(this);
176      if (result == JFileChooser.APPROVE_OPTION) {
177        File file = jfc.getSelectedFile();
178        BufferedReader in = null;
179        try {
180          in = new BufferedReader(new FileReader(file));
181          this.textArea.setText("");
182          String str = in.readLine();
183          while (str != null) {
184            this.textArea.append(str + "\n");
185            str = in.readLine();
186          }
187          this.currentFileName = file.getName();
188          this.currentFileParent = file.getParent();
189        } catch (Exception ex) {
190          ex.printStackTrace();
191          return;
192        } finally {
193          if (in != null) {
194            try {
195              in.close();
196            } catch (Exception ex) {}
197          }
198        }
199      }
200    }
201  
202    private void loadDBMenuItemActionPerformed(java.awt.event.ActionEvent evt) {
203      // 処理コードをここに追加します:
204      try {
205        int result = this.oc.showOpenDialog(false);
206        if (result == ObjectChooser.APPROVE) {
207          String objName = oc.getSelectedObject();
208          XoneObject[] xos = mwMain.load(new String[]{objName});
209          if (xos == null || xos.length != 1) return;
210          XoneObject xo = xos[0];
211          if (!xo.getClassName().equals(TEXT_FILE_CLASS)) return;
212          this.textArea.setText(xo.getElementValue("body"));
213          this.currentFileName = xo.getElementValue("name");
214          this.currentFileParent = xo.getElementValue("parent");
215        }
216      } catch (Exception ex) {
217        ex.printStackTrace();
218        return;
219      }
220    }
221    
222    private void exitMenuItemActionPerformed(java.awt.event.ActionEvent evt) {
223      exit();
224    }
225    
226    /** Exit the Application */
227    private void exitForm(java.awt.event.WindowEvent evt) {
228      exit();
229    }
230    
231    // logout
232    private void exit() {
233      if (mwMain.isLogin()) mwMain.logout();
234      System.exit(0);
235    }
236    
237    // override
238    public void show() {
239      super.setSize(500, 400);
240  
241      mwMain = new MwMain();
242      try {
243        XoneUser xu = mwMain.showLoginDialog(this);
244      } catch (Exception ex) {
245        ex.printStackTrace();
246        System.exit(0);
247      }
248      // loginしていなければ終了
249      if (!mwMain.isLogin()) System.exit(0);
250      // TextFileクラスを読み込んでおく
251      try {
252        XoneObject[] xos = mwMain.load(new String[]{TEXT_FILE_CLASS});
253        if (xos == null || xos.length != 1) exit();
254        this.textFileClass = (XoneClass)xos[0];
255        // 上の行は下でもよい
256        // this.textFileClass = XoneModel.getXoneClass(TEXT_FILE_CLASS);
257      } catch (Exception ex) {
258        ex.printStackTrace();
259        this.exit();
260      }
261      
262      // ObjectChooserとJFileChooserの準備
263      oc = new ObjectChooser(this, mwMain);
264      oc.setCurrentFolder(INSTANCES_FOLDER);
265      jfc = new JFileChooser();
266      jfc.setMultiSelectionEnabled(false);
267      
268      super.show(); 
269    }
270    
271    /**
272     * @param args the command line arguments
273     */
274    public static void main(String args[]) {
275      new TextEditor().show();
276    }
277    
278      // 変数宣言 - 編集不可
279      private javax.swing.JMenuItem exitMenuItem;
280      private javax.swing.JMenu fileMenu;
281      private javax.swing.JScrollPane jScrollPane1;
282      private javax.swing.JMenuItem loadDBMenuItem;
283      private javax.swing.JMenuBar menuBar;
284      private javax.swing.JMenuItem openMenuItem;
285      private javax.swing.JMenuItem saveDBMenuItem;
286      private javax.swing.JMenuItem saveMenuItem;
287      private javax.swing.JTextArea textArea;
288      // 変数宣言の終わり
289  
290    private MwMain mwMain;
291    private ObjectChooser oc;
292    private XoneClass textFileClass;
293    
294    private JFileChooser jfc;
295    private String currentFileName;
296    private String currentFileParent;
297    
298    private static final String TEXT_FILE_CLASS = "root/classes/TextFile";
299    private static final String INSTANCES_FOLDER = "root/instances";
300  }

実行すると、最初にログインダイアログがあらわれ、ログインに成功すると使えるようになります(図は実行中のスクリーンショット)。



実行してみればわかりますが、メニューは「ファイル」だけであり、テキストエディタといっても単にJTextAreaにテキストを貼り付けているだけ、というこれ以上はないくらいシンプルなものです(^^;)。

ただ、JDBCを使ってデータを保存するより、ずいぶん簡単になっていることがわかると思います。このプログラムで留意してほしいのは、次の2点です。

・アプリで使うクラスは最初に用意しておく(250〜260行)
・必ずlogoutしてから終了する(232行のexitメソッド)

クラスは必要になったときにロードすると、ユーザに少々待たせることになることもあるので、できれば最初にロードしておくといいでしょう。また、255行のコメントにあるように、クラスはロードするとキャッシュ(com.fiverworks.xone.mw.Cache)に保存されるので、必要なときにキャッシュからクラスを取り出すこともできます。このXoneModel.getXoneClass(TEXT_FILE_CLASS)は、Cache.getInstance().getXoneClass(TEXT_FILE_CLASS)と等価です。

■ 最後に

今回のように、Xoneの1つのオブジェクトに1つのファイルを格納する、ということをすべてのファイルタイプに対して行うと、モデルマネージャはファイルマネージャとほぼ同等となります。そして、それをWebサービスを使って利用した場合、WebDAVと同じようなことができてしまいます。Xoneの場合、フォルダ単位でのアクセス制御やフォルダ/オブジェクトのロックができますから、より高度なデータの共有環境ができると思います(アクセス制御やロックはスタンダード版のみの機能です)。どなたか、作ってみます?

次回は、ここで作成したGUIアプリをWebサービスで実行するための環境を構築します。

mailto:hnumakura@fiverworks.com

次ページ|ページTOP|サンプルTOP|

『Java』は米国および、その他の国における米国Sun Microsystems, Inc.の商標です。
その他本文に記載されている会社名、商品名は、各社の登録商標または商標です。
| 会社案内 | 著作権 | 免責事項 | 通信販売法規に基づく表記 | プライバシーポリシー | お問い合せ | サイトマップ |
COPYRIGHT(C) 2004 Fiverworks Ltd. ALL RIGHTS RESEREVED.