Grape

groovysh でライブラリを参照したかったので Grape について調べた。(Groovy 1.8.0 時点)
参考

groovy.grape.Grape.grab メソッド

スクリプトで使用する @Grab の代わりに groovy.grape.Grape.grab メソッドを使用すればよいみたいだ。
@Grab は AST 変換で static イニシャライザでの Grape.grab 呼び出しに書き換えられているらしい。

@Grab(group='commons-primitives', module='commons-primitives', version='1.0')
import org.apache.commons.collections.primitives.ArrayIntList

a = new ArrayIntList()


groovysh で

groovy:000> import groovy.grape.*
===> [import groovy.grape.*]
groovy:000> Grape.grab(group:'commons-primitives', module:'commons-primitives', version:'1.0')
===> null
groovy:000> import org.apache.commons.collections.primitives.ArrayIntList
===> [import groovy.grape.*, import org.apache.commons.collections.primitives.*]
groovy:000> a = new ArrayIntList()
===> []

使えることは分かったけど、他にもメソッドがあるので調べてみた。

groovy.grape.Grape クラス

まずはクラス本体から。
クラスのコメントには 「Facade to GrapeEngine.」 と書かれている。
GrapeEngine の実装は Apache Ivy を用いたものしかないが別のものにも置き換えられるつくりになっているようだ*1
Grape は GrapeEngine をシングルトンで保持していてそのメソッドを呼び出しているのだがその前に2つのチェックを行っている

  1. システムプロパティ groovy.grape.enable が false に設定されていれば処理を行わない
  2. grab の引数に autoDownload が指定されていなければ システムプロパティ groovy.grape.autoDownload を設定する(デフォルトは true)

@Grab はスクリプトと相性がいいがアプリケーションなどでファイル毎に呼び出されても困るのでそういうときに使えるかもしれない*2

autoDownload は resolver を切り替えるための設定。
true だと downloadGrapes が使われて、false だと cachedGrapes が使われる。

<ivysettings>
  <settings defaultResolver="downloadGrapes"/>
  <resolvers>
    <chain name="downloadGrapes">
      <filesystem name="cachedGrapes">
        <ivy pattern="${user.home}/.groovy/grapes/[organisation]/[module]/ivy-[revision].xml"/>
        <artifact pattern="${user.home}/.groovy/grapes/[organisation]/[module]/[type]s/[artifact]-[revision](-[classifier]).[ext]"/>
      </filesystem>
      <!-- todo add 'endorsed groovy extensions' resolver here -->
      <ibiblio name="codehaus" root="http://repository.codehaus.org/" m2compatible="true"/>
      <ibiblio name="ibiblio" m2compatible="true"/>
      <ibiblio name="java.net2" root="http://download.java.net/maven/2/" m2compatible="true"/>
    </chain>
  </resolvers>
</ivysettings>


他にも classLoader, refObject, validate, noExceptions といったパラメータを指定できる。
ドキュメントに載っていないところでは calleeDepth, preserveFiles, excludes がある。
excludes は @GrabExclude の指定と同じで group と module を指定した Map の List を渡せる。

@Grab('net.sourceforge.htmlunit:htmlunit:2.8')
@GrabExclude('xml-apis:xml-apis')

classLoader の指定は groovy.lang.GroovyClassLoader か org.codehaus.groovy.tools.RootLoader のインスタンスを指定する*3
どちらのインスタンスでもない場合は、grab を呼び出したクラスの getClassLoader() が使われる
*4
@GrabConfig で systemClassLoader を指定できるがどちらのインスタンスでもないので効果はないはずなんだけどどうなんだろう。

 @Grab('mysql:mysql-connector-java:5.1.6'),
 @GrabConfig(systemClassLoader=true)

groovy.grape.Grape.listDependencies メソッド

ライブラリは ClassLoader 単位に管理されているので ClassLoader を指定してライブラリ一覧を取得する。
ところが先程 classLoader を指定しなかったのでもう同じ ClassLoader を指定することができない。
groovysh では1行ごとに新しいクラスローダーでスクリプトを実行しているためだ。
もう1回 grab を実行すれば確認できる。

groovy:000>  Grape.getInstance().@loadedDeps
===> {groovy.lang.GroovyClassLoader$InnerLoader@620e06ce=[groovy.grape.IvyGrabRecord@246ba659]}
groovy:000> Grape.grab(group:'commons-primitives', module:'commons-primitives', version:'1.0')
===> null
groovy:000>  Grape.getInstance().@loadedDeps
===> {groovy.lang.GroovyClassLoader$InnerLoader@620e06ce=[groovy.grape.IvyGrabRecord@246ba659], groovy.lang.GroovyClassLoader$InnerLoader@71a2f5b1=[groovy.grape.IvyGrabRecord@246ba659]}


でも ArrayIntList を使えたということは Class のロードは別の ClassLoader で行われているということか?

groovy:000> ArrayIntList.classLoader
===> groovy.lang.GroovyClassLoader@2a63b2e6


Groovy の ClassLoader まわりのドキュメントが見つけられなかった。
groovysh 自体をロードした ClassLoader はまた別のようだ。

groovy:000> org.codehaus.groovy.reflection.ReflectionUtils.getCallingClass().classLoader
===> org.codehaus.groovy.tools.RootLoader@5013582d

groovy.grape.Grape.resolve メソッド

ライブラリをダウンロードし、そのファイルのパスを取得する。
grab は内部で resolve を呼び出し戻り値を ClassLoader#addURL で設定している。

groovy.grape.Grape.enumerateGrapes メソッド

ダウンロード済みのライブラリの一覧を取得する。

// grape list コマンド から
    Grape.enumerateGrapes().each {String groupName, Map group ->
        group.each {String moduleName, List<String> versions ->
            println "$groupName $moduleName  $versions"
            moduleCount++
            versionCount += versions.size()
        }
    }

groovy.grape.Grape.addResolver メソッド

@GrabResolver と同じで maven リポジトリを指定できる。

@GrabResolver(name='restlet', root='http://maven.restlet.org/')
@Grab(group='org.restlet', module='org.restlet', version='1.1.6')

grape コマンド

grape はコマンドラインからも呼び出すことができる。
実装クラスは org.codehaus.groovy.tools.GrapeMain.groovy

grape install <group> <module> [<version>] [<classifier>]
grape uninstall <group> <module> <version>
grape list
grape resolve [-(a|d|s|i)] (group module version)+

install では何故か resolve ではなく grab が呼び出されていた。
classLoader に設定できることを確認しているのだろうか?
resolve は ant や ivy 用の設定を文字列で出力するためのもの。
uninstall は Grapes のメソッドにはないが GrapeEngine には存在する。
いまのところ jar ファイルと ivy-*.xml だけを cache から削除している。

grapeConfig.xml

Apache Ivy の設定ファイル。
Resolver として downloadGrapes と cachedGrapes を設定しておく必要がある。
システムプロパティでデフォルト以外のファイルを指定できる。
優先順位の高い順に

  1. ${grape.config}
  2. ${grape.root}/grapeConfig.xml
  3. ${groovy.root}/grapeConfig.xml
  4. ${user.home}/.groovy/grapeConfig.xml
  5. groovy.grape パッケージの defaultGrapeConfig.xml

最初は 5 が使われて 4 になるのだと思うがどのタイミングで保存されているのか分からなかった。
Ivy 内部か?

cache ディレクト

変更できるが名前が grapes 固定だった。

  1. ${grape.root}/grapes
  2. ${groovy.root}/grapes
  3. ${user.home}/.groovy/grapes

一応、ivy2/cache にあればダウンロードしないでコピーするようにはしてみたができれば統合したい。

<ivysettings>
  <settings defaultResolver="downloadGrapes"/>
  <resolvers>
    <chain name="downloadGrapes">
      <chain name="cachedGrapes">
        <filesystem name="local-grapes">
          <ivy      pattern="${user.home}/.groovy/grapes/[organisation]/[module]/ivy-[revision].xml"/>
          <artifact pattern="${user.home}/.groovy/grapes/[organisation]/[module]/[type]s/[artifact]-[revision].[ext]"/>
        </filesystem>
        <filesystem name="local-ivy2">
          <ivy      pattern="${user.home}/.ivy2/cache/[organisation]/[module]/ivy-[revision].xml"/>
          <artifact pattern="${user.home}/.ivy2/cache/[organisation]/[module]/[type]s/[artifact]-[revision].[ext]"/>
        </filesystem>
        <url name="local-maven2" m2compatible="true">
          <artifact pattern="file:${user.home}/.m2/repository/[organisation]/[module]/[revision]/[module]-[revision].[ext]"/>
        </url>
      </chain>
      <!-- todo add 'endorsed groovy extensions' resolver here -->
      <ibiblio name="codehaus"  m2compatible="true" root="http://repository.codehaus.org/"/>
      <ibiblio name="ibiblio"   m2compatible="true"/>
      <ibiblio name="java.net2" m2compatible="true" root="http://download.java.net/maven/2/"/>
    </chain>
  </resolvers>
</ivysettings>

その他

Grape を使用しない方法で Maven Ant Tasks を用いて依存関係を解決する方法もあるみたいだ。

追記

version を指定しないか version に * を指定すると latest.default になる。
Ivy でのバージョンの指定方法はいろいろあるみたいだ。

追記 2011-07-22

設定ファイルは以前 ローカルの Maven リポジトリを参照するために自分で配置したようで defaultGrapeConfig.xml がコピーされるわけではない。
昨日リリースされた Groovy 1.8.1 からデフォルトで local の maven リポジトリを参照するようになったので特に設定ファイルを変更する理由もなくなった*5
実際にどこからダウンロードされたか知りたい場合、システムプロパティでログレベルを設定すれば ivy のログを見ることができる。

groovy -Divy.message.logger.level=3 arrayint.groovy

修正 2011-07-31

systemClassLoader の設定が気になっていたので確認してみた。
@GrabConfig で systemClassLoader を true に設定した場合と設定しない場合では動作が違う。
ただし true に設定してもシステムクラスローダーではなくこのとき grab を呼び出した org.codehaus.groovy.tools.RootLoader でロードされる。
これはロードしたクラスの Class#getClassLoader() で確かめられる。


つまり systemClassLoader が false の場合にも何か特別なクラスローダーが設定されているということで GrabAnnotationTransformation を調べてみた。

basicArgs.put("classLoader", loader != null ? loader : sourceUnit.getClassLoader());

sourceUnit のクラスローダーが設定されている。
いつか AST 変換を調べたときに続きを調べる。

*1:現状は GrapeIvy 固定で TODO にサービスプロバイダーについて書かれている

*2:直接 GrapeEngine を呼び出されると意味はなくなるが

*3:superClass をさかのぼるので継承していれば OK

*4:RefrectionUtils で calleeDepth 分さかのぼる。厳密にメソッド呼び出しを数えてないので多分

*5:.ivy2/cache は参照しないけど