No Programming, No Life

新しいNPNLです。http://d.hatena.ne.jp/fumokmm/ から引っ越してきました。

GroovyのConfigSlurperがめっちゃ便利

はじめに

本日参加してきたGrailsドキュメント会にて紹介されていたConfigSlurperをちょっと調べてみたらめっちゃ便利だったのでメモしておきます。*1

で、ConfigSlurperって何?

簡単に説明すると、groovyファイルで各種設定系定義を書いておいて使いましょうというノリのものです。groovy.utilパッケージに含まれているクラスですのでデフォルトでインポートされています。
機能としては

  • ドット区切りで階層指定できたり
  • クロージャで階層指定できたり
  • プロパティファイル(java.util.Properties)からもパースできたり
  • 設定同士をマージできたり
  • 設定をエクスポートできたり
  • 環境によって設定の値を変えたり
  • 変数をバインドしたり

できるようです。では簡単に使用例をば。
動作確認 (Groovy Version: 1.7.5 JVM: 1.6.0_22)

使用例その1.ドット区切りで

config.groovy
user.name = 'fumokmm'
user.kind = 'cat'
user.is.programmer = true

こんな感じでドット区切りで値を指定します。一つ注意としてはdefはつけちゃだめみたいです。

main.groovy
def config = new ConfigSlurper().parse(new File('config.groovy').toURL())

// 値を確認
assert 'fumokmm' == config.user.name
assert 'cat' == config.user.kind
assert config.user.is.programmer
// 型を確認
assert java.lang.String == config.user.name.class
assert java.lang.Boolean == config.user.is.programmer.class

また、設定定義の方も普通のGroovyスクリプトなので、

(1..10).each { num ->
  loop."value$num" = num
}

みたいなこともできちゃったりします。この定義から

loop.value1 = 1
loop.value2 = 2
loop.value3 = 3
loop.value4 = 4
loop.value5 = 5
loop.value6 = 6
loop.value7 = 7
loop.value8 = 8
loop.value9 = 9
loop.value10 = 10

を定義したことになります。さすがGroovy。

使用例その2.クロージャで階層指定

その1でやったのと同じことをクロージャ版でやってみます。

config.groovy
user {
  name = 'fumokmm'
  kind = 'cat'
  is {
    programmer = true
  }
}
main.groovy
def config = new ConfigSlurper().parse(new File('config.groovy').toURL())

// 値を確認
assert 'fumokmm' == config.user.name
assert 'cat' == config.user.kind
assert config.user.is.programmer
// 型を確認
assert java.lang.String == config.user.name.class
assert java.lang.Boolean == config.user.is.programmer.class

さて、対比していただくと対応関係が見えてきますね。階層をドット区切りで表現するかクロージャで表現するかの違いのみというわけですね。






user.name = 'fumokmm'
user.kind = 'cat'
user.is.programmer = true

user {
  name = 'fumokmm'
  kind = 'cat'
  is {
    programmer = true
  }
}


ちなみに、ドット区切りとクロージャは組み合わせ可能ですので、以下は両方とも同じ結果になります。*2





user {
  is {
    programmer = true
  }
}

user {
  is.programmer = true
}

ところで、ConfigSlurperでparseした後の戻り値はConfigObjectというクラスなんですが、これはjava.util.HashMapを継承しているため、実際のところ普通にuser.nameのようにGroovy風にMapアクセスできるというわけです。使用例1,2で取得しているconfigの内容は

["user":["name":"fumokmm", "kind":"cat", "is":["programmer":true]]]

のようになっています。

使用例その3.プロパティファイル(java.util.Properties)からもパース

ConfigSlurper#parseメソッドは引数にPropertiesも取れます。

ひとまずこんなプロパティファイルを用意して

prop1.properties
user.name=fumokmm
user.kind=cat
user.is.programmer=true

こんな感じでプロパティファイルからconfigを生成することが可能です。

def prop1 = new Properties()
prop1.load(new FileInputStream('prop1.properties'))
def config = new ConfigSlurper().parse(prop1)

assert 'fumokmm' == config.user.name
assert 'cat'     == config.user.kind
assert 'true'    == config.user.is.programmer // 注意:'true'のように文字列になっている

プロパティファイルから読み込んでいるため、値にtrueと書いてあってもそれは文字列として扱われるようなので注意が必要です。

さらに、

config.toProperties()

のよう#toProperties()を呼び出すとプロパティファイルに戻せるようです。

ということで、configを加工してプロパティファイルに書き出してみると

config.user.hobby = 'Programming' // 追加
def prop2 = config.toProperties()

prop2.store(new FileOutputStream('prop2.properties'), 'Created using ConfigSlurper.')
結果 (prop2.properties)
#Created using ConfigSlurper.                                                                                          
#Tue Nov 30 00:21:21 JST 2010                                                                                          
user.name=fumokmm
user.kind=cat
user.is.programmer=true
user.hobby=Programming

のようになりました。ちゃんと追加したuser.hobbyがプロパティファイルの方にも追加されて出力されました。

使用例その4.設定同士をマージ

configが2つ以上ある場合、それらを一つにマージすることができます。

config1.groovy
user.name = 'fumokmm'
user.kind = 'cat'
user.is.programmer = true
config2.groovy
user {
  url {
    hatena   = "http://d.hatena.ne.jp/fumokmm/"
    twitter  = "http://twitter.com/#!/fumokmm"
    facebook = "http://www.facebook.com/fumokmm"
  }
}

さて、それではマージしてみましょう。

main.groovy
def config1 = new ConfigSlurper().parse(new File('config1.groovy').toURL())
def config2 = new ConfigSlurper().parse(new File('config2.groovy').toURL())

def config = config1.merge(config2) // マージ!

// 値を確認
assert 'fumokmm' == config.user.name
assert 'cat' == config.user.kind
assert config.user.is.programmer
assert 'http://d.hatena.ne.jp/fumokmm/' == config.user.url.hatena
assert 'http://twitter.com/#!/fumokmm' == config.user.url.twitter
assert 'http://www.facebook.com/fumokmm' == config.user.url.facebook

マージするには#mergeメソッドを使うだけ、すっきり簡単ですね。

ところで、わざわざconfig1とか変数を宣言するのもあれなので、以下みたいなヘルパークロージャを作ってもいいかもしれませんね。

def loadConfig = { file ->
  new ConfigSlurper().parse(new File(file).toURL())
}
def config = loadConfig('config1.groovy').merge(loadConfig('config2.groovy'))
...

使用例その5.設定をエクスポート

お次はconfigのエクスポートです。せっかくなので、使用例その4でマージしたconfigをファイルに出力してみましょう。
(config1.groovyとconfig2.groovyは使用例その4のものを流用)

main.groovy
def loadConfig = { file ->
  new ConfigSlurper().parse(new File(file).toURL())
}
def config = loadConfig('config1.groovy').merge(loadConfig('config2.groovy'))

// ライターを指定
new File('mergedConfig.groovy').withWriter{ writer ->
  config.writeTo(writer)
}

こんな感じで書き出したいconfigの#writeToメソッドを使います。ライターはgroovy.lang.Writableインタフェースの実装クラスならなんでも渡すことができます。
さて、出力はこうなりました。

出力 (mergedConfig.groovy)
user {
    name="fumokmm"
    kind="cat"
    is.programmer=true
    url {
        hatena="http://d.hatena.ne.jp/fumokmm/"
        twitter="http://twitter.com/#!/fumokmm"
        facebook="http://www.facebook.com/fumokmm"
    }
}

整形されて出力されました。いい感じです。

使用例その6.環境によって設定の値を変える

一つの設定ファイルに複数の環境定義を書きたい。そんな場合は以下の例が使えます。

serverEnv.groovy
server {
  setting {
    url = 'http://app/setting'
  }
}

environments {
  development {
    server {
      setting {
        url = 'http://app:8080/setting'
      }
    }
  }
  test {
    server {
      setting {
        url = 'http://app:8081/setting'
      }
    }
  }
}

environmentsという名前でクロージャを定義した場合だけ、特別な動作となります。

main.groovy
def configDev = new ConfigSlurper('development').parse(new File('serverEnv.groovy').toURL())
def configTest = new ConfigSlurper('test').parse(new File('serverEnv.groovy').toURL())

assert 'http://app:8080/setting' == configDev.server.setting.url
assert 'http://app:8081/setting' == configTest.server.setting.url

new ConfigSlurper('development')のようにコンストラクタに指定した文字列にマッチする方のenvironmentsの設定が利用されるようになります。

使用例その7.変数をバインドする

パースする際にあらかじめ指定した変数をバインドすることもできるみたいです。
以下に使用例を示します。

config.groovy
target.path = "${basedir}/path/to/anywhere"
main.groovy
def config = new ConfigSlurper()
config.binding = [basedir: '/root']
config = config.parse(new File('config.groovy').toURL())

assert '/root/path/to/anywhere' == config.target.path

こんな感じで#setBinding*3にてMapを指定することで変数をバインドすることができます。バインドされた値はconfig.groovyの中で利用することができます。

ところで、main.groovyでは#setBindingの戻り値がvoidなので手続き的になってしまっていてカッコ悪いですね。そこで、

main2.groovy
def config = new ConfigSlurper()
  .with{ it.binding = [basedir: '/root']; it }
  .parse(new File('config.groovy').toURL())

assert '/root/path/to/anywhere' == config.target.path

とwithを使えばつなげて書く事ができます。Groovyっぽいね!

まとめ

TODO これらの機能を包含した便利クラスを書き途中

おわりに

ということで、このConfigSlurperを利用すれば、とっても見やすく、表現力に富んだ設定ファイルを書く事が可能となります。積極的に使っていきましょう!
Enjoy your programming life more groovier!

更新履歴

  • 2011-03-10 保留になったままだった使用例その4, その5を書きました。その他、文言など修正。
  • 2010-11-21 新規作成。

*1:これを使えば[http://d.hatena.ne.jp/fumokmm/20101017/1287344928:title=ここ]でやってた定義情報なんかはすっきりGroovyで書けるから今度書き直そうかと思う。

*2:でも、user.is { programmer = true } はダメみたいです。柔軟になんとかならないのかなぁ。

*3:groovyのsetXxxメソッドはxxx=yyyの形に置き換え可能であるため、今回はそれを利用しています。