Blog

[java] SnakeYaml の機能を利用して Java でも YAML の custom tag を利用する

YAML には tag という型を規定する機能がある。 ! が文書固有の型で !! がグローバルな型。 see http://yaml.org/type/

h2o の 2.1 では以下のような設定ファイルが書けるようになるそうです。 !file がカスタムタグになっていて、設定されているハンドラによってファイルが読み込まれる。

hosts:
  "example.com":
    listen:
      port: 443
      ssl: !file default_ssl.conf
    paths:
      ...
  "example.org":
    listen:
      port: 443
      ssl:
        <<: !file default_ssl.conf
        certificate-file: /path/to/example.org.crt
        key-file:         /path/to/example.org.crt
    paths:
      ...

同じことを jackson-dataformat-yaml でやろうと思ったが、これは無理。issue は上がっている

jackson-dataformat-yaml の元になっている SnakeYaml を直接使えばいけるので、直接使う。

package me.geso.tinyconfig;

import org.yaml.snakeyaml.Yaml;
import org.yaml.snakeyaml.constructor.AbstractConstruct;
import org.yaml.snakeyaml.constructor.SafeConstructor;
import org.yaml.snakeyaml.nodes.Node;
import org.yaml.snakeyaml.nodes.Tag;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Paths;

public class ImportableConstructor extends SafeConstructor {
    private Yaml yaml;

    public ImportableConstructor() {
        this.yamlConstructors.put(new Tag("!file"), new FileConstruct());
        this.yamlConstructors.put(new Tag("!resource"), new ResourceConstruct());
    }

    public void setYaml(Yaml yaml) {
        this.yaml = yaml;
    }

    public Yaml getYaml() {
        if (this.yaml == null) {
            throw new IllegalStateException("You must set Yaml object to ImportableConstructor.");
        }
        return this.yaml;
    }

    private class FileConstruct extends AbstractConstruct {
        @Override
        public Object construct(Node nnode) {
            org.yaml.snakeyaml.nodes.ScalarNode snode = (org.yaml.snakeyaml.nodes.ScalarNode) nnode;
            String fileName = snode.getValue();
            try (BufferedReader bufferedReader = Files.newBufferedReader(Paths.get(fileName))) {
                return getYaml().load(bufferedReader);
            } catch (IOException e) {
                throw new YamlImportFailedException(fileName, snode.getTag(), e);
            }
        }
    }

    private class ResourceConstruct extends AbstractConstruct {
        @Override
        public Object construct(Node nnode) {
            org.yaml.snakeyaml.nodes.ScalarNode snode = (org.yaml.snakeyaml.nodes.ScalarNode) nnode;
            String resourceName = snode.getValue();
            try (InputStream resourceAsStream = getClass().getClassLoader().getResourceAsStream(resourceName)) {
                return getYaml().load(resourceAsStream);
            } catch (IOException e) {
                throw new YamlImportFailedException(resourceName, snode.getTag(), e);
            }
        }
    }

    public static class YamlImportFailedException extends RuntimeException {
        public YamlImportFailedException(String fileName, Tag tag, IOException cause) {
            super("Cannot load " + tag.getValue() + " from " + fileName + " : " + cause.getClass().getCanonicalName() + " : " + cause.getMessage(), cause);
        }
    }
}

利用側はこんな感じ。

        this.yaml = new Yaml(importableConstructor);
        importableConstructor.setYaml(yaml);

情報が少ないので難儀するが、頑張れば実装できました。

Java なので、!file!resource をじっそうして、!resource の方は classpath からロードできるようにしているところがオシャレポイントです。