IntelliJ/Android StudioのGradleプラグインを拡張してみた。


#前置き

以前書いた、[IntelliJ/Android Studio用にHide Tool Windows Exというプラグインを作って公開してみた。](http://qiita.com/shiraji/items/5a7a1654dc7b6da20a60)の反応から、ニーズがないとわかっていますが、だからなんだ、俺が面白いからいいんだ！ということで、突き進みます。一応それでも前回の反省を踏まえ、このエントリーでは失敗した流れや試行錯誤した経緯とか全部省略し、OSSとして公開している箇所の技術説明にフォーカスしようと思います。

今回はIntelliJにデフォルトで組み込まれているgradleプラグインを拡張するプラグイン[Gradle Confirmation](https://plugins.jetbrains.com/plugin/7848?pr=)を作成してみました。ソースコードは[こちら(GitHub)](https://github.com/shiraji/GradleConfirmation)。毎度のことですが、GitHubのスターくれるとやる気が増えます。

#環境構築

通常のplugin開発と違い、特定のpluginの拡張を行う場合、そのpluginのjarファイルをclasspathに追加する必要があります。

`File > Project Structure... > SDKs > IntelliJ Plugin SDK(ここはSDK追加時に名前を変えられる。プラグイン開発で使っているSDK）> Classpathタブ > + > /Applications/IntelliJ IDEA 14 CE.app/Contents/plugins/gradle/lib/gradle.jar`

![project_structure.png](https://qiita-image-store.s3.amazonaws.com/0/20058/63c25875-64d4-870b-bfd8-b5705d55ac0e.png "project_structure.png")


#プラグインの拡張方法

IntelliJのプラグインはextensionPoints内でそのプラグイン自体の拡張ポイントを作成することが可能です。
IntelliJのGradleプラグインは以下のような拡張ポイントがあります。

```intellij-community/plugins/gradle/src/META-INF/plugin.xml
  <extensionPoints>
    <extensionPoint name="projectResolve" interface="org.jetbrains.plugins.gradle.service.project.GradleProjectResolverExtension"/>
    <extensionPoint name="taskManager" interface="org.jetbrains.plugins.gradle.service.task.GradleTaskManagerExtension"/>
    <extensionPoint name="resolve.contributor" interface="org.jetbrains.plugins.gradle.service.resolve.GradleMethodContextContributor"/>
    <extensionPoint name="settingsControlProvider" interface="org.jetbrains.plugins.gradle.service.settings.GradleSettingsControlProvider"/>
    <extensionPoint name="importCustomizer" interface="org.jetbrains.plugins.gradle.service.project.GradleImportCustomizer"/>
    <extensionPoint name="frameworkSupport" interface="org.jetbrains.plugins.gradle.frameworkSupport.GradleFrameworkSupportProvider"/>
  </extensionPoints>
```

今回利用する拡張ポイントは`taskManager`です。これはGradleタスクが実行される直前に呼ばれる拡張ポイントで、まさにこのプラグインのためのものでした。

（拡張ポイントの探し方はとりあえず、Gradleプラグインが提供している全ての拡張ポイントを拡張したログを吐くだけのクラスを作成し、全メソッドにブレークポイントを置き、実際にデバッグモードで走らせてみました。それからコードを読んで利用する箇所を把握しました。まさに手探りでした。）

拡張ポイントを利用する定義をplugin.xmlに記載します。

```plugin.xml
    <depends>org.jetbrains.plugins.gradle</depends>
    
    <extensions defaultExtensionNs="com.intellij">
    </extensions>

    <extensions defaultExtensionNs="org.jetbrains.plugins.gradle">
        <taskManager implementation="com.github.shiraji.gradleconfirmation.task.GradleConfirmationTaskManager"/>
    </extensions>
```


通常と異なっているのは以下の3点

* Gradleプラグインに依存するということを`<depends>`で定義する
* defaultExtensionNsの値はGradleプラグインのIDである、org.jetbrains.plugins.gradleを指定する
* `<extensions>`の子要素に拡張ポイントには、extensionPointのnameの値である、`<taskManager>`タグを設定する。そのタグ内で、実装したクラスを`implementation`に明記する。(もし、resolve.contributorを拡張したい場合は、`<resolve.contributor>`タグを作成する。)

#拡張ポイントを実装する

`intellij-community/plugins/gradle/src/META-INF/plugin.xml`にある、`<extensionPoint name="taskManager"`で記載されているinterfaceは`org.jetbrains.plugins.gradle.service.project.GradleProjectResolverExtension`であるため、plugin.xmlで定義した`com.github.shiraji.gradleconfirmation.task.GradleConfirmationTaskManager`はGradleTaskManagerExtensionの子クラスとする必要があります。

```com.github.shiraji.gradleconfirmation.task.GradleConfirmationTaskManager.java
package com.github.shiraji.gradleconfirmation.task;

public class GradleConfirmationTaskManager implements GradleTaskManagerExtension {
```

GradleTaskManagerExtensionはシンプルなinterfaceで以下の定義がされています。

```GradleTaskManagerExtension.java
public interface GradleTaskManagerExtension {
    ExtensionPointName<GradleTaskManagerExtension> EP_NAME = ExtensionPointName.create("org.jetbrains.plugins.gradle.taskManager");

    boolean executeTasks(@NotNull ExternalSystemTaskId var1, @NotNull List<String> var2, @NotNull String var3, @Nullable GradleExecutionSettings var4, @NotNull List<String> var5, @NotNull List<String> var6, @Nullable String var7, @NotNull ExternalSystemTaskNotificationListener var8) throws ExternalSystemException;

    boolean cancelTask(@NotNull ExternalSystemTaskId var1, @NotNull ExternalSystemTaskNotificationListener var2) throws ExternalSystemException;
}
```

executeTasksがGradleタスク実行前に呼ばれるメソッド、cancelTaskがキャンセル時に呼ばれるメソッドです。
引数が何に値するのか、戻り値のbooleanの意味はここからでは読み取ることが出来ません。
そこでこのinterfaceが呼ばれている箇所を見てみます。

```GradleTaskManager.java
    if (ExternalSystemApiUtil.isInProcessMode(GradleConstants.SYSTEM_ID)) {
      for (GradleTaskManagerExtension gradleTaskManagerExtension : GradleTaskManagerExtension.EP_NAME.getExtensions()) {
        if (gradleTaskManagerExtension.executeTasks( // ここでexecuteTasksが呼び出される。
          id, taskNames, projectPath, settings, vmOptions, scriptParameters, debuggerSetup, listener)) {
          return;
        }
      }
    }
    // ここから実際にGradleタスクを実行する処理がつらつらと記載されている
```

このコードから、戻り値がtrueの場合、タスクを実行せずに止まり、戻り値がfalseの場合、そのまま実行されることがわかります。
第2引数が実行されるtaskの名前を含んだ`List<String>`であることもわかりました。
`ExternalSystemTaskId`という第1引数でplugin開発でよく利用する、Projectインスタンスを取得できます。

ここまでわかってやっとプラグインの開発を進めることができました。

ポップアップにはSwingのJOptionPaneを利用しました。

```GradleConfirmationTaskManager.java
        int optionDialog = JOptionPane.showOptionDialog(null, questionMessage, "Gradle Confirmation",
                JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE, null, new String[]{"Yes", "No"}, null);

        if (optionDialog == JOptionPane.YES_OPTION) {
            return START_EXECUTE;
        } else {
            showCancelMessage(taskListNames);
            return STOP_EXECUTE;
        }
```

# 設定の保存方法

設定の保存方法は前回の[Hide Tool Windows Ex](https://plugins.jetbrains.com/plugin/7836?pr=)と違い、プロジェクトベースにしました。
ただし、PersistStateComponentだとなぜかうまく保存できる時とできない時がありました。(環境の問題？？？)
今回は単純にこのプラグインをenable/disableにするフラグだけだったので、一番シンプルな設定保存方法である、PropertiesComponentを使って保存する方法を採用しました。

このPropertiesComponentは全プラグインで共通の場所でK-Vで保存します。
公式ではそのプラグイン固有のprefixをつけて、バッディングを避けて下さいと記載されています。
（IntelliJさんがPrefixにプラグインIDつければいいだけなんじゃあ・・・ゲフンゲフン）

```GradleConfirmationConfig.java
public class GradleConfirmationConfig {

    public static final String IS_ENABLE_CONF_KEY // キーはOSSだけど一応省略しておきます。

    public static boolean isSelected(Project project) {
        return PropertiesComponent.getInstance(project).getBoolean(IS_ENABLE_CONF_KEY, true);
    }

    public static void setSelected(Project project, boolean isEnable) {
        PropertiesComponent.getInstance(project).setValue(IS_ENABLE_CONF_KEY, String.valueOf(isEnable));
    }
}
```

という感じで、設定周りのラッパークラスを作成し、staticメソッドにそこら中からアクセスしていくようにしました。
相変わらず、thread何それおいしいの？状態ですが、問題になるまで置いておきます。

これであとはいつも通り、実装が完了したら、IntelliJ Plugin Repositoryに公開しておきました。

# 最後に

このプラグインは拡張プラグインのサンプルとして作成しました。
自分の誤クリックから作るに至ったプラグインですが、なかなかシンプルにやれたと思います。
IntelliJプラグインの作成にちょっと慣れてきました。
プラグイン開発するなら、intellij-communityのソースは必須だなと改めて認識しました。
cloneにすごい時間かかるので、寝る前にcloneしておくと良いです。

i18nどうすればいいのかさっぱりで、確認ポップアップの文言、思いっきりコードに直接記載してあります。
いくつかのプラグインもi18nしてるっぽいのだけど、自分の環境では英語だしな・・・とこれも問題になったら考えようと思います。

テスト作らなきゃー。未調査です。。。