gradle eclipse が生成する .classpath ファイルで相対パスを出力する

## 概要

手元で docker + eclipse + eclim + vim な環境を構築しようとしている関係で、 `gradle eclipse` が書き出す `.classpath` ファイル内のファイルパスを絶対パスから相対パスに書き換えるようにしてみました。

## 準備

```
$ mkdir javajava/ && cd javajava/
```

docker コンテナから gradle を利用できる用のファイルを作成する。

**Dockerfile**:

```dockerfile
FROM openjdk:8
WORKDIR /usr/local/app
ENV M2_HOME=/apache-maven-3.5.4/
ENV PATH=$PATH:/apache-maven-3.5.4/bin/:/gradle-4.8.1/bin/
RUN cd / && \
    wget http://ftp.jaist.ac.jp/pub/apache/maven/maven-3/3.5.4/binaries/apache-maven-3.5.4-bin.tar.gz && \
    tar xzvf apache-maven-3.5.4-bin.tar.gz && \
    rm apache-maven-3.5.4-bin.tar.gz
RUN cd / && \
    wget https://downloads.gradle.org/distributions/gradle-4.8.1-bin.zip && \
    unzip gradle-4.8.1-bin.zip && \
    rm gradle-4.8.1-bin.zip
```

**docker-compose.yml**:

```yaml

version: "3"
services:
  java8:
    build: .
    volumes:
      - .:/usr/local/app
      - .m2/:/root/.m2/
      - .gradle/:/root/.gradle/
```

ホスト側の `./.gradle` をコンテナ側の `/root/.gradle/` にマウントして、ホスト側にもキャッシュするようにしているので、別の eclimd コンテナからこのプロジェクトを参照する際に、 `.classpath` 内のパスが相対パスになっていてほしい、という感じ。

で、上記ファイルを作ると、とりあえず下記が実行できるようになる。

```
$ docker-compose run --rm java8 gradle tasks 
```

## だめなパターン

まずは、適当なビルドスクリプトを作成して、`gradle eclipse` で `.classpath` を書き出してみる。

**build.gradle**:

```
apply plugin: 'eclipse'
apply plugin: 'java-library'

repositories {
    mavenCentral()
}

dependencies {
    compile group: 'log4j', name: 'log4j', version: '1.2.17'
}
```

```
$ docker-compose run --rm java8 gradle eclipse
(略)
$ cat .classpath 
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
        <classpathentry kind="output" path="bin/default"/>
        <classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.8/"/>
        <classpathentry sourcepath="/root/.gradle/caches/modules-2/files-2.1/log4j/log4j/1.2.17/677abe279b68c5e7490d6d50c6951376238d7d3e/log4j-1.2.17-sources.jar" kind="lib" path="/root/.gradle/caches/modules-2/files-2.1/log4j/log4j/1.2.17/5af35056b4d257e4b64b9e8069c0746e8b08629f/log4j-1.2.17.jar">
                <attributes>
                        <attribute name="gradle_used_by_scope" value="main,test"/>
                </attributes>
        </classpathentry>
</classpath>
```

上記のようにパスが `/root/.gradle` になってしまっていて、ほかでは使えない設定になってしまっている。

## `.classpath` を相対パスで書き出す

ここで `build.gradle` に下記を追記する。

```groovy
import java.util.regex.Pattern
import org.gradle.plugins.ide.eclipse.model.Library
import org.gradle.plugins.ide.eclipse.model.internal.FileReferenceFactory

eclipse {
    classpath {
        file {
            whenMerged { classpath ->
                classpath.getEntries().each {
                    if (it instanceof Library) {
                        def path = it.getPath()
                        def sourcePath = it.getSourcePath()
                        def pattern = Pattern.compile(gradle.gradleUserHomeDir.absolutePath)
                        it.setPath(path.replaceFirst(pattern, ".gradle"))
                        if(sourcePath) {
                            def newSourcePath = sourcePath.path.replaceFirst(pattern, ".gradle")
                            def fileReference = new FileReferenceFactory().fromPath(newSourcePath)
                            it.setSourcePath(fileReference)
                        }
                    }
                }
            }
        }
    }
}
```

`gradle eclipse` を再実行してみる。

```
$ docker-compose run --rm java8 gradle eclipse                                                                        (略)
$ cat .classpath 
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
        <classpathentry kind="output" path="bin/default"/>
        <classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.8/"/>
        <classpathentry sourcepath=".gradle/caches/modules-2/files-2.1/log4j/log4j/1.2.17/677abe279b68c5e7490d6d50c6951376238d7d3e/log4j-1.2.17-sources.jar" kind="lib" path=".gradle/caches/modules-2/files-2.1/log4j/log4j/1.2.17/5af35056b4d257e4b64b9e8069c0746e8b08629f/log4j-1.2.17.jar">
                <attributes>
                        <attribute name="gradle_used_by_scope" value="main,test"/>
                </attributes>
        </classpathentry>
</classpath>
```

`root/.gradle` だったのが `.gradle` になったので、java8 コンテナの外でも使える感じになった。

## 参考資料

* [Eclipse Classpath Editing — eclim \(eclipse \+ vim\)](http://eclim.org/vim/java/classpath.html)
    * `.classpath` に書いてある内容の意味を把握するのに便利だった
* [EclipseClasspath \- Gradle DSL Version 4\.10](https://docs.gradle.org/current/dsl/org.gradle.plugins.ide.eclipse.model.EclipseClasspath.html)
    * `gradle eclipse` 実行時に `.classpath` の中身を書き換えるために必要な、`whenMerged` フックが紹介されている
* [gradle/FileReferenceFactory\.java at master · gradle/gradle](https://github.com/gradle/gradle/blob/master/subprojects/ide/src/main/java/org/gradle/plugins/ide/eclipse/model/internal/FileReferenceFactory.java)
   * gradle 的に classpathentry の sourcePath 属性のほうだけよく分からないけど FileReference というインタフェースで表現してた
* [FileReference \(Gradle API 4\.10\)](https://docs.gradle.org/current/javadoc/org/gradle/plugins/ide/eclipse/model/FileReference.html)
   * 上記のインタフェース
* [How do I create a concrete instance of FileReference? \- Old Forum \- Gradle Forums](https://discuss.gradle.org/t/how-do-i-create-a-concrete-instance-of-filereference/1707/2)
   * 動的に FileReference を実装したクラスのオブジェクトを生成する方法