フィードリーダー

著者: Rich Unger 氏
この記事は、Win With NetBeans コンテストの一部として投稿されたものです。

NetBeans プラットフォームのサンプルとチュートリアル

フィードリーダーの機能

フィードリーダーとは、Mozilla Firefox の Sage プラグインに基づいて作成された、基本的な RSS/Atom フィードブラウザのことです。その構成要素は次のとおりです。

  • フィード (RSS/RDF/Atom の記述子ファイルへの URL) のリスト

  • 各フィードからのヘッドラインのリスト

  • ブラウザウィンドウ (Mozilla が JFrame に埋め込まれたもの)

Mozilla が埋め込まれるフレームは、JNI 呼び出しを使用する JDIC ライブラリで提供されています。この例は、Linux 版と Windows 版の両方の JDIC バイナリを使用するように設定されています。

このチュートリアルの対象読者

主な読者層はもちろん、NetBeans プラットフォーム上でアプリケーションを構築したいと思っている方々です。筆者がどのようにしてフィードリーダーを作成したか、また、マニフェスト、レイヤーファイル、および Java ソースの各行で何が行われているのかなど、特に少し変わったことを行う必要があった箇所で、詳しく解説していきます。

それが、このチュートリアルを執筆した 2 つ目の理由です。問題点と不自然な回避策には、「問題点ボックス」を付加しています。

これを解決するには、変わったことをする必要があります。

そうしたすべての場合について、IssueZilla の対応する未解決の問題へのリンクを示します。このドキュメントが更新され続け、これらの問題点ボックスが時間の経過とともに削除され、より簡単にアプリケーションを作成できるプラットフォームが実現されることを期待しています。

はじめに

完全なソースコードを入手できます。

このチュートリアルでは、特定の IDE を想定することはしません。むしろ、テキストエディタ、JDK 1.5.0 (1.4.x ではバグのためにこのサンプルのコンパイルが失敗するが、1.4.x でもこのサンプルの実行は可能)、および Apache Ant 1.6.2 以上があるだけとしましょう。コマンド行の例を単純化できるように、Bash (UNIX または Cygwin) を想定します。必要に応じて Windows のコマンド行に読み替えてください。それでもうまく動作するはずです。

筆者は単に、NetBeans IDE や NetBeans プラットフォームのことを説明して読者を混乱させたくないだけです。まずこのチュートリアルに従ってください。モジュールがどのように動作するのかを理解してください。そのあとで、それらの構築を少しだけ容易にしてくれるツール (NetBeans IDE など) について検討してください。

作業を始めるにあたって最初に必要なものは、NetBeans プラットフォームのコピーです。それをソースコードから構築する必要はありません。単にバイナリリリースをダウンロードし、それを任意の場所に解凍するだけです。ここでは /home/rich/netbeans 内に配置しました。

次に、最初の手掛かりとなる何らかのサンプルソースコードを手元に置いておくと便利です。筆者は開始時にクラスタ構築ハーネスを使用しました。これは、モジュールの構築やパッケージ化を行うための Ant 構築スクリプトをすでに備えているほか、「snipe」という名前のスターターモジュールも含んでいるので、便利です。もちろん、筆者がこの記事を執筆したので、読者はフィードリーダーのソースツリーを使って同じくらい容易にアプリケーションを開始できます。

ここでは、ハーネスを /home/rich/src/rss に解凍しました。

最後に、構築スクリプトの設定を行うことで、構築スクリプトが NetBeans プラットフォームインストールをどこで検索すればよいかわかるようにする必要があります。

/home/rich/src/rss/nbbuild/user.build.properties の数行を変更します。

netbeans.dest.dir=/home/rich/netbeans
clustername=rssreader1

慣例ではクラスタの末尾にバージョン番号を付けるようになっていますが、これは厳格に要求されているわけではありません。

定義: クラスタとは、モジュールや関連するリソースファイルの集合のことです。NetBeans インストールは一連のクラスタで構成することが可能ですが、それらのクラスタは、NetBeans の起動時に選択されます。たとえば、NetBeans IDE は platform4、ide4、nb4.0、extra の各クラスタを実行します。クラスタの背後にある考え方は、NetBeans プラットフォームを 1 つインストールするだけで、多数のブランドアプリケーションが同じクラスタを共有できるようにする、というものです。たとえば、rssreader1 クラスタを 1 つの Netbeans IDE インストールにインストールし、異なる 2 つの起動スクリプトを実行することによって、IDE とフィードリーダーが、platform4 クラスタを共有しつつも、それ以外の点は完全に独立したアプリケーションとなるようにすることができます。

次に、user.build.properties で指定したばかりのクラスタ名と同期が取れるように、/home/rich/src/rss/nbbuild/user.cluster.properties の数行を変更します。

user.cluster=cluster.rssreader1
cluster.rssreader1.dir=rssreader1
cluster.rssreader1= snipe

この時点で、nbbuild/ ディレクトリで ant を実行すると、snipe モジュールを含むディレクトリ /home/rich/netbeans/rssreader1 が得られます。もちろん、snipe モジュールは不要なので、ant clean と入力してこれを削除します。

次に、必要なモジュールのことに話を進めます。

ライブラリモジュール

フィードリーダーアプリケーションの全体を単一のモジュールにまとめることも可能です。そうすると、モジュール性があまり高くなくなります。フィードリーダーはたまたま、ライブラリ JDOM、Rome、および JDIC を必要としています。これらのライブラリを使用する可能性のあるほかのモジュールを使ってこのアプリケーションを拡張する際には、フィードリーダー全体に依存するのではなく、そのライブラリモジュールのみに依存することをお勧めします。また、ライブラリモジュールの自動読み込みを有効にすることもできます。

定義: 自動読み込みモジュールとは、(別のモジュールから) 要求された時点で NetBeans によって自動的に読み込まれるモジュールのことです。それまでは、実行時にメモリーが占有されることはありません。

ソースツリーへのモジュールの追加

新しいモジュールを追加するたびに、そのことを構築ハーネスに知らせる必要があります。2 つのファイルを変更します。

/home/rich/src/rss/nbbuild/user.cluster.properties:

cluster.rssreader1=snipe, \
                   anothermodule, \
                   yetanother

これは、クラスタに含まれるモジュールを構築スクリプトに通知することで、クラスタを 1 つの単位として構築する方法や結果となる JAR ファイルのインストール先ディレクトリを構築スクリプトが判断できるようにします。

/home/rich/src/rss/nbbuild/modules.xml:

<module>
  <path>snipe</path>
  <cnb>org.netbeans.modules.snipe</cnb>
</module>

これは、モジュールのソースコードを含むディレクトリの名前を、NetBeans が実行時にモジュールを認識する際に使用する名前 (「cnb」は code-name-base を表します) にマップします。

JDOM

ここでは、JDOM が何であるかをほとんどの読者が知っているものと仮定します。これは XML 解析 API の 1 つですが、フィードリーダーでこれが必要となる唯一の理由は、Rome ライブラリで使用されるからです。

まず、「jdom」をクラスタリストに追加し、

<module>
  <path>jdom</path>
  <cnb>org.jdom.api</cnb>
</module>

モジュールリストに追加します。続いて、JDOM モジュールのファイルリストを次に示します。

build.xml
manifest.mf
nbproject/project.xml
nbproject/project.properties
lib/jdom.jar
src/org/jdom/api/Bundle.properties

これらを一度に 1 つずつ取り上げましょう。

build.xml: 構築ファイル内ではまず、projectized.xml という名前の別の構築ファイルがインポートされます。ほとんどの場合、モジュールの構築ファイルに含める必要があるのは、それだけです。ただし、JDOM モジュールでは、モジュール自体の JAR のほかに jdom.jar も、モジュールのパッケージングに含める必要があります。

問題点 #1: 追加のライブラリを含める際に、構築動作を上書きしないですむようにすべきです。project.xml ファイル内でそのような依存性を宣言すれば、対応するマニフェストエントリを作成して nbm ファイル内に含める方法を構築スクリプトが認識できるようにすべきです。

https://netbeans.org/bugzilla/show_bug.cgi?id=52354

したがって、2 つのターゲットを上書きします。「files-init」ターゲットは、次の行が追加されている点を除けば、projectized.xml 内のターゲットとまったく同じです。

<include name="${nb.modules.dir}/ext/jdom.jar"/>

「files-init」ターゲットは、モジュールに属するファイルを構築スクリプトが認識できるようにします。これらは、「clean」ターゲットの実行時に削除されるファイルです。規約上、モジュールでない JAR は ext/ ディレクトリに格納することになっています。

上書きする必要のあるもう 1 つのターゲットは、「netbeans-extra」です。これは、配備プロセスの一部としてファイルコピーなどを行うための場所として、構築スクリプトによって提供されているフックです。

<target name="netbeans-extra" depends="init">
    <mkdir dir="${netbeans.dest.dir}/${cluster.dir}/${nb.modules.dir}/ext"/>
    <copy todir="${netbeans.dest.dir}/${cluster.dir}/${nb.modules.dir}/ext">
      <fileset dir="lib">
        <include name="jdom.jar"/>
      </fileset>
    </copy>
</target>

これは、jdom.jar ファイルを /home/rich/netbeans/rssreader1/modules/ext にコピーします。

manifest.mf: JAR ファイルは必ずマニフェストを必要とします。NetBeans モジュールとは単に、少なくとも次の 2 つの行がマニフェストに含まれている JAR ファイルのことです。

OpenIDE-Module: org.jdom.api/1
OpenIDE-Module-Specification-Version: 1.0

1 行目は単なるモジュールの名前であり、必要に応じてリリースバージョンが付加されます。この名前は、modules.xml ファイルの <cnb> (code-name-base) に一致します。

2 行目は、モジュールの仕様バージョンです。

バージョン番号について一言... モジュールは異なる 3 つのバージョン番号を持つことができます。リリースバージョン仕様バージョン、および実装バージョンです。
たとえば、モジュール A のリリースバージョンは 1、仕様バージョンは 2.0、実装バージョンは beta3 であるとします。このとき、モジュール B で、モジュール A への依存性を (project.xml ファイル内で) 宣言します。リリースバージョン 1 は指定する必要があります。仕様バージョン 2.0 への依存性は必要に応じて指定できます。指定した場合、A の作者がバージョン 2.1 をリリースしても問題は生じず、依存性は引き続き機能します。規約上、モジュール A によって公開された公開 API クラス (project.xml の <public-packages> 要素を参照) は、互換性を破らないことになっています。仕様バージョンへの依存性は、それら API クラスへのアクセス権のみを B に与えます。
モジュール B で実装バージョン beta3 への依存性を指定した場合、B はそのバージョンのモジュール A でのみ動作します。ただしそれは、モジュール A のすべての公開クラスにアクセスすることができます。(モジュールの実行時に NoClassDefFoundException が発生する場合は、実装依存を指定せずに非 API クラスにアクセスしようとしていることが原因である可能性があります。)

マニフェスト内の残りの行は、あと 2 行だけです。

OpenIDE-Module-Localizing-Bundle: org/jdom/api/Bundle.properties
Class-Path: ext/jdom.jar

1 行目は完全に省略可能です。これは、追加のマニフェストエントリを含むバンドルを指します。それらのマニフェストエントリはすべて、このモジュールの表示名や説明など、ローカライズ可能な文字列です。

2 行目は標準のマニフェストエントリ Class-Path であり、モジュールの JAR ファイルのクラスパスに jdom.jar ファイルを含めます。パス「ext/jdom.jar」は、build.xml ファイルでの jdom.jar のコピー先と一致します。

問題点 #2: Class-Path を指定しないで済むようにすべきです。これは自動生成されるべきです (問題点 #1 を参照)。

nbproject/project.xml: これは、依存性宣言とクラスパスの生成方法を構築スクリプト (および IDE を使用している場合は IDE) に指示するファイルです。ここでも、見慣れた情報 <code-name-base> と <path> を指定する必要があります。次に、ほかのモジュールへの依存をすべて指定する必要があります。ここで、モジュール自体には実際のコードは含まれておらず、jdom.jar はコア JRE クラスの外側のコードを一切必要としないので、宣言すべきモジュールの依存性は実際に存在しません。それでも、コア OpenAPI クラスへの依存性は常に宣言すべきです。

<dependency>
  <code-name-base>org.openide</code-name-base>
    <build-prerequisite/>
    <compile-dependency/>
    <run-dependency>
      <release-version>1</release-version>
      <specification-version>4.5</specification-version>
  </run-dependency>
</dependency>

これはある意味で特殊な場合です。最近の仕様バージョンを使ってこれを指定しなかった場合、NetBeans はこれを古いモジュールとみなし、実行時に下位互換性のために一連の依存関係を自動的に読み込みます。

次に、モジュールの <public-packages> を宣言する必要があります。これには 2 つの目的があります。公開パッケージは、このモジュールへの依存を宣言したほかのモジュールから表示可能であるほか、「javadoc」Ant ターゲットの実行時に Javadoc が生成される一連のパッケージを構成します。

各パッケージを個別に指定できます。

<public-packages>
  <package>org.jdom</package>
  <package>org.jdom.adapters</package>
  <package>org.jdom.input</package>
  ...
</public-packages>

あるいは、ツリー全体を 1 行で取得することもできます。

<public-packages>
  <subpackages>org.jdom</subpackages>
</public-packages>

ただし、この方法は「javadoc」ターゲットでは使えません。

project.properties: 構築プロセスに対するいくつかの追加のヒント...

is.autoload=true
cp.extra=lib/jdom.jar
module.javadoc.packages=org.jdom

1 行目は、このモジュールを自動読み込みモジュールにします。2 行目は、コンパイルのクラスパスの末尾に追加されます。3 行目が必要となるのは、project.xml 内で <subpackages> の方法を使って <public-packages> を宣言した場合です。

問題点 #3: ここでは、module.javadoc.packages の指定は必須でないようにすべきです。構築スクリプトは、ユーザーがこのモジュールの Javadoc には興味を持っていないとみなし、正常に引き下がるべきです。今は、これを指定しないと構築が失敗します。

https://netbeans.org/bugzilla/show_bug.cgi?id=52135

* 更新: NetBeans 4.1 で修正されました

Rome

Rome

Rome ライブラリは、RSS および Atom フィードを読み取ります (ただし、API は非常に単純である)。Rome モジュールと JDOM モジュールの違いは 2 点だけです。Rome モジュールには 1 つではなく 2 つの JAR ファイル (rome-0.4.jar と rome-fetcher-0.4.jar) がバンドルされており、その project.xml 内では JDOM モジュールへの依存性が宣言されています。

<dependency>
  <code-name-base>org.jdom.api</code-name-base>
  <build-prerequisite/>
  <compile-dependency/>
  <run-dependency>
    <release-version>1</release-version>
    <specification-version>1.0</specification-version>
  </run-dependency>
</dependency>

JDIC

JDIC ライブラリは、Java プログラムが、ブラウザやメーラー、システムトレイ、MIME タイプレジストリといった特定のネイティブデスクトップ機能を活用できるようにします。フィードリーダーは、IE または Mozilla レンダリングエンジンを使って JFrame 内に Web ページをレンダリングする目的で、埋め込みネイティブブラウザコンポーネントを使用します。

JDIC はこれを実現するため、その共有ライブラリ (jdic.dll または libjdic.so) に対して Java Native Interface (JNI) 呼び出しを行うとともに、ネイティブの実行可能ファイル (IeEmbed.exe または mozembed-linux-gtk2) を実行します。これらのライブラリや実行可能ファイルが実行時に NetBeans から使用可能となるようにするには、モジュールの構築ファイル内で若干の設定を行う必要があります。

「files-init」タスク内で一連のファイルが宣言されています。

<include name="${nb.modules.dir}/ext/jdic.jar"/>
<include name="lib/libjdic.so"/>
<include name="lib/libmozembed-linux-gtk1.2.so"/>
<include name="lib/libmozembed-linux-gtk2.so"/>
<include name="lib/mozembed-linux-gtk1.2"/>
<include name="lib/mozembed-linux-gtk2"/>
<include name="lib/jdic.dll"/>
<include name="lib/IeEmbed.exe"/>
<include name="lib/nspr4.dll"/>
<include name="bin/${shell.script}"/>
<include name="bin/${batch.script}"/>

見慣れた JAR ファイル宣言のほかに、lib/ 内の一連のネイティブライブラリや実行可能ファイル、および 1 対のスクリプトが存在します。この特定のディレクトリ内にネイティブ情報を配置するという決定は多少任意的なものですが、これが、アーキテクチャードキュメントでの推奨の場所となります。この場所を把握しておく必要があるのは、ほかには起動スクリプトだけです。

構築ファイルの shellscript ターゲットによってシェルスクリプトとバッチスクリプトが生成されます。これらのスクリプトは、NetBeans プラットフォームを rssreader1 クラスタで起動し、JDIC バイナリを適切なパスで起動します。

フィードリーダーモジュール

これで、必要なライブラリモジュールがすべて得られたので、実際に何かを行うモジュールを作成する準備が整ったことになります。

まず、モジュールの構築スクリプトと宣言要素を作成します。build.xml ファイルは、標準の「projectized.xml」に対するインポート行を除けば、幸いなことに空になっています。nbproject/project.xml ファイルでは、単純に 3 つのライブラリモジュールへの依存が宣言されており、公開パッケージは 1 つもありません (このモジュールはほかのモジュールに API を提供しません)。nbproject/project.properties ファイルも空です。manifest.mf ファイルもごく普通に見えるはずです。ここでの新しい要素は 1 つだけです。

OpenIDE-Module-Layer: org/netbeans/modules/feedreader/resources/layer.xml

ライブラリモジュールの場合と異なり、「システムファイルシステム」に含めるための、フィードリーダーのユーザーインタフェースに関する情報を、レイヤーファイル内に宣言します。

定義: システムファイルシステムとは、NetBeans が、すべてのシステム設定、GUI レイアウト情報、アクション、テンプレート、およびその他の、NetBeans インストールの状態維持に必要なあらゆるものを格納する場所のことです。これは、各クラスタの「config」ディレクトリの下や NetBeans ユーザーディレクトリの「config」ディレクトリの下に格納された実際のファイル、およびモジュールのレイヤーファイル内で宣言された仮想的な「ファイル」から構成されます。

レイヤーファイル

最初のエントリは、Actions という名前の最上位フォルダの下にあります。これは、javax.swing.Action の実装のためのリポジトリです。これらはメニュー項目、ツールバーボタン、およびキーボードショートカットで使用できます。このエントリを追加すると、メニューから「ツール」>「キーボードショートカット」を選択して VewFeedsAction にキーボードショートカットを割り当てることができます。

2 番目のエントリは、ViewFeedsAction を「表示」メニューの下に配置します。シャドウファイル構文に注意してください。これは、UNIX のソフトリンクや Windows のショートカットを使用するのに似ています。

残りのエントリは、Windows2 最上位フォルダの下に収まっています。このフォルダはプラットフォームに対し、インスタンス化すべき TopComponent の種類とそれらの配置先に関する情報を提供します。(なお、これが「Windows2」と呼ばれているのは、「Windows」が 3.6 より前の古いウィンドウシステムであり、その古いシステムが下位互換のために維持されているからです。) フィードリーダーに含まれる TopComponent 定義は 2 つあります。SiteListComponent と EntryListComponent です。後者は、デフォルトの場所 (中央) で開かれるように設計されているため、この動作を上書きする情報はレイヤーファイル内に一切必要ありません。

これに対し、SiteListComponent は左側で合体するように、つまり「explorer」モードで合体するように設計されています。したがって、エントリを追加します。

<folder name="Modes">
    <folder name="explorer">            
        <file name="rss_list.wstcref" url="feedList.wstcref"/>
    </folder>
</folder>

これは、ID が「rss_list」の TopComponent はデフォルトで「explorer」モードで合体することを宣言しています。(なお、拡張子「wstcref」は Window System Top Component REFerence を表します。) 宣言されたモード内での「rss_list」の配置は、ファイル feedList.wstcref から派生したものです。

<tc-ref version="2.0">
    <module name="org.netbeans.modules.feedreader/1" spec="1.0" />
    <tc-id id="rss_list" />
    <state opened="true" />
</tc-ref>

<tc-id id> の値は、レイヤー内で宣言されたファイルのベース名 (「rss_list」) と一致する必要があります。また、「.settings」拡張子を持つ同じ名前が、Windows2/Components フォルダ内にも存在する必要があります。

<folder name="Components">
    <file name="rss_list.settings" url="feedList.settings" />
</folder>

この設定ファイルの内容は、「rss_list」をインスタンス化する方法を NetBeans に指示します。このファイルの内容を手動で記述してもかまいませんが、このレイヤーファイルの Windows2 セクションの全体をコメントアウトして NetBeans を起動し、ViewFeedsAction を実行して SiteListComponent をインスタンス化し、$userdir/config/Windows2Local/Component 内に自動生成された設定ファイルを取り出すこともできます。続いて、<serialdata> セクションを次で置き換えます。

<instance class="org.netbeans.modules.feedreader.SiteListComponent"/>

この場合、SiteListComponent のデフォルトコンストラクタを使ってコンポーネントが作成されます。クラスで静的なファクトリメソッドが必要な場合は、メソッド属性を追加できます。

<instance class="org.netbeans.modules.feedreader.SiteListComponent" method=”makeSiteListComponent”/>

ViewFeedsAction.java

これで、残りの作業は、実際の Java コードだけになりました。レイヤーファイル内で宣言されている最初のクラスは、ViewFeedsAction.java です。これは、javax.swing.Action のシングルトン実装である CallableSystemAction の単純なサブクラスです。

    public void performAction() {
        SiteListComponent.activate();
    }

このアクションを実行すると、SiteListComponent のシングルトンインスタンスが開かれ、そのインスタンスにフォーカスが移ります (後述の SiteListComponent の実装を参照)。

    public String getName() {
        return NbBundle.getMessage(SiteListComponent.class, "SLC_title");
    }

このアクションの名前は Bundle.properties 内に格納されているため、名前のローカライズが可能です。

    public HelpCtx getHelpCtx() {
        return HelpCtx.DEFAULT_HELP;
    }

JavaHelp の ID や、このアクションのより具体的なヘルプを含む URL がわかっている場合は、これを変更します。

    protected boolean asynchronous() {
        return false;
    }

詳細は、javadoc のエントリを参照してください。

SiteListComponent.java

問題点 #4: このクラス内の煩雑なコードの一部 (ID を処理している部分) は、レイヤーファイル内で指定された ID を使って SiteListComponent のシングルトンインスタンスを保証することと関係があります。これは頻繁に必要となる処理だと思われるので、理想的には SingletonTopComponent のような名前を持つ、TopComponent のサブクラスが存在すべきです。

https://netbeans.org/bugzilla/show_bug.cgi?id=53252

シングルトンを保証するコードを次に示します。

    /** ウィンドウシステムに対する、一意の ID を生成するためのヒント */
    private static final String PREFERRED_ID = "rss_list"; // NOI18N
    
    /** (シングルトン) インスタンスの実際の ID */
    private static String s_id = PREFERRED_ID;

    ...

    public static synchronized SiteListComponent getInstance()
    {
        TopComponent c;
        c = WindowManager.getDefault().findTopComponent(s_id);
        if (c == null)
        {
            c = new SiteListComponent();
            s_id = WindowManager.getDefault().findTopComponentID(c);
        }
        return (SiteListComponent)c;
    }

    ...

    protected String preferredID() { 
        return PREFERRED_ID;
    }

NetBeans は、現在メモリー内に存在するすべての TopComponent のマップを維持します。このマップのキーは、筆者が「ID」と呼んでいるものです。「優先」ID は、新しいインスタンスの作成時に使用される、ウィンドウシステムへのヒントにすぎません。このヒントが何らかの効果を持つ保証は一切ありません。これは一般に非常に有用であり、$userdir/config/Windows2Local の内容を詳細に調べることで問題の原因を突きとめる場合に、コンポーネントの名前を認識できます。

一方、静的なフィールド s_id は、メモリー内の 1 つの SiteListComponent インスタンスの ID となるよう設計されています。そのデフォルトは「rss_list」ですが、これは、それが feedList.wstcref 内で指定された ID の値であるからです。したがって、ユーザーがこのモジュールをはじめて使用する場合は、ID が「rss_list」のコンポーネントが宣言的にインスタンス化されます。一方、ユーザーがそのコンポーネントを閉じ、NetBeans を再起動してから ViewFeedsAction を呼び出した場合、新しいコンポーネントは別の ID を持つ可能性があります。これが、getInstance() メソッドによって新しい値が s_id に代入される可能性がある理由です。

ID が実際に変更された場合、レイヤーファイル内には、新しいコンポーネントが「explorer」モードで合体することを主張するものは何もありません。これを強制するには、open() メソッドを上書きします。

    private static final String MODE = "explorer"; // NOI18N

    public void open()
    {
        Mode m = WindowManager.getDefault().findMode(MODE);
        m.dockInto(this);
        super.open();
    }

このクラスの残りは、NetBeans に固有のものではありません。このコンポーネントは 1 つの JList と 2 つのボタンから構成されており、ボタンの 1 つは新しいフィードの追加用、もう 1 つは選択されたフィードの削除用です。このリストは SiteListModel によってサポートされます。

SiteListModel

SiteListModel クラスは、Swing の DefaultListModel に直列化だけを追加したものです。これは構築時に、フィードのリストをディスクから読み込みます。これは、リストが編集されるたびにディスク上のリストを更新します。(やりすぎでしょうか。そうかもしれませんが、これはそれほど長いリストにはならず、ユーザーが一度設定を行えば、編集頻度も少なくなります。)

重要な部分は、リストを直列化する場所を判定する部分です。それをシステムファイルシステム内に配置することで、直列化されたファイルを $userdir/config 内のファイル構造の一部にすることができます。

    private static final String DIR = "FeedReader"; //NOI18N
    private static final String FILENAME = "feeds.ser"; //NOI18N

    private FileObject getSerializedFile(boolean create) throws IOException
    {
        FileSystem sysFs = Repository.getDefault().getDefaultFileSystem();
        FileObject dir = sysFs.findResource(DIR);
        if (dir == null)
        {
            if (create)
                dir = sysFs.getRoot().createFolder(DIR);
            else
                return null;
        }
        
        FileObject fo = dir.getFileObject(FILENAME);
        if (fo == null)
        {
            if (create)
                fo = dir.createData(FILENAME);
            else
                return null;
        }
        
        return fo;
    }

ブール型パラメータの create によって、ファイルがまだ存在していない場合にファイルを作成するかどうかが決まります (書き込みは true、読み取りは false です)。

したがって、直列化されたリストは $userdir/config/FeedReader/feeds.ser 内に格納されます。

これは、フィードのリストを書き出すための若干低レベルのアプローチです。実際、この一部をユーザーに代わって処理してくれる NetBeans API が存在します。SystemOption のサブクラスを作成し、その readExternal()writeExternal() をオーバーライドするだけで、直列化を実装できます。ここでは、内部で何が起こっているかを理解しやすいように、システムファイルシステムに直接書き込むことを選択しました。またこの実装では、異なるファイル形式を使用する柔軟性も得られます。getSerializedFile() から返された FileObject には、ObjectOutputStream を使用する場合と同じくらい簡単に、XML データを書き込むことができます。

EntryListComponent

このクラスは、中央つまり「editor」モードで開かれ、単一フィードからのニュース項目のリストを含むコンポーネントを定義します。

public class EntryListComponent extends TopComponent 
{
    protected static final String PREFERRED_ID = "rss_entry_list"; //NOI18N
    
    protected String preferredID() { 
        return PREFERRED_ID;
    } 

このクラス内の ID について、変わったことは何もありません。それらは、ウィンドウシステムへのヒントとして機能しているだけです。

    public static TopComponent getInstance(Feed feed)
    {
        // このフィードを含むオープン状態のインスタンスを検索します
        Iterator opened = TopComponent.getRegistry().getOpened().iterator();
        while (opened.hasNext())
        {
            Object tc = opened.next();
            if (tc instanceof EntryListComponent)
            {
                EntryListComponent elc = (EntryListComponent)tc;
                if (feed.equals(elc.m_feed))
                {
                    elc.initData(feed);
                    return elc;
                }
            }
        }
        
        // 1 つも見つからない場合は新しいものを作成します
        return new EntryListComponent(feed);
    }

getInstance() メソッドは、一意のフィードごとにインスタンスが 1 つだけ存在することを保証します。したがって、開かれたタブが一度に複数存在していてもかまいませんが、それらのタブはフィード URL ごとに 1 つだけです。

    
    protected EntryListComponent(Feed feed) 
    {
        super();
        initComponents();
        if (feed != null)
            initData(feed);
    }

コンストラクタを protected にすることで、クライアントが getInstance() を使用する必要があるようにします。

    
    protected void initData(Feed feed)
    {
        m_list.setFeed(feed);
        setDisplayName(feed.toString());
        
        m_feed = feed;
    }

JList にフィードを与え、コンポーネントタブ上やウィンドウメニュー内などで使用される表示名を設定します。

    public int getPersistenceType() {
        return PERSISTENCE_NEVER;
    }

NetBeans の終了時に、これらのコンポーネントの直列化で困らせません。

EntryList、BrowserFrame、Feed

これらのクラスには、NetBeans に固有のコードはあまり含まれていません。コード内のコメントをお読みください。

試してみましょう

nbbuild/ ディレクトリで ant を実行します。次に、/home/rich/netbeans/rssreader1/bin/rss-reader.sh (Windows の場合は rss-reader.bat) を実行します。フィードの URL を追加可能な空のリストが左側に表示されるはずです。



動作中のフィードリーダーです。不適切な箇所が 1 つだけあります。RSS 機能がまだ NetBeans にひも付けされているように見えます。RSS の読み取りに役立つことを何も行わないツールバーボタンやメニュー項目がまだ存在します。

ブランド設定

フィードリーダーのブランド設定モジュールは、非常に単純なモジュールです。Java コードは一切ありません。一連の不要なツールバーやメニュー項目を非表示にするレイヤーファイルがあるだけです。

    <folder name="Toolbars">
        <folder name="File_hidden"/>
        <folder name="Edit_hidden"/>
    </folder>
    
    <folder name="Menu">
        <folder name="File">
            <file name="org-openide-actions-SaveAction.instance_hidden"/>
            <file name="org-openide-actions-SaveAllAction.instance_hidden"/>
            <file name="org-netbeans-core-actions-RefreshAllFilesystemsAction.instance_hidden"/>
            <file name="org-openide-actions-PageSetupAction.instance_hidden"/>
            <file name="org-openide-actions-PrintAction.instance_hidden"/>
        </folder>
        
        <folder name="Edit_hidden"/>
        
        <folder name="View">
            <!-- コンテンツのレンダリング品質があまり良くないデフォルトの Web ブラウザを非表示にします -->
            <file name="org-netbeans-core-actions-HTMLViewAction.instance_hidden"/>
        </folder>
        
        <folder name="Window">
            <file name="org-netbeans-core-actions-GlobalPropertiesAction.instance_hidden"/>
        </folder>
    </folder>

皆さんが心に抱いているに違いない質問は、「非表示にするこれらのファイルすべての名前を、筆者はどうやって知ったのか」でしょう。確かに、「ファイル」>「保存」メニュー項目を非表示にするには <file name=”org-openide-actions-SaveAction.instance_hidden”/> を指定する必要があるということは、すぐにはわかりません。方法は 2 つあります。NetBeans のソースコード内をあちこち調べ、それが最初に宣言されたレイヤーファイルを見つけます。あるいは、NetBeans アップデートセンターの「Open APIs Support」 (org-netbeans-modules-apisupport) モジュールに含まれるツールである Bean ブラウザを使用してもかまいません。この場合、システムファイルシステムの参照を可能にする「Bean ブラウザ」という名前のノードが、「ウィンドウ」 > 「実行時」ウィンドウに作成されます。

非常に有用な小規模ツールです。

ブランド設定モジュールには、レイヤーファイル以外に、通常とは異なる側面がもう 1 つ存在します。構築スクリプト内には、今となっては見慣れた、配備対象ファイルセットにリソースファイルを追加するセクションが存在します。

    <!-- 
    配備時にこのモジュールの一部とみなすべきファイルをすべて特定します 
    -->
    <target name="files-init" depends="basic-init">
        <patternset id="module.files">
            <include name="${module.jar}"/>
            <include name="${javahelp.jar}" if="has.javahelp"/>
            <include name="${nb.system.dir}/Modules/${code.name.base.dashes}.xml"/>

            <!-- フィードリーダー用の追加がここから始まります -->
            <include name="${nb.lib.dir}/locale/core_rss.jar"/>
            <include name="${nb.modules.dir}/locale/org-netbeans-core-windows_rss.jar"/>
        </patternset>
    </target>

    <!-- 
    netbeans-extra は、ファイルコピーをプラグインするために用意されたフックです。
    -->
    <target name="netbeans-extra" depends="init">
        <mkdir dir="${netbeans.dest.dir}/${cluster.dir}/${nb.lib.dir}/locale"/>
        <mkdir dir="${netbeans.dest.dir}/${cluster.dir}/${nb.modules.dir}/locale"/>

        <jar destfile="${netbeans.dest.dir}/${cluster.dir}/${nb.lib.dir}/locale/core_rss.jar" 
             basedir="core"/>
        <jar destfile="${netbeans.dest.dir}/${cluster.dir}/${nb.modules.dir}/locale/org-netbeans-core-windows_rss.jar" 
             basedir="core-windows"/>
    </target>

この場合、2 つのJAR ファイル core_rss.jarorg-netbeans-core-windows_rss.jar を追加しています。これらの JAR は core.jarorg-netbeans-core-windows.jar に対応しています。実際、標準 NetBeans インストール内の任意の JAR に対して「ブランド設定」を行うには、その元の JAR からの相対的な位置に別の JAR を配置します。

/path/to/original.jar
/path/to/locale/original_brandname.jar

JDIC モジュールのシェルスクリプトが「–branding rss」をパラメータとして渡していたことを思い出してください。したがって、この場合の「brandname」は「rss」です。

ほとんどすべてのリソースファイルにブランドを設定できます。アイコン、バンドル、レイヤーファイルなど。core_rss.jar では、スプラッシュ画面と、そのスプラッシュ画面の表示方法に影響を与えるキーを含むリソースバンドルとにブランドを設定しました。org-netbeans-core-windows_rss.jar では、1 つのバンドルだけにブランドを設定し、メインタイトルバーのテキストを上書きしました。

もう一度試してみましょう

これで、この記事の冒頭のスクリーンショットに似た画面が表示されるはずです。そして、この時点で皆さんは、NetBeans プラットフォーム用の実際のアプリケーションを記述する方法を理解できているはずです。

質問

ご質問があれば、 メーリングリスト宛にご連絡ください。NetBeans のメーリングリストについては、https://netbeans.org/community/lists/top.html を参照してください。