アプリケーションの独自プロパティ編集でコード補完ができるようにする

# 概要

Spring Bootの設定ファイル(application.properteis or .yml)の編集をEclipseやIntelliJ IDEAなどのIDE上で行うと、プロパティ名や値をコード補完できます。

**注**

* Eclipse、Spring Tool Suiteなどでは未確認です。もしかしたらバージョンやプラグインの有無によりコード補完ができないかもしれません。
* 確認した環境はIntelliJ IDEA 2017.3 Ultimate Editon (+ Spring Support Plugin)です。

IntelliJ IDEAではこの図のように<kbd>Ctrl + Space</kbd>でコード補完できます。

![cc1.png](https://qiita-image-store.s3.amazonaws.com/0/22772/1de90c25-4f79-41db-1965-b63dd42c996d.png)

この記事はコード補完をアプリケーションの独自プロパティでも利用できるにするための手順といくつかのtipsについて説明します。

**環境**

* Windows 10 Professional
* Java 1.8.0_162
* Spring Boot 2.0.0.RC2
* IntelliJ IDEA 2017.3 Ultimate Edition

**参考**

* [Spring Boot Reference | Appendix B. Configuration Metadata] (https://docs.spring.io/spring-boot/docs/2.0.0.RC2/reference/htmlsingle/#configuration-metadata)


# メタデータファイル

Spring Bootのjarファイルには、設定ファイル(application.properties or .yml)のプロパティのメタデータファイルが含まれていて、IDEでのコード補完に利用されています。
アプリケーションの独自プロパティもメタデータファイルを作成することで同じようにコード補完ができるようになります。

メタデータファイルはMETA-INFディレクトリ下に配置し、ファイル名はspring-configuration-metadata.jsonです。ただしこのファイルは自動的に生成されるものを使用するので基本的に手動で編集しません。

## メタデータファイルのフォーマット

メタデータファイルはjsonフォーマットで記述します。大まかなアウトラインは次のようになっています。

```json:spring-configuration-metadata.json
{
  "property": [
    {
      "name": "The full name of the property. Names are in lower-case period-separated form.",
      "type": "The full signature of the data type of the property but also a full generic type.",
      "description": "A short description of the group that can be displayed to users.",
      "defaultValue": "The default value, which is used if the property is not specified."
    }
  ],
  "groups": [
    {
      "name": "The full name of the group. This attribute is mandatory.",
      "type": "The class name of the data type of the group. ",
      "description": "A short description of the group that can be displayed to users."
    }
  ],
  "hints": [
    {
      "name": "The full name of the property to which this hint refers.",
      "values": [
        "value": "A valid value for the element to which the hint refers.",
        "description": "A short description of the value that can be displayed to users."
      ],
      "providers": [
        "name": "The name of the provider to use to offer additional content assistance for the element to which the hint refers."
      ] 
    }
  ]
}
```

## 独自プロパティのメタデータファイルを作る

プロジェクトの依存関係にspring-boot-configuration-processorを追加します。

***Maven***

```xml
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-configuration-processor</artifactId>
  <optional>true</optional>
</dependency>
```

***Gradle***

```groovy
compileOnly("org.springframework.boot:spring-boot-configuration-processor")
```

次に、プロパティをバインドするクラスにConfigurationPropertiesアノテーションを付加します。
コンパイル時に、このアノテーションが付いたクラス定義からメタデータファイルが自動的に生成されます。

```java
@Component
@ConfigurationProperties(prefix = "my-app.my-module.foo")
@Validated
public class FooProperties {

    /**
     * foo name description.
     */
    @NotNull
    private String name;

    /**
     * foo max height description.
     */
    @NotNull
    private Integer maxHeight;

    /**
     * foo min height description.
     */
    @NotNull
    private Integer minHeight;

    // getter/setterは省略
}
```

このクラスをコンパイルして生成されたメタデータファイルです。

```json:spring-configuration-metadata.json
{
  "hints": [],
  "groups": [
    {
      "sourceType": "com.example.demomavenspring2.configure.FooProperties",
      "name": "my-app.my-module.foo",
      "type": "com.example.demomavenspring2.configure.FooProperties"
    }
  ],
  "properties": [
    {
      "sourceType": "com.example.demomavenspring2.configure.FooProperties",
      "name": "my-app.my-module.foo.max-height",
      "description": "foo max height description.",
      "type": "java.lang.Integer"
    },
    {
      "sourceType": "com.example.demomavenspring2.configure.FooProperties",
      "name": "my-app.my-module.foo.min-height",
      "description": "foo min height description.",
      "type": "java.lang.Integer"
    },
    {
      "sourceType": "com.example.demomavenspring2.configure.FooProperties",
      "name": "my-app.my-module.foo.name",
      "description": "foo name description.",
      "type": "java.lang.String"
    }
  ]
}
```

この状態で基本的なコード補完ができるようになっています。
コード補完はこの図のようになります。表示されているプロパティの説明文はJavaDocが利用されます。

![cc2.png](https://qiita-image-store.s3.amazonaws.com/0/22772/955811a5-41c8-5d17-c0e7-b027cde09448.png)

### コード補完をカスタマイズする

コード補完をカスタマイズするにはMETA-INFディレクトリ下にadditional-spring-configuration-metadata.jsonという名前でカスタマイズ用のメタデータファイルを作成します。
このファイルはコンパイル時にspring-configuration-metadata.jsonへマージされます。

#### 既定値の中から選択したい場合

例えばnameプロパティの値を既定値の中から選択したい場合、additional-spring-configuration-metadata.jsonに次のヒントを設定することでnameプロパティの値を"simple","combine","complex"から選択できるようになります。

```json:additional-spring-configuration-metadata.json
{
  "hints" [
    {
      "name": "my-app.my-module.foo.name",
      "values": [
        {
          "value": "simple",
          "description": "simple module description."
        },
        {
          "value": "combine",
          "description": "combine module description."
        },
        {
          "value": "complex",
          "description": "complex module description."
        }
      ]
    }
  ]
}
```

既定値がコード補完されるようになりました。

![cc3.png](https://qiita-image-store.s3.amazonaws.com/0/22772/0ee2f3f1-c446-c5d9-bac1-3c94585e0386.png)

既定値以外を入力すると警告メッセージが表示されます。

![cc4.png](https://qiita-image-store.s3.amazonaws.com/0/22772/b0ed0a31-191a-7804-ba0c-abc1fd50a6d0.png)

#### 既定値に加え任意の値を設定したい場合

既定値に加え任意の値を指定できるようにすることもできます。次のように"providers"要素を追加します。

```json:additional-spring-configuration-metadata.json
{
  "hints" [
    {
      "name": "my-app.my-module.foo.name",
      "values": [
        // 省略
      ],
      "providers": [
        {
          "name": "any"
        }
      ]
    }
  ]
}
```

既定値がコード補完されつつ、且つ既定値以外も設定できるようになりました。

![cc5.png](https://qiita-image-store.s3.amazonaws.com/0/22772/d3c94667-618b-a322-4364-37464f115f2c.png)

#### Enumを使用する

既定値から選択させたい場合、もっと簡単な方法はフィールドの型にEnumを使用することです。
この方法ではadditional-spring-configuration-metadata.jsonの編集は不要です。

```java
/**
 * foo color description.
 */
@NotNull
private Color color;
```

```java
/**
 * color description.
 */
public enum Color {
    /**
     * red description.
     */
    RED,
    /**
     * green description.
     */
    GREEN,
    /**
     * blue description.
     */
    BLUE,
    /**
     * yellow description.
     */
    YELLOW,
    /**
     * cyan description.
     */
    CYAN,
    /**
     * magenta description.
     */
    MAGENTA
}
```

コード補完

![cc6.png](https://qiita-image-store.s3.amazonaws.com/0/22772/8b17227d-4229-ab89-f224-e71f52ec324a.png)

#### コレクションを使用する

**注意点**

フィールドがコレクション型の場合、設定ファイルのフォーマット(.properties or .yml)の違いによりいくつか問題が出ます。


##### List型

フィールドがList型で、その要素にEnumを使用した場合、メタデータのヒントは不要です。

```java
/**
 * foo colors description.
 */
private List<Color> colors = new ArrayList<>();
```

コード補完

![cc7.png](https://qiita-image-store.s3.amazonaws.com/0/22772/b3be9462-e489-282e-c2c3-c95672b05b83.png)

**注意点(1)**

コード補完されるようになるのですが、Enumに定義されていない値を指定してもIDE上ではエラーになりません。
この例ではEnumに定義されていない"white"を指定してもエラーになりません。

![cc8.png](https://qiita-image-store.s3.amazonaws.com/0/22772/d83aed85-94fa-5269-6ef1-b0387354e468.png)

ただ、実行時にプロパティのバインドで失敗してエラーになります。

```text
***************************
APPLICATION FAILED TO START
***************************

Description:

Failed to bind properties under 'my-app.my-module.foo.colors[2]' to com.example.demomavenspring2.Color:

    Property: my-app.my-module.foo.colors[2]
    Value: white
    Origin: class path resource [application.yml]:76:11
    Reason: Failed to convert from type [java.lang.String] to type [com.example.demomavenspring2.Color] for value 'white'; nested exception is java.lang.IllegalArgumentException: No enum constant com.example.demomavenspring2.Color.white

Action:

Update your application's configuration. The following values are valid:

    BLUE
    CYAN
    GREEN
    MAGENTA
    RED
    YELLOW
```

**注意点(2)**

.properties形式のファイルでList型のプロパティに複数の値を記述する場合、カンマ区切りで値を記述することができますが、コード補完が有効だとIDE上でエラー扱いになります。（アプリケーションの実行には問題なくプロパティはバインドされます。）

```text
my-app.my-module.foo.colors=red, green, blue
```

![cc0.png](https://qiita-image-store.s3.amazonaws.com/0/22772/5522bc2f-c9f4-a56e-8329-acbb52350826.png)

この場合は次のように配列形式で記述することで回避できます。冗長ですがコード補完が有効です。

```text
my-app.my-module.foo.colors[0]= red
my-app.my-module.foo.colors[1]= green
my-app.my-module.foo.colors[2]= blue
```

![cc1.png](https://qiita-image-store.s3.amazonaws.com/0/22772/6c381c3d-8541-c16f-911b-9bc55ed7228a.png)

##### Set型

フィールドがSet型で、その要素にEnumを使用した場合は、メタデータのヒントが必要です。

```java
/**
 * foo colors description.
 */
private Set<Color> colors = new HashSet<>();
```

Enumの列挙子がコード補完されるようにするには以下のヒントを記述します。

```json
{
  "hints": [
    {
      "name": "my-app.my-module.foo.colors",
      "providers": [
        {
          "name": "handle-as",
          "parameters": {
            "target": "com.example.demomavenspring2.Color"
          }
        }
      ]
    }
  ]
}
```

**注意点(1)**

List同様にコード補完はされますが、Enumに定義されていない値を指定してもエラーにならいないので注意が必要です。

**注意点(2)**

List同様にカンマ区切りで複数の値を記述するとIDE上ではエラー扱いになります。
（アプリケーションの実行には問題ありません。）

```text
my-app.my-module.foo.colors= red, green, blue
```

また、Listのときの回避策である配列形式で記述すると実行時にエラーになるので使えません。

#### プロパティを非推奨にする

あるプロパティの利用が非推奨であることをIDE上で表示させることができます。
アクセサにDeprecatedアノテーションを付け、getterにDeprecatedConfigurationPropertyアノテーションを付けます。
reason属性に非推奨の理由を記述することもできます。
（additional-spring-configuration-metadata.jsonの編集は不要です。）

```java
/**
 * foo budget description.
 */
private Long budget;

@DeprecatedConfigurationProperty(reason = "reason not to use budget.")
@Deprecated
public Long getBudget() {
    return budget;
}

@Deprecated
public void setBudget(Long budget) {
    this.budget = budget;
}
```

コード補完時に取り消し線が表示されます。

![cc9.png](https://qiita-image-store.s3.amazonaws.com/0/22772/749b7078-e19e-7242-d31b-16ef4635c91e.png)

アノテーションに指定した非推奨の理由が表示されます。

![cca.png](https://qiita-image-store.s3.amazonaws.com/0/22772/f5791a24-22ee-541c-aef2-385027a6c2c1.png)

#### マップを使用する

```java
/**
 * foo parts description.
 */
private Map<Part, State> parts = new HashMap<>();
```

```java
public enum Part {
    UPPER,
    LOWER,
    RIGHT,
    LEFT
}

public enum State {
    START,
    STOP,
    SUSPEND
}
```

##### .properties形式の場合

キーと値にEnum型を使った場合、メタデータのヒント無しでコード補完ができます。

![cc2.png](https://qiita-image-store.s3.amazonaws.com/0/22772/0f7d88e3-51d9-2b95-a19f-fbf1ebf43aad.png)

![cc3.png](https://qiita-image-store.s3.amazonaws.com/0/22772/770d7e08-0a02-7073-ea71-b53b77fd771c.png)

##### .yml形式の場合

.yml形式ではキーと値にEnum型を使った場合、キーのコード補完が期待通りに動きませんでした。

![cc4.png](https://qiita-image-store.s3.amazonaws.com/0/22772/37aef103-3f1b-504b-bccc-a079e0af2c59.png)

**注意点(1)**

キーにEnumに定義されていない値を記述してもIDE上ではエラー扱いになりません。
この例の"middle"というキーはEnum Partには定義されていません。

![cc5.png](https://qiita-image-store.s3.amazonaws.com/0/22772/ace32f4b-5040-2640-2a8a-5eb93a5a6ff0.png)

##### マップの値をString型にして既定値をコード補完させる

```java
/**
 * foo parts description.
 */
private Map<Part, String> parts = new HashMap<>();
```

マップの値をコード補完させるには、メタデータにヒントを記述します。
name属性にはプロパティ名(この例ではparts)に続けて".values"を付加します。

```json:additional-spring-configuration-metadata.json
{
  "hints": [
    {
      "name": "my-app.my-module.foo.parts.values",
      "values": [
        {
          "value": "TRUE",
          "description": "parts enable description."
        },
        {
          "value": "FALSE",
          "description": "parts disable description."
        }
      ]
    }
  ]
}
```

コード補完

![cc6.png](https://qiita-image-store.s3.amazonaws.com/0/22772/3960ece3-0e68-b999-c985-ac281757a214.png)

![cc7.png](https://qiita-image-store.s3.amazonaws.com/0/22772/f9986f54-2546-671b-200d-9fedef578416.png)