NetBeansのプラグインの作り方を簡単に紹介します。
NetBeans Moduleプロジェクトの作成
NetBeansのプラグインはmoduleという単位でNetBeans本体に追加できます。NetBeans本体もプラグインと同じmoduleが集まって構成されています。
moduleのプロジェクトの作成方法は2通りあります。違いはビルドツールで、1つはAnt、もう1つはmavenです。NetBeans本体はAntプロジェクトでつくらていますが、モダンなツールはmavenです。どちらでつくっても動作は同じですが、外部のライブラリなどを使って開発したい場合は、mavenのプロジェクトを使ったほうが管理が楽だと思います。
Ant プロジェクトの作成
[File]メニューの[New Project…] をクリックして、New Project ウィザードを開きます。Categories: Java with Ant > NetBeans Modules、Projects: Moduleを選択して、[Next]ボタンを押します。
プロジェクト名を入力して、[Next]ボタンを押します。
Code Name BaseとModule Display Nameを入力して[Finish]ボタンを押すと、プロジェクトが作成されます。
Maven プロジェクトの作成
Antプロジェクトの時と同様に、New Project ウィザイードを開き、Categories: Java with Maven、Projects: NetBeans Moduleを選択して、[Next]ボタンを押します。
必要な情報を入力して、[Finish]ボタンを押すとプロジェクトが作成されます。これらの情報はあとからpom.xmlファイルを修正して変更できます。NetBeans Version:はRELEASE82までしか選択できないので、pom.xmlでRELEASE110に変更しましょう。(バグ報告しました:https://issues.apache.org/jira/browse/NETBEANS-2628)
以下、Antプロジェクトを使って説明しています。
コーディング(Actionの追加)
NetBeansにはplugin(module)用のコードのテンプレートが用意されています。用意されているものに関してはウィザードに従って値を入力していくと、コードが自動で生成されます。ここでは、例としてActionを追加する方法を紹介します。
エディタで選択された文字列をHTML Entity Numberに変換するアクションを追加してみましょう。
ファイルを追加したいパッケージを選択して、[File]メニューの[New File]をクリックします。New Fileダイアログが表示されるので、Categories: Module Development のFile Types: Actionを選択して[Next]をクリックします。
特定の条件でアクションを実行するようにもできますが、ここでは常に有効なアクションでつくってみましょう。Always Enabled を選択して[Next]をクリックします。
次にアクションをどこに追加するのかを設定します。以下のように設定して[Next]をクリックします。ここでは[Edit]メニューの一番最初にアクションを追加します。
- Category: Edit
- Global Menu Item: チェックを入れる
- Menu: Edit
- Position: デフォルト(メニューの先頭)
最後にクラス名や表示されるアクション名を入力してFinishをクリックすると、ファイルが生成されます。ここでは、以下のように設定します。アイコンを表示したければ、16×16で制作してBrowseボタンから追加してください。
- Class Name: ConvertToHtmlEntityNumberAction
- Display Name: Convert To Html Entity Number
このような感じでファイルがつくられて、コードが生成されます。
実際に書いていかなければいけない部分は actionPerformed()の中身です。アクションが実行されたときにここが呼びだされます。
最初に、コンポーネントを取得し、エディタの文字列を取得します。ここでは最後にフォーカスされたエディタのコンポーネントを取得して、文字列を変換することにします。
コンポーネントを取得するために下記のメソッド(EditorRegistry.lastFocusedComponent())を呼び出します。
public void actionPerformed(ActionEvent e) { EditorRegistry.lastFocusedComponent(); }
このままでは、赤のアンダーラインが表示されて、エラーになります。moduleの依存関係を追加する必要があります。追加する方法は次の3つです。
- Project Propertiesから追加する
- ヒント([Alt] + [Enter]/エディタのライン番号のバルブをクリック)から追加する
- プロジェクトツリーのLibrariesから追加する
Project Propertiesから追加する
プロジェクトのノードを右クリックしてPropertiesをクリックします。
Project Propertiesダイアログが表示されます。Librariesを選択してModule DependenciesのAddボタンをクリックします。
Add Module Dependenciesダイアログが表示されます。 Filterのテキストフィールドにテキストを入力すると、関係するmoduleの一覧が表示されるので必要なものを追加します。ここではEditorRegistryと入力して、Editor Library 2を追加します。これでエラーがなくなるはずです。もしエラーが出ている場合は、import文が不足していないか確認してください。不足している場合は[Ctrl] + [Shift] + [I]で追加されます。
ヒント([Alt] + [Enter]/エディタのライン番号のバルブ)から追加する
エディタのライン番号のところにバルブのアイコンが表示されている場合は、ヒントの機能を利用できます。[Alt] + [Enter] もしくはバルブのアイコンをクリックします。Search Module Dependency for **** を選択([Enter]/クリック)するとProject Propertiesから追加したときと同じAdd Module Dependencyダイアログが表示されるので、そこでmoduleを選択して追加できます。
プロジェクトツリーのLibrariesから追加する
プロジェクトツリーのLibrariesを右クリックして、Add Module Dependency… をクリックしても同様に追加できます。
コードの続きを書いていきます。Html Entity Numberに変換するメソッドを追加します。ここでは、アルファベット等を変換しない等の処理は入れずに、単純に全部の文字を変換するメソッドにしています。つまり、全て”&#nnnn;” の形式に変換します。
static String convertToHtmlEntityNumbers(String string) { StringBuilder sb = new StringBuilder(); for (int i = 0; i < string.length(); i++) { sb.append(String.format("&#%d;", Character.codePointAt(string, i))); } return sb.toString(); }
actionPerfomed()の残りも書きます。選択されたテキストを取得して、変換して、documentに挿入するようにします。
補足:メソッド呼び出しの結果を変数に代入したい場合は、[Alt] + [Enter] でヒントを表示して、[Assign Return Value To New Variable]を選択すると、変数に代入するコードを追加してくれます。
@Override public void actionPerformed(ActionEvent e) { JTextComponent editor = EditorRegistry.lastFocusedComponent(); if (editor == null) { return; } String selectedText = editor.getSelectedText(); if (selectedText == null) { return; } // convert selected text String convertedText = convertToHtmlEntityNumbers(selectedText); int selectionStart = editor.getSelectionStart(); int selectionEnd = editor.getSelectionEnd(); int length = selectionEnd - selectionStart; Document document = editor.getDocument(); try { // remove and insert converted text document.remove(selectionStart, length); document.insertString(selectionStart, convertedText, null); } catch (BadLocationException ex) { Exceptions.printStackTrace(ex); } }
補足ですが、documentの文字列を削除して変換された文字列を挿入している部分は、NetBeansの履歴では削除と挿入として残ります。つまり「戻る」を実行したときに一旦、削除後の状態に戻り、さらに「戻る」を実行すると元の状態に戻ることになります。ドキュメントの複数の操作を1つの履歴として扱いたい場合は、NbDocument(https://bits.netbeans.org/dev/javadoc/org-openide-text/org/openide/text/NbDocument.html)のrunAtomicUser()もしくはrunAtomic()メソッドを使います。
全体のコードは以下のようになります。
package com.junichi11.my.netbeans.plugin; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import javax.swing.text.BadLocationException; import javax.swing.text.Document; import javax.swing.text.JTextComponent; import org.netbeans.api.editor.EditorRegistry; import org.openide.awt.ActionID; import org.openide.awt.ActionReference; import org.openide.awt.ActionRegistration; import org.openide.util.Exceptions; import org.openide.util.NbBundle.Messages; @ActionID( category = "Edit", id = "com.junichi11.my.netbeans.plugin.ConvertToHtmlEntityNumberAction" ) @ActionRegistration( displayName = "#CTL_ConvertToHtmlEntityNumberAction" ) @ActionReference(path = "Menu/Edit", position = 100) @Messages("CTL_ConvertToHtmlEntityNumberAction=Convert To Html Entity Number") public final class ConvertToHtmlEntityNumberAction implements ActionListener { @Override public void actionPerformed(ActionEvent e) { JTextComponent editor = EditorRegistry.lastFocusedComponent(); if (editor == null) { return; } String selectedText = editor.getSelectedText(); if (selectedText == null) { return; } // convert selected text String convertedText = convertToHtmlEntityNumbers(selectedText); int selectionStart = editor.getSelectionStart(); int selectionEnd = editor.getSelectionEnd(); int length = selectionEnd - selectionStart; Document document = editor.getDocument(); try { // remove and insert converted text document.remove(selectionStart, length); document.insertString(selectionStart, convertedText, null); } catch (BadLocationException ex) { Exceptions.printStackTrace(ex); } } static String convertToHtmlEntityNumbers(String string) { StringBuilder sb = new StringBuilder(); for (int i = 0; i < string.length(); i++) { sb.append(String.format("&#%d;", Character.codePointAt(string, i))); } return sb.toString(); } }
実行
コードを書き終えたら、実行してみましょう。実行する方法は下記のようにいくつかあります。好きなところから実行してください。
- [Run]メニューの[Run Project]
- ツールバーの[Run Project]ボタン
- プロジェクトのコンテキストメニューの[Run]
- デフォルトのショートカットキーの[F6]
実行するともう1つNetBeansが初期設定の状態で起動します。この起動したNetBeansの中でプラグインが有効になっています。作ったアクションは、最初に[Edit]メニューに登録したので、そこに[Convert To Html Entity Number]があるはずです。
実際にテキストを選択して、アクションを実行してみます。
ユーザーディレクトリ(オプションなどの設定があるディレクトリ)は、本体とは別に各プラグインごとにつくられます(build/testuserdir)。プロジェクトのコンテキストメニューから[Clean]を実行するとこれらは初期化されます。
うまく動かなかったりする場合は、デバッグしてみましょう。
デバッグ
デバッグする方法も実行するときと同様で、下記の場所からデバッグを開始できます。
- [Debug]メニューの[Debug Project]
- ツールバーの[Debug Project]ボタン
- プロジェクトのコンテキストメニューの[Debug]
- デフォルトのショートカットキーの[Ctrl] + [F5]
ブレークポイントはライン番号をクリックすると有効/無効の切り替えができます。
nbm
プラグインを配布・インストールするには、拡張子がnbmのファイルが必要です。
nbmを生成
プラグインはnbmファイルを生成して、それをインストールすることで有効になります。プロジェクトを右クリックして、[Create NBM]を実行してください。問題がなければ、Output ウィンドウでBUILD SUCCESSFULが表示されます。nbmファイルはbuildディレクトリ内につくられます。
nbm(プラグイン)のインストール
インストールは、[Tools]メニューの[Plugins]から行います。[Tools]メニューの[Plugins]をクリックすると、Pluginsダイアログが表示されます。[Downloaded]タブの[Add Plugins…]ボタンを押して、生成したnbmファイルを選択します。他のプラグインをダウンロードしてきたときも同様です。
[Install]ボタンを押すと、ライセンスの確認等があるので、[I accept the terms in all of the license agreements.]にチェックを入れて[Install]を押して、途中の警告も[Continue]すれば、インストールできます。
ライセンスファイルを追加していないと上のような表示になるので、正しいライセンスファイルを追加しておきましょう。(下のプロジェクトの設定を参照)
nbm(プラグイン)の配布
プラグインを配布する方法は下記のようにいくつかあります。
- Plugin Portalに登録する
- GitHubのリポジトリのreleasesにアップロードする
- 独自のアップデートセンターをつくる
Plugin Portalに登録する
Plugin Portalに登録するには、最初にアカウントを作成して、nbmファイルをアップロードします。
NetBeans 本体のPluginsダイアログのAvailable Pluginsに表示されているプラグインは、Plugin Portalに登録されているものですが、登録したものすべてが表示されるわけではありません。
Available Plugins の一覧に表示されるようにするためには、nbmファイルに署名する(sign)必要があります。署名の方法についてはこちら (https://netbeans.apache.org/wiki/DevFaqSignNbm.asciidoc) を参照してください。
git等でバージョン管理している場合は、nbproject/private ディレクトリをignoreするようにしてください。(mavenプロジェクトの場合はkeystoreファイルがignoreされるようにしてください。)
GitHubのリポジトリのreleasesにアップロードする
たとえば、https://github.com/junichi11/netbeans-rainbow-braces/releases のようにgitでバージョンのタグをつくって、そこにnbmファイルをアップロードします。
独自のアップデートセンターをつくる
これに関しては省略しますが、特定のURLをNetBeansに登録してもらって、ダウンロードしてもらうこともできます。
テスト
ユニットテストも追加しておきましょう。(JUnitに関する詳細はおぐぐりください)このままだとactionPerformed()メソッドは、JTextComponentをテスト時に取得できないので、スタブをつくってテストできるように、ConvertToHtmlEntityNumberActionクラスのfinal修飾子を削除して、getComponent()メソッドを導入します。以下のコードの部分をメソッドにします。
(※このコードの場合だと上に書いたようにしなくても、actionPerformed(ActionEvent event, JTextComponent component) メソッドを作って、それをactionPerformed(ActionEvent event)から呼び出すようにして、その追加したメソッドをテストすればよさそうです。)
JTextComponent editor = EditorRegistry.lastFocusedComponent();
選択範囲のコードをメソッドにしたい場合は、範囲を選択した状態で、[Alt] + [Enter] でヒントを表示して、メソッドを導入できます。ポップアップウィンドウの項目で[Introduce Method…] (これはNetBeansのリファクタリングの機能です)というのを選択します。
Introduce Method ダイアログが表示されます。NameとAccessを適切に設定してOkボタンを押すと、メソッドが追加されます。
冗長であれば必要に応じて、メソッドの中身を修正します。
テストファイルの作成
テストファイルは、[Tools]メニューの[Create/Update Tests]から作れます。
Create/Update Tests ダイアログが表示されます。必要に応じて変更して(Method Bodyなどいらなければチェックをはずします)OKボタンをクリックすると、テストファイルが作成されます。
コードを書かなければいけないのは、test****()メソッドのbodyです。各メソッドのテストを書きます。ここでは testActionPerformed() のコードを書いていきましょう。
最初にConvertToHtmlEntityNumberActionを継承したテスト用のクラスをConvertToHtmlEntityNumberActionStub を作ります。
private static final class ConvertToHtmlEntityNumberActionStub extends ConvertToHtmlEntityNumberAction { }
getComponent()メソッドをオーバーライドしてテストのデータを変更できるようにしましょう。
メソッドをオーバーライドするにはInsert Code([Alt] + [INS])の機能を使う方法もありますが、1つだけオーバーライドしたい場合は、コード補完からオーバーライドする方法が速いです。クラス内でコード補完のポップアップウィンドウを表示します([Ctrl] + [Space])。項目の中からgetComponent() – overrideを選択すると、メソッドが挿入されます。
以下のようにテストを追加してみました。未実装のテストメソッドや足りないテストケースは追加してみてください。
package com.junichi11.my.netbeans.plugin; import javax.swing.JEditorPane; import javax.swing.text.JTextComponent; import org.junit.After; import org.junit.AfterClass; import static org.junit.Assert.assertEquals; import org.junit.Before; import org.junit.BeforeClass; import org.junit.Test; public class ConvertToHtmlEntityNumberActionTest { public ConvertToHtmlEntityNumberActionTest() { } @Test public void testConverToHtmlEntityNumbers() { // TODO add } @Test public void testActionPerformed() { testActionPerformed("@ほげほげテスト", "@ほげほげテスト", 0, 0); testActionPerformed("@ほげほげテスト", "@ほげほげテスト", 0, 1); // TODO add more tests } private void testActionPerformed(final String original, final String expectedResults, final int selectionStart, final int selectionEnd) { ConvertToHtmlEntityNumberAction action = new ConvertToHtmlEntityNumberActionStub(original); JTextComponent component = action.getComponent(); component.setSelectionStart(selectionStart); component.setSelectionEnd(selectionEnd); action.actionPerformed(null); assertEquals(expectedResults, component.getText()); } private static final class ConvertToHtmlEntityNumberActionStub extends ConvertToHtmlEntityNumberAction { private final JTextComponent editor; public ConvertToHtmlEntityNumberActionStub(String text) { this.editor = text != null ? new JEditorPane("text/plain", text) : null; } @Override JTextComponent getComponent() { return editor; } } }
テストの実行
テストもプロジェクトの実行の時と同様に実行する方法はいくつかあります。
エディタのコンテキストメニュー(右クリック)
- [Run File]([Shift] + [F5])
- [Test File]([Ctrl] + [F6])
[Run] メニュー
- [Run File]([Shift] + [F5])
- [Test File]([Ctrl] + [F6])
- [Test Project]([Alt] + [F6]) (すべてのテスト)
プロジェクトツリーのコンテキストメニュー
ファイル([Test File])だったり、パッケージ([Test Package])だったり、プロジェクト([Test])だったり、選択中のノードによってテストの対象が変わります。
プロジェクトのコンテキストメニューから[Test]を実行してみましょう。テストが成功すればグリーンの表示結果になります。
失敗した場合は、進捗バーがレッドになります。テストが間違ってなければ、実装を修正しましょう。
NbTestCase
別のディレクトリにテストデータを用意しておいて、それらを使ってテストするときなどは、テストクラスにNbTestCase(エラーがでたら依存関係の修正)を継承させて使うと便利だと思います。コンストラクタに引数があるので注意。
Loggerの追加
例外が発生する場合は、処理を正常に終了させ、どこで問題が起きたのかLoggerを追加してログを残しましょう。特定のクラス用のLoggerは、Insert Code([Alt] + [INS])の機能でコードを生成できます。クラス内でInsert Code([Alt] + [INS]) を実行して[Logger…] を選択します。
キャレット位置にコードが生成されます。
ここでは Exceptions.printStackTrace(ex); の部分をLoggerに置き換えてみましょう。よりよいメッセージがあれば、メッセージ部分を変更してください。
try { // remove and insert converted text document.remove(selectionStart, length); document.insertString(selectionStart, convertedText, null); } catch (BadLocationException ex) { // Exceptions.printStackTrace(ex); LOG.log(Level.WARNING, "Incorrect offset: " + ex.offsetRequested(), ex); }
プロジェクトの設定(バージョン等)
プロジェクトのPropertiesでJavaのバージョンやプラグインのバージョン等を設定できます。moduleの依存関係を追加したときと同様に、プロジェクトのコンテキストメニューで[Properties]を選択します。通常必要と思われる部分だけ紹介します。以下AntのプロジェクトのProject Propertiesのダイアログです。
Mavenのプロジェクトを作った場合は、Antの場合とProject Propertiesの項目が異なる部分があります。
Sources
Source Level はJavaのバージョンです。これはヒント等に使用されます。例えば、Java 8からラムダ式を使用できますが、Source Levelを1.7にしておくと、ラムダ式を使っている箇所ではエラー表示されます。また、Source Levelを1.8にしておくと、ラムダ式が使える箇所では、ラムダ式に変更できるよと提案のヒントが表示されます。使いたいJavaのバージョンの文法でエラーになる場合は、ここの設定を確認してみてください。
Display
基本的には、プラグインをインストールする時等に、プラグインの情報として表示されます。
API Versioning
プラグインのバージョンを変更したい場合は、Specification Version を変更してください。プラグインを修正して、新しいバージョンを配布するときは、忘れずにバージョンを変更しましょう。たとえ中身が違っていても、バージョンの情報が同じなら、すでにそのバージョンはインストールされているものとみなされ、プラグインを更新するために、一度インストール済みのプラグインを削除しなければなりません。
Build Packaging
プロジェクト内にライセンスファイルを作って、適切なライセンスを設定しましょう。(例えば、https://github.com/junichi11/netbeans-rainbow-braces を参照)Home Pageには、ソースコードのリポジトリのアドレス等を設定するといいかと思います。
ライセンスヘッダー
ライセンスヘッダーの設定をしていない場合、新規にファイルを作成すると、ファイルの先頭に下記のコメントが追加されます。
/* * To change this license header, choose License Headers in Project Properties. * To change this template file, choose Tools | Templates * and open the template in the editor. */
ファイルはテンプレートを使用して作成されています。どのライセンスを使用するか設定しておくと、このコメントの部分に適切なライセンスヘッダーが挿入されるようになります。
Mavenのプロジェクトの場合はProject Propertiesにライセンスヘッダーを設定できるカテゴリーがあるのですが、Antのプロジェクトにはそれがありません。
その場合は、nbproject/project.propertiesファイルを編集します。
既存のライセンステンプレートを使用する
project.propertiesファイルを開いて、project.license=[license name] を追加してください。例えば、Apache License, Version 2.0を使用したい場合は下記のように書きます。
project.license=apache20
この[license name]の部分は、デフォルトのライセンスのテンプレートのファイル名で決まります。既存のテンプレートのファイル名は license-[license name].txt の形式になっています。この[license name]をproject.licenseに使用できます。ファイル名は、[Tools]メニューの[Templates]からTemplate Managerダイアログを開いて、Licensesの特定のライセンスを右クリックして、Propertiesから確認できます。
独自のライセンステンプレートを使用する
project.propertiesファイルを開いて、project.licensePath=[path to the license template file]を追加してください。例えば下記のように書きます。
project.licensePath=./nbproject/licenseheader.txt
リファレンス
英語ですが下記のチュートリアル動画やWikiが参考になると思います。わからないことや質問があれば、メーリングリストやSlackで聞いてみてください。また、あれはどうやって取得するんだろうと思うことがあれば、NetBeans本体のコードをありそうなメソッド名などでgrepしてみるのも1つの方法です。例えば、getFileObjectなど。
- Apache NetBeans (incubating) API List
- Getting Started with the Apache NetBeans Platform / Apache NetBeans
- DevFaq wiki index
さらなる改善?
これを読んで実際に実行してみる人はいないかもしれませんが、もしいましたら、次のような機能の追加・改善が考えられるので、それらを実装してみるのも練習になると思います。
- 逆の変換の(元の文字列に戻す)アクションを追加する
- 特定の文字は変換の対象からはずす
- このプラグインのためのオプションを追加する
- 文字列が選択されていなかったときに、ダイアログでメッセージを表示する
- 変換するstaticメソッド(convert***)をUtilitiesクラスを作って、そちらに移動する
- ActionListenerではなくEditor用のActionを使って実装・登録してみる
- アクション用のアイコンを追加する
- などなど
長々と書いて、クソコードだったりする部分もあるかもしれませんが、もし何かありましたらご指摘いただけると助かります。
NetBeansとプラグインの作成に興味をもってくれた人がいれば幸いです。