Java

JavaのYAMLライブラリのSnakeYamlを使う

最近はプロパティファイルを書くときに人間が読みやすくて記述量が少ないという理由でYAMLをよく使っています。
インデントが違うとエラーになりますが、その規約のお陰で記述量の少なさと読みやすさにつながっていると思います。
JavaでYAMLを読み書きするためのライブラリはSpringBootでも使われているSnakeYamlが有名だと思います。
SpringBootは特に何もしなくてもプロパティからJavaObjectを作ってくれるのでSnakeYamlを意識することはあんまりないのですが、今回素で触る機会があったので色々触ってみました。
ソースコードはGitHubをご参照ください。
beeete2/snakeyaml-examples

今回は以下のようなファイルで色々試してみたいと思います。
[python]
name: server1
hosts:
- hostid: '1'
name: host1
items:
- itemid: '1'
key: item1
startDate: 2017-05-01
endDate: 2017-05-31
- itemid: '2'
key: item2
startDate: 2017-05-01
endDate: 2017-05-31
[/python]

yamlファイルをロード

yamlファイルをロードするときは非常に簡単です。
キーと値のペアのMapに変換されます。リスト型はListに変換されます。
また日付はDateオブジェクトに変換されるようです。

[java]
String text = readDefaultText();

Yaml yaml = new Yaml();
Map map = (Map) yaml.load(text);

softly.assertThat(getString(map, "name")).isEqualTo("server1");

List hosts = (List) map.get("hosts");
softly.assertThat(hosts).hasSize(1);

Map host = (Map) hosts.get(0);
softly.assertThat(getString(host, "name")).isEqualTo("host1");

List items = (List) host.get("items");
softly.assertThat(items).hasSize(2);
System.out.println(items);

Map item = (Map) items.get(0);
softly.assertThat(getString(item, "key")).isEqualTo("item1");
softly.assertThat(item.get("startDate")).isInstanceOf(Date.class); // 日付はDateオブジェクトになる
[/java]

yamlファイルをロードし任意の型にマッピングする

多分、多くの使い方がこの使い方だと思います。
任意の型にマッピングすることができます。
このときに日付をLocalDateTime型にマッピングしたいと思います。

任意の型をマッピングするときはConstructorを継承したクラスを作成してYamlインスタンス生成時のコンストラクタに指定します。
[java]
// LocalDateTimePropertyConstructor
class LocalDateTimePropertyConstructor extends Constructor {
public LocalDateTimePropertyConstructor() {
yamlClassConstructors.put(NodeId.scalar, new TimeStampConstruct());
}

class TimeStampConstruct extends Constructor.ConstructScalar {
@Override
public Object construct(Node node) {
if (node.getTag().equals(Tag.TIMESTAMP)) {
Construct dateConstructor = yamlConstructors.get(Tag.TIMESTAMP);
Date date = (Date) dateConstructor.construct(node);
LocalDateTime localDateTime = LocalDateTime.ofInstant(date.toInstant(), ZoneId.of("UTC"));
return localDateTime;
} else {
return super.construct(node);
}
}
}
}

// Yamlインスタンスの生成
Yaml yaml = new Yaml(new LocalDateTimePropertyConstructor());
[/java]

以下のようにマッピングできます。
[java]
String text = readDefaultText();

Server server = main.loadAs(text, Server.class);
softly.assertThat(server.name).isEqualTo("server1");
softly.assertThat(server.hosts).hasSize(1);

Host host = server.hosts.get(0);
softly.assertThat(host.name).isEqualTo("host1");
softly.assertThat(host.items).hasSize(2);

Item item = host.items.get(0);
softly.assertThat(dateFormat(item.startDate)).isEqualTo("2017-05-01T00:00:00"); // LocalDateTimeになる
softly.assertThat(dateFormat(item.endDate)).isEqualTo("2017-05-31T00:00:00");// LocalDateTimeになる
[/java]

もちろんオブジェクトからyamlファイルへダンプすることもできます。

ダンプ

[java]
Server server = createServer();
Yaml yaml = new Yaml();
String text = yaml.dump(server);
softly.assertThat(text).isNotNull();
[/java]

こうなります。
[python]
!!b3.snakeyaml.entities.Server
hosts:
- hostid: '1'
items:
- endDate: {}
itemid: '1'
key: item1
startDate: {}
- endDate: {}
itemid: '2'
key: item2
startDate: {}
name: host1
name: server1
[/python]
LocalDateTimeの値が変換できていないようです。
対象のオブジェクト(クラス)の完全修飾名が表示されています。
あと、これは好みかもしれませんが、プロパティにインデントがないのが少し気になります。
できれば、こうしたい。
[python]
hosts:
- hostid: '1'
items:
- endDate: 2016-05-31
itemid: '1'
key: item1
startDate: 2016-05-01
- endDate: 2016-05-31
itemid: '2'
key: item2
startDate: 2016-05-01
name: host1
name: server1
[/python]

書式と任意の型を変換する

まず書式(フォーマット)はDumperOptionsのインスタンスを生成し好みに設定します。
色々設定できるみたいなのですが、インデントとスタイルの設定は以下のようにしました。
[java]
DumperOptions options = new DumperOptions();
options.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK); // ブロックスタイルにする
options.setIndicatorIndent(2); // プロパティのインデントを2に設定する
options.setIndent(4); // インデントを4に設定する
[/java]

任意の型の変換はRepresenterを使って設定します。
LocalDateTimeを変換する例は以下のようになります。
[java]
class LocalDateTimeRepresenter extends Representer {

public LocalDateTimeRepresenter() {
multiRepresenters.put(LocalDateTime.class, new RepresentLocalDateTime());
}

private class RepresentLocalDateTime extends RepresentDate {
public Node representData(Object data) {
LocalDateTime localDateTime = (LocalDateTime) data;

String date = localDateTime.format(DateTimeFormatter.ofPattern("yyyy-MM-dd"));
return representScalar(getTag(data.getClass(), Tag.TIMESTAMP), date, null);
}
}
}
[/java]

以下のようになります。
[java]
// LocalDateTimeの変換設定
Representer representer = new LocalDateTimeRepresenter();
representer.addClassTag(Server.class, Tag.MAP); // 完全修飾名を表示しない

// 書式の設定
DumperOptions options = new DumperOptions();
options.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK);
options.setIndicatorIndent(2);
options.setIndent(8);

Yaml yaml = new Yaml(representer, options);
return yaml.dump(object);
[/java]
実行すると以下のようになります。
[python]
hosts:
- hostid: '1'
items:
- endDate: 2016-05-31
itemid: '1'
key: item1
startDate: 2016-05-01
- endDate: 2016-05-31
itemid: '2'
key: item2
startDate: 2016-05-01
name: host1
name: server1
[/python]

まとめ

大体望むようなyamlファイルを生成することができました。
あとはフィールドの並び順の調整が出来ればよかったのですが、そこまでは対応しませんでした。
ちなみに並び順はJavaBeansのアルファベット順になっているみたいです。

define order for JavaBean properties. Order may be very informative: for instance ID or name are normally expected to be the first. (Currently it is alphabetically sorted by default.)

Dumping a custom YAML document

-Java

© 2024 ビー鉄のブログ Powered by AFFINGER5