SnakeYAML で YAML を扱う

# 概要
Java で YAML を扱ってみます。YAML 操作用ライブラリには、 @hryshtk さんの「[JavaのYAMLライブラリ比較](http://qiita.com/hryshtk/items/94e9110e05b53a7915ca)」を拝見したところ、[SnakeYAML](https://bitbucket.org/asomov/snakeyaml/wiki/Documentation)がよさそうなので、これを使ってみました。

なお、YAMLがどんなものかについては [YAML(Wikipedia)](https://ja.wikipedia.org/wiki/YAML) へのリンクを記すにとどめます。

## 依存の追加
ドキュメントだと oss.sonatype からダウンロードするように書いてありますが、[Maven Central](http://search.maven.org/#artifactdetails%7Corg.yaml%7Csnakeyaml%7C1.17%7Cbundle)にも置いてあったので、そちらを参照します。

```groovy:build.gradle
dependencies {
    compile 'org.yaml:snakeyaml:1.17'
```

## 準備
dump にする用のオブジェクトを用意します。

```java:Person.java
package jp.toastkid.verification;

public class Person {
    String firstName;
    String familyName;
    String nationality;
    int age;
}
```

## dump
オブジェクトを yaml に変換してみます。

```java:Client
import org.yaml.snakeyaml.Yaml;

public class SnakeYamlVerificaation {
    public static void main(final String[] args) {
        final Person p = new Person();
        p.firstName    = "John";
        p.familyName   = "Kay";
        p.nationality  = "UK";
        p.age          = 28;
        final Yaml y = new Yaml();
        System.out.println(y.dump(p));
    }
}
```

実行してみると……

```:実行すると下記の通り失敗する
Exception in thread "main" org.yaml.snakeyaml.error.YAMLException: No JavaBean properties found in jp.toastkid.verification.Person
	at org.yaml.snakeyaml.introspector.PropertyUtils.getPropertiesMap(PropertyUtils.java:87)
	at org.yaml.snakeyaml.introspector.PropertyUtils.createPropertySet(PropertyUtils.java:110)
	at org.yaml.snakeyaml.introspector.PropertyUtils.getProperties(PropertyUtils.java:102)
	at org.yaml.snakeyaml.introspector.PropertyUtils.getProperties(PropertyUtils.java:94)
	at org.yaml.snakeyaml.representer.Representer.getProperties(Representer.java:246)
	at org.yaml.snakeyaml.representer.Representer$RepresentJavaBean.representData(Representer.java:50)
	at org.yaml.snakeyaml.representer.BaseRepresenter.representData(BaseRepresenter.java:105)
	at org.yaml.snakeyaml.representer.BaseRepresenter.represent(BaseRepresenter.java:64)
	at org.yaml.snakeyaml.Yaml.dumpAll(Yaml.java:242)
	at org.yaml.snakeyaml.Yaml.dumpAll(Yaml.java:206)
	at org.yaml.snakeyaml.Yaml.dump(Yaml.java:181)
	at jp.toastkid.verification.SnakeYamlVerificaation.main(SnakeYamlVerificaation.java:12)
```

SnakeYAMLで扱うクラスには Getter & Setter が要るようです。 Person クラスに Getter & Setter を(Eclipse の自動生成機能により)追加して再度実行します。

```:実行結果
!!jp.toastkid.verification.Person {age: 28, familyName: Kay, firstName: John, nationality: UK}
```

## load
先ほどの実行結果の文字列からオブジェクトを復元してみます。

```java:load
final Yaml y = new Yaml();
final Person loaded = (Person) y.load("!!jp.toastkid.verification.Person {age: 28, familyName: Kay, firstName: John, nationality: UK}");
System.out.printf("%s %s(%s), age = %d", loaded.firstName, loaded.familyName, loaded.nationality, loaded.age);
```

読み込むデータのクラスを指定することができないのはちょっと不便に思ったら、ちゃんと loadAs という class を引数に持つメソッドが用意されていました。通常はこちらを使うことになるでしょう。

```java:loadAs
final Person p1 = y.loadAs("!!jp.toastkid.verification.Person {age: 28, familyName: Kay, firstName: John, nationality: UK}", Person.class);
```

```:実行結果
John Kay(UK), age = 28
```

## 複数オブジェクトを扱う

### dump
Person を多少拡張し(後述)、List に詰めて dump してみます。

```java
final List<Person> people = Arrays.asList(
                new Person.Builder().setFirstName("John").setFamilyName("Kay")
                        .setNationality("UK").setAge(28).build(),
                new Person.Builder().setFirstName("Don").setFamilyName("Hall")
                        .setNationality("US").setAge(28).build(),
                new Person.Builder().setFirstName("Ken").setFamilyName("Rodriguez")
                        .setNationality("AR").setAge(28).build());
final Yaml y = new Yaml();
System.out.println(y.dump(people));
```

```:実行結果
- !!jp.toastkid.verification.Person {age: 28, familyName: Kay, firstName: John, nationality: UK}
- !!jp.toastkid.verification.Person {age: 28, familyName: Hall, firstName: Don, nationality: US}
- !!jp.toastkid.verification.Person {age: 28, familyName: Rodriguez, firstName: Ken,
  nationality: AR}
```

### 複数のオブジェクトを dump した yaml を load
こういう時にヒアドキュメントが使えない Java は地味につらいと思います。

```java
final List<Person> loaded = y.loadAs(
        "- !!jp.toastkid.verification.Person {age: 28, familyName: Kay, firstName: John, nationality: UK}\n"
        + "- !!jp.toastkid.verification.Person {age: 28, familyName: Hall, firstName: Don, nationality: US}\n"
        + "- !!jp.toastkid.verification.Person {age: 28, familyName: Rodriguez, firstName: Ken,"
        + "  nationality: AR}", List.class);
loaded.forEach(p -> System.out.printf("%s %s(%s), age = %d\n",
                p.getFirstName(), p.getFamilyName(), p.getNationality(), p.getAge())
```

```:実行結果
John Kay(UK), age = 28
Don Hall(US), age = 28
Ken Rodriguez(AR), age = 28
```

### Person クラスの修正
オブジェクト生成をやりやすくするために Builder Pattern を実装してみたところ、どうも Setter と Getter は両方持たせないとダメなようで、おまけに load する際には空のコンストラクタが必要なため、こんなに長くなってしまいました……

```java:Person.java
public class Person {
    private String firstName;
    private String familyName;
    private String nationality;
    private int    age;

    public static class Builder {

        private String firstName;
        private String familyName;
        private String nationality;
        private int    age;

        public Builder setFirstName(final String firstName) {
            this.firstName = firstName;
            return this;
        }

        public Builder setFamilyName(final String familyName) {
            this.familyName = familyName;
            return this;
        }

        public Builder setNationality(final String nationality) {
            this.nationality = nationality;
            return this;
        }

        public Builder setAge(final int age) {
            this.age = age;
            return this;
        }

        public Person build() {
            return new Person(this);
        }
    }

    public Person() {
        /* NOP. */
    }

    private Person(final Builder b) {
        this.firstName   = b.firstName;
        this.familyName  = b.familyName;
        this.nationality = b.nationality;
        this.age         = b.age;
    }

    public String getFirstName() {
        return firstName;
    }

    public String getFamilyName() {
        return familyName;
    }

    public String getNationality() {
        return nationality;
    }

    public int getAge() {
        return age;
    }

    public void setFirstName(final String firstName) {
        this.firstName = firstName;
    }

    public void setFamilyName(final String familyName) {
        this.familyName = familyName;
    }

    public void setNationality(final String nationality) {
        this.nationality = nationality;
    }

    public void setAge(final int age) {
        this.age = age;
    }

}
```

## YAMLファイルからの読み込み
下記のような yaml ファイルを用意し、

```yaml:people.yaml
- !!jp.toastkid.verification.Person {age: 28, familyName: Kay, firstName: John, nationality: UK}
- !!jp.toastkid.verification.Person {age: 28, familyName: Hall, firstName: Don, nationality: US}
- !!jp.toastkid.verification.Person {age: 28, familyName: Rodriguez, firstName: Ken,
  nationality: AR}
```

loadAs の引数に FileInputStream を指定します。

```java
public static void main(final String[] args) throws IOException {
    final Yaml y = new Yaml();
    try (final InputStream in = Files.newInputStream(Paths.get("people.yaml"))) {
        final List<Person> loaded = y.loadAs(in, List.class);
        loaded.forEach(p -> System.out.printf("%s %s(%s), age = %d\n",
                    p.getFirstName(), p.getFamilyName(), p.getNationality(), p.getAge())
                );
    }
}
```

```:実行結果
John Kay(UK), age = 28
Don Hall(US), age = 28
Ken Rodriguez(AR), age = 28
```

----

# 参考
1. [JavaのYAMLライブラリ比較](http://qiita.com/hryshtk/items/94e9110e05b53a7915ca)
2. [公式ドキュメント](https://bitbucket.org/asomov/snakeyaml/wiki/Documentation)