NetBeans IDE 5.0 FeedReader チュートリアルへようこそ。このチュートリアルで構築する FeedReader アプリケーションは Mozilla Firefox の Sage プラグインを参考にしてモデル化された、簡単な RSS/Atom フィードブラウザです。ブラウザで開くことができる個々のフィードエントリを表すサブノードを持つ、フィードのツリーを提供します。例として、PlanetNetBeans RSS フィードのフィードエントリを NetBeans IDE の内部ブラウザで開いた様子を示します。
目次はじめにFeedReader アプリケーションのコーディングを始める前に、NetBeans IDE のアプリケーション開発の分野でよく使われる用語を理解しておきましょう。その過程で、これから作成するアプリケーションの概要について理解し、学習内容を知り、必要な設定を行います。 頻出用語についてこのチュートリアルは、NetBeans に組み込まれているインフラストラクチャーの基本概念を理解していることが前提です。理解すべきことはそれほど多くはありません。次に、知っておくべき頻出用語を示します。
余談ですが、NetBeans の用語では、「プラグイン」は形容詞で「モジュール」は名詞です。両者の間に意味的な違いはありません。
FeedReader アプリケーションについてFeedReader アプリケーションの作成には、NetBeans インフラストラクチャーを大いに利用します。最初に利用するのは、システムファイルシステムです。先ほど指摘したように、システムファイルシステムには、構成データが含まれています。これは、システム内にあるすべてのプラグインモジュールの構成ファイル (それぞれ「layer.xml」ファイルとしてディスクに格納されている) から構築され、これらのファイルをユーザーの設定ディレクトリに書き込みます。 システムファイルシステムは、ディスク上にあるユーザーのファイルの認識に使用されるのと同じインフラストラクチャーを使ってファイルの認識を行います。このことは、IDE の構成データの内部にあるフォルダの表示が、ディスク上のフォルダを表示するのと同じくらい簡単にできることを意味します。このように、ファイルやツリーの表示などには、NetBeans に組み込まれているすべての plumb 機能を使用できます。実際、IDE に見られる多くのビューには、これと同じ手法が使用されています。たとえば、「お気に入り」ウィンドウは、システムファイルシステム内のフォルダを表示するビューであり、このフォルダにはディスク上のファイルへのリンクが含まれています。また、「実行時」ウィンドウの内容もシステムファイルシステムのフォルダのビューであるため、プラグインモジュールはそこにノードを追加できます。ディスク上のファイル認識に用いられるのと同じ仕組みが使用されるため、フォルダ内部のオブジェクトには、どんなアイコンや表示名でも選択できます。 使用するほかの要素として、ノード API があります。ノード API は TreeNode を汎用的にしたものですが、ノードは、ツリー以外にもさまざまなビューアコンポーネントに表示できます。一般にノードは、DataObject を表します。基本的に DataObject は解析済みのファイル、すなわち、ファイルの内容やファイルが表すものの意味を認識し、それを使用してなんらかの処理を実行できる Java オブジェクトです。ノードは、アクションや各言語対応の表示名、アイコンなどの、ユーザーが対話する DataObject に機能を追加します。 そこで、ウィザードを使用して基本のテンプレートを生成したあとで、layer.xml ファイルを使用して、システムファイルシステム内に RSS フィードオブジェクト用のフォルダを作成します (RssFeeds フォルダの作成)。次に、生成したファイルの 1 つを基にして、IDE の「プロジェクト」ウィンドウまたは「ファイル」ウィンドウに類似した、フォルダのビューを構築します (フィードウィンドウの拡張)。このビューは、RSS フィードオブジェクト用のフォルダに基づきます。続いて、このフォルダを表す DataObject とそのノードを取得します。さらに、このノードを FilterNode にラップします (RssFeeds フォルダの作成)。FilterNode は、別のノードのラッパーとして動作するノードです。デフォルトでは、ほかのノードと同じように動作しますが、メソッドをオーバーライドして設定を変更することで、独自のアイコン、表示名、およびアクションを与えることができます。その後、FilterNode のそれぞれの子ノードもラップし、同じように設定を変更します。 次に、ルートノード上に「Add Feed」アクションを作成します。ユーザーが RSS フィードを追加したときに、次のような簡単な処理を行います。新しいフィードオブジェクト (実際は、その URL を含むただのオブジェクト。フィードオブジェクトの作成を参照) を作成し、このフィードオブジェクトを RSSFeeds フォルダのファイルとしてシリアライズします。ファイルの視覚化に NetBeans 組み込みインフラストラクチャーを使用しているので (ファイルの追加や削除を認識できる、フォルダの標準ノードを取得しているため)、新たに追加されたフィードのノードがユーザーインタフェースに即座に現れます。このように、システムファイルシステムを使用すると、終了時に RSS フィードの一覧を保存するためのコードを書く必要がなくなります。フィードをユーザーが作成した時に保存すれば、そのデータは自動的にディスクに保持されます。そのため、基本的には、Feed POJO をフォルダにドロップするだけで、そのフォルダのビューが表示されます。それ以外のことは、ほぼすべてシステムが処理します。 このチュートリアルについて
必要なソフトウェアのインストールが完了したら、このチュートリアルに要する時間は 60 分ほどです。 リソースについて始める前に、コンピュータに次のリソースをインストールする必要があります。
アプリケーションの確認アプリケーションを作成する前に、最終成果物について知りたいと思われるかもしれません。FeedReader アプリケーションは正式な NetBeans サンプルとして IDE にバンドルされているので、いつでも「新規プロジェクト」ウィザードから開くことができます。 アプリケーションのインストール
アプリケーションの紹介FeedReader アプリケーションによって、「RSS/Atom フィード」というノードを含む「RSS/Atom フィード」ウィンドウが表示されます。
このリッチクライアントアプリケーションによって、次のような機能も提供されます。
ソースの紹介この FeedReader サンプルは、メインファイル (Java クラス) および サポートファイルで構成されています。
アプリケーションの設定NetBeans IDE では、アプリケーションの基盤として使用する多数のファイルを生成することから、NetBeans 上のアプリケーション構築が始まります。たとえば、NetBeans IDE は、NetBeans Platform 上に構築されるプラグインモジュールおよびアプリケーションで必要な基本ファイルをすべて設定する、「モジュールプロジェクト」ウィザード、「モジュールスイートプロジェクト」ウィザード、および「ライブラリラッパーモジュールプロジェクト」ウィザードを提供しています。
モジュールスイートプロジェクトの作成
IDE によって feedreader-suite プロジェクトが作成されます。このプロジェクトには、次で作成するモジュールプロジェクトおよびライブラリラッパーモジュールプロジェクトが含まれます。 IDE でプロジェクトが開きます。「プロジェクト」ウィンドウ (Ctrl-1) でプロジェクトの論理構造を、「ファイル」ウィンドウ (Ctrl-2) でそのファイル構造を見ることができます。 ライブラリのラップFeedReader アプリケーション全体を 1 つのプラグインモジュールとして統合することもできます。ただし、FeedReader アプリケーションは、Rome、Rome Fetcher、および JDom ライブラリを必要とします。
あとから FeedReader アプリケーションを拡張する場合、追加するモジュールでもこれらのライブラリを使用する可能性があります。追加するモジュールは、FeedReader 全体にではなく、ライブラリモジュールだけに依存させることをお勧めします。また、ライブラリモジュールは自動読み込みが可能です。これは、NetBeans が 必要なときにだけライブラリモジュールを読み込むことを意味します。それまでは、実行時にメモリーが占有されることはありません。
これで、JDom JAR ファイルを含むライブラリラッパーモジュールプロジェクトと、Rome および Rome Fetcher の各 JAR ファイルを含む別の 2 つのライブラリラッパーモジュールプロジェクトができました。 モジュールプロジェクトの作成
IDE によって FeedReader プロジェクトが作成されます。このプロジェクトには、モジュールのソースと、プロジェクトの Ant 構築スクリプトなどのプロジェクトメタデータがすべて含まれます。IDE でこのプロジェクトが開きます。「プロジェクト」ウィンドウ (Ctrl-1) でプロジェクトの論理構造を、「ファイル」ウィンドウ (Ctrl-2) でそのファイル構造を見ることができます。「プロジェクト」ウィンドウには次のように表示されるはずです。 feedreader-suite プロジェクトノードを右クリックし、「プロパティー」を選択して、「プロジェクトプロパティー」ダイアログの「ソース」をクリックします。プロジェクトの作成中にモジュールスイートに追加されたモジュールが、パネルに表示されます。この「スイートモジュール」リストには、FeedReader、jdom、rome、および rome-fetcherが表示されます。 FeedReader ウィンドウの作成この節では、「ウィンドウコンポーネント」ウィザードを使用して、カスタムウィンドウコンポーネントを作成するファイルと、このコンポーネントを呼び出すアクションを作成するファイルを生成します。また、このウィザードは、アクションをメニュー項目として layer.xml 構成ファイルに登録し、ウィンドウ処理コンポーネントをシリアライズするためのエントリを追加します。この節のすぐあとで、「ウィンドウコンポーネント」ウィザードが生成するファイル動作を確認する方法を説明します。
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE filesystem PUBLIC "-//NetBeans//DTD Filesystem 1.1//EN" "http://www.netbeans.org/dtds/filesystem-1_1.dtd"> <filesystem> <folder name="Actions"> <folder name="Window"> <file name="org-myorg-feedreader-FeedAction.instance"/> </folder> </folder> <folder name="Menu"> <folder name="Window"> <file name="FeedAction.shadow"> <attr name="originalFile" stringvalue="Actions/Window/org-myorg-feedreader-FeedAction.instance"/> </file> </folder> </folder> <folder name="Windows2"> <folder name="Components"> <file name="FeedTopComponent.settings" url="FeedTopComponentSettings.xml"/> </folder> <folder name="Modes"> <folder name="explorer"> <file name="FeedTopComponent.wstcref" url="FeedTopComponentWstcref.xml"/ </folder> </folder> </folder> </filesystem> layer.xml ファイル内のエントリは、次のように動作します。
アプリケーションの動作確認コードを入力することなく、この段階でアプリケーションの動作確認を行うことができます。ここでの動作確認とは、NetBeans IDE のインストール環境の別のインスタンスにアプリケーションを配備して、空のフィードウィンドウが正しく表示されるかどうかを確認することです。 NetBeans IDE は、Ant 構築スクリプトを使用して、アプリケーションの構築とインストールを行います。構築スクリプトは、モジュールプロジェクトの作成時に作成されたものです。
アプリケーションへのコードの追加アプリケーションの基本的な部分はできあがったので、次に独自のコードを追加してみましょう。その前に、アプリケーションの依存関係を指定する必要があります。そのあとで、「新規ファイル」ウィザードとソースエディタを使用して、ソースの紹介の項で説明した主クラスの作成とコーディングを行います。 アプリケーションの依存関係の指定NetBeans API に属するいくつかのクラスをサブクラス化する必要があります。それぞれのクラスを依存関係として宣言する必要があります。このためには「プロジェクトプロパティー」ダイアログを使用します。
RssFeeds フォルダの作成システムファイルシステムブラウザを使用して、RSS フィードオブジェクトのフォルダを追加します。このあとで FeedTopComponent.java にコードを追加します。このファイルは、フォルダの内容を表示するために、「ウィンドウコンポーネント」ウィザードによって作成されたものです。
フィードオブジェクトの作成次に、URL とそれに関連づけられた Rome フィードをカプセル化する簡単な POJO を作成します。
public class Feed implements Serializable { private static FeedFetcher s_feedFetcher = new HttpURLFeedFetcher( HashMapFeedInfoCache.getInstance()); private transient SyndFeed m_syndFeed; private URL m_url; private String m_name; protected Feed() { } public Feed(String str) throws MalformedURLException { m_url = new URL(str); m_name = str; } public URL getURL() { return m_url; } public SyndFeed getSyndFeed() throws IOException { if (m_syndFeed == null) { try { m_syndFeed = s_feedFetcher.retrieveFeed(m_url); if (m_syndFeed.getTitle() != null) m_name = m_syndFeed.getTitle(); } catch(Exception ex) { throw new IOException(ex.getMessage()); } } return m_syndFeed; } public String toString(){ return m_name; } } コードの多くに下線が引かれているのは、それらのパッケージが宣言されていないためです。次の手順でこれを行います。 このファイルを再フォーマットして依存関係を宣言するには、次の手順に従います。
これで、赤い下線がすべて消えます。そうならない場合、チュートリアルを続行する前に、この問題を解決してください。 フィードウィンドウの拡張
setLayout(new BorderLayout()); add(view, BorderLayout.CENTER); view.setRootVisible(true); try { manager.setRootContext(new RssNode.RootRssNode()); } catch (DataObjectNotFoundException ex) { ErrorManager.getDefault().notify(ex); } ActionMap map = getActionMap(); map.put("delete", ExplorerUtils.actionDelete(manager, true)); associateLookup(ExplorerUtils.createLookup(manager, map)); コードの多くには下線が引かれているのは、関連付けられているパッケージが宣言されていないためです。次の手順でこれを行います。 このファイルを再フォーマットして依存関係を宣言するには、次の手順に従います。
RssNode クラスの作成class RssNode extends FilterNode { /** ルート RSS ノードの子を宣言 */ public RssNode(Node folderNode) throws DataObjectNotFoundException { super(folderNode, new RssFolderChildren(folderNode)); } /** 「Add Feed」アクションと「Add Folder」アクションを宣言 */ public Action[] getActions(boolean popup) { DataFolder df = (DataFolder)getLookup().lookup(DataFolder.class); return new Action[] { new AddRssAction(df), new AddFolderAction(df) }; } /** ルートノードを取得 */ public static class RootRssNode extends RssNode { public RootRssNode() throws DataObjectNotFoundException { super(DataObject.find( Repository.getDefault().getDefaultFileSystem() .getRoot().getFileObject("RssFeeds")).getNodeDelegate()); } public String getDisplayName() { return NbBundle.getMessage(RssNode.class, "FN_title"); } } /** ルートノードの子を取得 */ private static class RssFolderChildren extends FilterNode.Children { RssFolderChildren(Node rssFolderNode) { super(rssFolderNode); } protected Node[] createNodes(Object key) { Node n = (Node) key; try { if (n.getLookup().lookup(DataFolder.class) != null) { return new Node[] { new RssNode(n) }; } else { Feed feed = getFeed(n); if (feed != null) { return new Node[] { new OneFeedNode(n, feed.getSyndFeed()) }; } else { // ベストエフォート return new Node[] { new FilterNode(n) }; } } } catch (IOException ioe) { ErrorManager.getDefault().notify(ioe); } catch (IntrospectionException exc) { ErrorManager.getDefault().notify(exc); } // その他の型のノード (なんらかの処理を行う必要あり) return new Node[] { new FilterNode(n) }; } } /** フィードノードを取得して FilterNode 内にラップ */ private static class OneFeedNode extends FilterNode { OneFeedNode(Node feedFileNode, SyndFeed feed) throws IOException, IntrospectionException { super(feedFileNode, new FeedChildren(feed), new ProxyLookup(new Lookup[] { Lookups.fixed(new Object[] { feed }), feedFileNode.getLookup() })); } public String getDisplayName() { SyndFeed feed = (SyndFeed) getLookup().lookup(SyndFeed.class); return feed.getTitle(); } public Image getIcon(int type) { return Utilities.loadImage("org/myorg/feedreader/rss16.gif"); } public Image getOpenedIcon(int type) { return getIcon(0); } public Action[] getActions(boolean context) { return new Action[] { SystemAction.get(DeleteAction.class) }; } } /** フィードノードの子を宣言 */ private static class FeedChildren extends Children.Keys { private final SyndFeed feed; public FeedChildren(SyndFeed feed) { this.feed = feed; } protected void addNotify() { setKeys(feed.getEntries()); } public Node[] createNodes(Object key) { try { return new Node[] { new EntryBeanNode((SyndEntry) key) }; } catch (final IntrospectionException ex) { ErrorManager.getDefault().notify(ErrorManager.INFORMATIONAL, ex); //決して発生しない - 上記の処理ができない理由はない return new Node[] { new AbstractNode(Children.LEAF) { public String getHtmlDisplayName() { return "<font color='red'>" + ex.getMessage() + "</font>"; } }}; } } } /** 子を FilterNode でラップ */ private static class EntryBeanNode extends FilterNode { private SyndEntry entry; public EntryBeanNode(SyndEntry entry) throws IntrospectionException { super(new BeanNode(entry), Children.LEAF, Lookups.fixed(new Object[] { entry, new EntryOpenCookie(entry) })); this.entry = entry; } /** HtmlDisplayName の使用により、RSS エントリタイトル内の HTML が /** 正しく処理され、エスケープされ、エンティティが解決されることなどを保証する */ public String getHtmlDisplayName() { return entry.getTitle(); } /** エントリの記述のツールチップを作成 */ public String getShortDescription() { return entry.getDescription().getValue(); } /** フィードエントリ上に Open アクションを用意 */ public Action[] getActions(boolean popup) { return new Action[] { SystemAction.get(OpenAction.class) }; } public Action getPreferredAction() { return (SystemAction) getActions(false) [0]; } } /** ユーザーによる Open アクションの呼び出し時に何が起こるかを指定 */ private static class EntryOpenCookie implements OpenCookie { private final SyndEntry entry; EntryOpenCookie(SyndEntry entry) { this.entry = entry; } public void open() { try { URLDisplayer.getDefault().showURL(new URL(entry.getUri())); } catch (MalformedURLException mue) { ErrorManager.getDefault().notify(mue); } } } /** フィードを検索 */ private static Feed getFeed(Node node) { InstanceCookie ck = (InstanceCookie) node.getCookie(InstanceCookie.class); if (ck == null) { throw new IllegalStateException("Bogus file in feeds folder: " + node.getLookup().lookup(FileObject.class)); } try { return (Feed) ck.instanceCreate(); } catch (ClassNotFoundException ex) { ErrorManager.getDefault().notify(ex); } catch (IOException ex) { ErrorManager.getDefault().notify(ex); } return null; } /** フィードをグループに分けて整理するためのフォルダを追加するアクションを作成 */ private static class AddFolderAction extends AbstractAction { private DataFolder folder; public AddFolderAction(DataFolder df) { folder = df; putValue(Action.NAME, NbBundle.getMessage(RssNode.class, "FN_addfolderbutton")); } public void actionPerformed(ActionEvent ae) { NotifyDescriptor.InputLine nd = new NotifyDescriptor.InputLine( NbBundle.getMessage(RssNode.class, "FN_askfolder_msg"), //NOI18N NbBundle.getMessage(RssNode.class, "FN_askfolder_title"), //NOI18N NotifyDescriptor.OK_CANCEL_OPTION, NotifyDescriptor.PLAIN_MESSAGE); Object result = DialogDisplayer.getDefault().notify(nd); if (result.equals(NotifyDescriptor.OK_OPTION)) { final String folderString = nd.getInputText(); try { DataFolder.create(folder, folderString); } catch (IOException ex) { ErrorManager.getDefault().notify(ex); } } } } /** フィードを追加するアクションを作成 */ private static class AddRssAction extends AbstractAction { private DataFolder folder; public AddRssAction(DataFolder df) { folder = df; putValue(Action.NAME, NbBundle.getMessage(RssNode.class, "FN_addbutton")); } public void actionPerformed(ActionEvent ae) { NotifyDescriptor.InputLine nd = new NotifyDescriptor.InputLine( NbBundle.getMessage(RssNode.class, "FN_askurl_msg"), //NOI18N NbBundle.getMessage(RssNode.class, "FN_askurl_title"), //NOI18N NotifyDescriptor.OK_CANCEL_OPTION, NotifyDescriptor.PLAIN_MESSAGE); Object result = DialogDisplayer.getDefault().notify(nd); if (result.equals(NotifyDescriptor.OK_OPTION)) { final String urlString = nd.getInputText(); try { Feed f = new Feed(urlString); FileObject fld = folder.getPrimaryFile(); String baseName = "RssFeed"; //NOI18N int ix = 1; while (fld.getFileObject(baseName + ix, "ser") != null) { ix++; } FileLock lock = null; try { FileObject writeTo = fld.createData(baseName + ix, "ser"); lock = writeTo.lock(); ObjectOutputStream str = new ObjectOutputStream(writeTo.getOutputStream(lock)); str.writeObject(f); } catch (IOException ioe) { ErrorManager.getDefault().notify(ioe); } finally { if (lock != null) lock.releaseLock(); } } catch (MalformedURLException ex) { IllegalArgumentException iae = new IllegalArgumentException(NbBundle.getMessage(RssNode.class, "FN_askurl_err", urlString), ex); //NOI18N throw iae; } } } } } コードの多くに下線が引かれているのは、それらのパッケージが宣言されていないためです。次の手順でこれを行います。このファイルを再フォーマットして依存関係を宣言するには、次の手順に従います。
RssNode クラスのローカライズ
次に、この新しいキーと値のペアを説明します。このキーと値のペアによって、RssNode.java に定義された文字列がローカライズされます。 次に、フィードを追加するユーザーインタフェースのローカライズについてを示します。
次に、フォルダを追加するユーザーインタフェースのローカライズについて示します。
アプリケーションのブランディング開発サイクルの最終段階で、アプリケーションを仕上げる間に、次のような疑問が生じます。
これらの疑問は、ブランディングに関係しています。これは、NetBeans Platform 上に構築されたアプリケーションを独自のものにするアクティビティーです。この IDE には、ブランディングを支援するパネルが、モジュールスイートプロジェクトの「プロジェクトプロパティー」ダイアログに用意されています。
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE filesystem PUBLIC "-//NetBeans//DTD Filesystem 1.1//EN" "http://www.netbeans.org/dtds/filesystem-1_1.dtd"> <!-- これは 「ブランディング」レイヤーです。ブランディング対象のレイヤーファイルとマージされます。 この場合、必要でないメニュー項目およびツールバーは非表示になります。 --> <filesystem> <!-- 未使用のツールバーを非表示にする --> <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="Tools_hidden"/> </folder> </filesystem> アプリケーションの配布NetBeans IDE では、Ant 構築スクリプトを使用して、アプリケーションの配布版を作成します。構築スクリプトは、プロジェクトの作成時に作成されます。
FeedReader アプリケーションの実行中は、「RSS/Atom フィード」というノードを含む「RSS/Atom フィード」ウィンドウが表示されます。詳細は、アプリケーションの確認を参照してください。
|
|