GradleでEclipseのWTPプロジェクトをマルチプロジェクトで作成したら、WTPプロジェクトが依存するプロジェクトでprovided指定したはずのjarもWEB-INF/libの下に配置されてとても困ったときの話

#環境
##Gradle
1.11

##Eclipse
Juno

#発生した問題
以下のような構成のマルチプロジェクトを作ったとする。

```text:フォルダ構成
├─build.gradle
├─settings.gradle
├─app\
└─web\
```

```groovy:settings.gradle
include 'web', 'app'
```

```groovy:build.gradle
subprojects {
    apply plugin: 'java'
    apply plugin: 'eclipse-wtp'

    sourceCompatibility = '1.7'
    targetCompatibility = '1.7'

    repositories {
        mavenCentral()
    }

    configurations {
        provided
    }

    sourceSets {
        main.compileClasspath += configurations.provided
        test {
            compileClasspath += configurations.provided
            runtimeClasspath += configurations.provided
        }
    }

    eclipse {
        classpath {
            plusConfigurations += configurations.provided
            noExportConfigurations += configurations.provided
        }
    }

    dependencies {
        compile 'org.apache.commons:commons-lang3:3.2.1'

        provided 'javax:javaee-api:7.0'

        testCompile 'junit:junit:4.11', {
            transitive = false
        }
        testCompile 'org.hamcrest:hamcrest-all:1.3'
    }
}

project(':app') {
    dependencies {
        compile 'org.apache.commons:commons-io:1.3.2'
    }

    eclipse {
        project {
            name = 'app'
        }
    }
}

project(':web') {
    apply plugin: 'war'

    archivesBaseName = 'web'

    dependencies {
        compile project(':app')
    }

    eclipse {
        project {
            name = 'web'
        }

        wtp {
            component {
                contextPath = 'web'
            }
            facet {
                facet name: 'java', version: '1.7'
                facet name: 'jst.web', version: '3.0'
            }
        }
    }
}
```

`build.gradle` で設定している依存関係をまとめると、以下のようになる。

|プロジェクト|コンフィグレーション|依存対象（ライブラリ、プロジェクト）|
|:---------|:------------|:------------------|
|app       |compile      |commons-lang 3.2.1 |
|          |             |commons-io 1.3.2   |
|          |provided     |javaee-api 7.0     |
|          |testCompile  |junit 4.11         |
|          |             |hamcrest-all 1.3   |
|web       |compile      |commons-lang 3.2.1 |
|          |             |app ( __project__ )|
|          |provided     |javaee-api 7.0     |
|          |testCompile  |junit 4.11         |
|          |             |hamcrest-all 1.3   |

この設定で `eclipse` タスクを実行して Eclipse プロジェクトを生成し、 Eclipse にインポートする。

そして、 Eclipse から web プロジェクトを AP サーバー（GlassFish）にデプロイする。

GlassFish のドメインフォルダを開き、 Eclipse がデプロイしたアプリケーションの WEB-INF/lib の下を見ると、以下のようになっている。

![webinflib.jpg](https://qiita-image-store.s3.amazonaws.com/0/28302/5febdc94-569a-5594-44d3-6a1179bb9bd1.jpeg)

`provided` で指定している奴はおろか、 `testCompile` で指定している jar まで配備されてしまっている。


##設定についての補足
###provided について
「WAR プラグイン」を使用していれば、 `providedCompile` を指定できる。

しかし、上記構成の場合、 WAR プロジェクトではない app プロジェクトも、実行環境が提供する API （`javaee-api 7.0`）を使用している。
このため、 app プロジェクトには「WAR プラグイン」を使用せずに `providedCompile` 相当の設定をしなければならない。

ところが Gradle は、標準では `providedCompile` 相当の設定を提供していない。

代わりに、以下のように設定することで `providedCompile` を実現できる。

```groovy:自前providedCompile
configurations {
    provided
}

sourceSets {
    main.compileClasspath += configurations.provided
    test {
        compileClasspath += configurations.provided
        runtimeClasspath += configurations.provided
    }
}

eclipse {
    classpath {
        plusConfigurations += configurations.provided
    }
}

dependencies {
    provided 'javax:javaee-api:7.0'
}
```

###app プロジェクトにも eclipse-wtp プラグインを適用している点について
```groovy:build.gradle一部抜粋
subprojects {
    apply plugin: 'java'
    apply plugin: 'eclipse-wtp'

（以下略）
```

このようになっているので、 app プロジェクトにも `eclipse-wtp` プラグインが適用されている。

一見すると app プロジェクトは普通の jar を生成するプロジェクトなので `eclipse` プラグインだけで良いように思える。

しかし、 `eclipse` プラグインだと問題が発生する。

具体的には、 WTP プロジェクトが依存しているプロジェクトに `eclipse` プラグインしか適用していないと、 WTP プロジェクトを Eclipse から AP サーバーにデプロイしたときに、依存しているプロジェクトが AP サーバーに配備されないという現象が発生する。

WTP プロジェクトが依存している他のプロジェクトにも、 `eclipse-wtp` プラグインを適用しないといけない。

__参考__

- [Gradleで陥りやすい問題点の解決策TIPS集 - 猫好きモバイルアプリケーション開発者記録](http://kkoudev.github.io/blog/2014/03/30/gradle-tips/)

#直接の原因
こんなことが起こってしまう直接の原因は、 Gradle が吐き出した Eclipse の設定ファイルにあると思われる。

```xml:appプロジェクトの.classpath
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
	<classpathentry kind="output" path="bin"/>
	<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER" exported="true"/>
	<classpathentry sourcepath="<略>/commons-lang3-3.2.1-sources.jar" kind="lib" path="<略>/commons-lang3-3.2.1.jar" exported="true">
		<attributes>
			<attribute name="org.eclipse.jst.component.dependency" value="../"/>
		</attributes>
	</classpathentry>
（中略）
	<classpathentry sourcepath="<略>/javaee-api-7.0-sources.jar" kind="lib" path="<略>/javaee-api-7.0.jar">
		<attributes>
			<attribute name="org.eclipse.jst.component.dependency" value="../"/>
		</attributes>
	</classpathentry>
（後略）
</classpath>
```

`classpathentry` タグの `exported` 属性が重要で、これが `true` だとその依存関係を公開する――要は、 jar を WEB-INF/lib の下に配備する。

`commons-lang3-3.2.1` は `exported="true"` となっているが、 `javaee-api-7.0.jar` には `exported` 属性が設定されていない。
これは、最初の `build.gradle` で指定していた `noExportConfigurations` の設定が効いているため。

```groovy:
eclipse {
    classpath {
        plusConfigurations += configurations.provided
        noExportConfigurations += configurations.provided // ←これ
    }
}
```

`noExportConfigurations` に追加したコンフィグレーションを持つ依存関係には、 `exported` 属性が設定されなくなる。

ところが、 `exported` が設定されていないにもかかわらず、 `javaee-api-7.0.jar` の `classpathentry` タグの要素には、 `attribute` タグで "org.eclipse.jst.component.dependency" が設定されている。

正確には理解していないけど、この "org.eclipse.jst.component.dependency" は「この依存関係をどこに公開するか」を設定するものっぽく、 `exported` が `true` でないときは設定しないものと思われる。

この "org.eclipse.jst.component.dependency" の指定があるせいで、本来配備されては困る jar が WEB-INF/lib の下に配備されてしまっている模様。
（実際、この `attributes` をごっそり消すと、問題は発生しなくなる）

#解決策
##１．何もしない
例であげた jar だけなら、正直実害は無い。

この現象が起こるのはあくまで Eclipse から AP サーバーにデプロイしたときの話なので、 Gradle の war タスクで war を生成した場合は、 WEB-INF/lib の下に全 jar が配備されるなんて現象は発生しない。

ていうか、たぶん普通に使っているだけだと裏でこんな現象が発生しているなんて気付かずに使い続けると思う。
（今回これを見つけたのも他の問題の原因を調べているときで、偶然だった）

でも、実害が完全にゼロというわけでもない。

AP サーバーの持っている jar とバッティングすることでエラーが起こることも無きにしもあらずだし、なにより実際の生成物とは異なる状態で開発＋動作確認をするというのは気持ち悪い話です。

##２．build.gradle 上で withXml を使ってゴリゴリ .classpath を書き換える

- [configurations.provided with eclipse-wtp plugin](http://forums.gradle.org/gradle/topics/configurations_provided_with_eclipse_wtp_plugin)
- [Gradle + Eclipse: How to not export JARs that are dependencies of a dependency?](http://forums.gradle.org/gradle/topics/gradle_eclipse_how_to_not_export_jars_that_are_dependencies_of_a_dependency)

上記２つのコンボで、なんとか問題を発生させなくできるようになった。

具体的には以下のように build.gradle を記述する。


```groovy:build.gradle
subprojects {
    apply plugin: 'java'
    apply plugin: 'eclipse-wtp'

    sourceCompatibility = '1.7'
    targetCompatibility = '1.7'

    repositories {
        mavenCentral()
    }

    configurations {
        noExport
    }

    sourceSets {
        main.compileClasspath += configurations.noExport
        test {
            compileClasspath += configurations.noExport
            runtimeClasspath += configurations.noExport
        }
    }

    eclipse {
        classpath {
            plusConfigurations += configurations.noExport
            noExportConfigurations += configurations.noExport
        }

        classpath.file.withXml {
            it.asNode().classpathentry.findAll { entry ->
                entry.@kind == 'lib' && !entry.@exported
            }.each { noExportEntry ->
                def attributes = noExportEntry.children()[0]
                if (attributes != null) {
                    def dependency = attributes.attribute.find {it.@name == 'org.eclipse.jst.component.dependency'}

                    if (dependency != null) {
                        attributes.remove(dependency)
                    }
                }
            }
        }
    }

    dependencies {
        compile 'org.apache.commons:commons-lang3:3.2.1'

        noExport 'javax:javaee-api:7.0'

        noExport 'junit:junit:4.11', {
            transitive = false
        }
        noExport 'org.hamcrest:hamcrest-all:1.3'
    }
}

project(':app') {
    dependencies {
        compile 'org.apache.commons:commons-io:1.3.2'
    }

    eclipse {
        project {
            name = 'app'
        }
    }
}

project(':web') {
    apply plugin: 'war'

    archivesBaseName = 'web'

    dependencies {
        compile project(':app')
    }

    eclipse {
        project {
            name = 'web'
        }

        wtp {
            component {
                contextPath = 'web'
            }
            facet {
                facet name: 'java', version: '1.7'
                facet name: 'jst.web', version: '3.0'
            }
        }
    }
}
```

最初のやつとの大きな違いは以下の２つ。

1. `classpath.file.withXml` でクラスパスエントリから `attributes` を無理やり削除している。
2. `provided` と `testCompile` をやめて `noExport` というのを宣言している。

とりあえず、これで app プロジェクトの .classpath は以下のように出力されるようになり、問題は発生しなくなる。

```xml:.classpath
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
	<classpathentry kind="output" path="bin"/>
	<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER" exported="true"/>
	<classpathentry sourcepath="<略>/commons-lang3-3.2.1-sources.jar" kind="lib" path="<略>/commons-lang3-3.2.1.jar" exported="true">
		<attributes>
			<attribute name="org.eclipse.jst.component.dependency" value="../"/>
		</attributes>
	</classpathentry>
	<classpathentry sourcepath="<略>/commons-io-1.3.2-sources.jar" kind="lib" path="<略>/commons-io-1.3.2.jar" exported="true">
		<attributes>
			<attribute name="org.eclipse.jst.component.dependency" value="../"/>
		</attributes>
	</classpathentry>
	<classpathentry sourcepath="<略>/javaee-api-7.0-sources.jar" kind="lib" path="<略>/javaee-api-7.0.jar">
		<attributes/>
	</classpathentry>
	<classpathentry sourcepath="<略>/junit-4.11-sources.jar" kind="lib" path="<略>/junit-4.11.jar">
		<attributes/>
	</classpathentry>
	<classpathentry sourcepath="<略>/hamcrest-all-1.3-sources.jar" kind="lib" path="<略>/hamcrest-all-1.3.jar">
		<attributes/>
	</classpathentry>
	<classpathentry sourcepath="<略>/activation-1.1-sources.jar" kind="lib" path="<略>/activation-1.1.jar">
		<attributes/>
	</classpathentry>
	<classpathentry sourcepath="<略>/javax.mail-1.5.0-sources.jar" kind="lib" path="<略>/javax.mail-1.5.0.jar">
		<attributes/>
	</classpathentry>
</classpath>
```

`exported="true"` のものだけ `attributes` が残って、それ以外は除去されている。


----

以下、調べてて分かったこととか。

#根本的な問題
調べてみると、関係してそうな問題はチラホラ見つけるも、英語力が無くて理解できず。

- [WTP Plugin should add org.eclipse.jst.component.dependency to all WEB-INF/lib deps and not add them to the WTP component file](http://issues.gradle.org/browse/GRADLE-2123)
- [Eclipse plugin should eliminate Eclipse warning: Classpath entry ... will not be exported or published](http://issues.gradle.org/browse/GRADLE-1707)

仕方ないのでソースを見に行ったら、 "org.eclipse.jst.component.dependency" を設定しているっぽいところを見つけた。

```groovy:EclipseWtpPlugin.groovy
private void configureEclipseClasspathForWarPlugin(Project project) {
    project.plugins.withType(WarPlugin) { // ①
        project.eclipse.classpath.containers WEB_LIBS_CONTAINER

        project.eclipse.classpath.file.whenMerged { Classpath classpath ->
            for (entry in classpath.entries) { // ②
                if (entry instanceof AbstractLibrary) {
                    //this is necessary to avoid annoying warnings upon import to Eclipse
                    //the .classpath entries can be marked all as non-deployable dependencies
                    //because the wtp component file declares the deployable dependencies
                    entry.entryAttributes[AbstractClasspathEntry.COMPONENT_NON_DEPENDENCY_ATTRIBUTE] = '' // ③
                }
            }
        }

        doLaterWithEachDependedUponEclipseProject(project) { Project otherProject -> // ④
            otherProject.eclipse.classpath.file.whenMerged { Classpath classpath -> 
                for (entry in classpath.entries) { // ⑤
                    if (entry instanceof AbstractLibrary) {
                        // '../' and '/WEB-INF/lib' both seem to be correct (and equivalent) values here
                        //this is necessary so that the depended upon projects will have their dependencies
                        // deployed to WEB-INF/lib of the main project.
                        entry.entryAttributes[AbstractClasspathEntry.COMPONENT_DEPENDENCY_ATTRIBUTE] = '../' // ⑥
                    }
                }
            }
        }
    }
}
```

`COMPONENT_DEPENDENCY_ATTRIBUTE` が "org.eclipse.jst.component.dependency" を保持している定数。

たぶん、以下のような処理をしている。

1. このプロジェクトに WAR プラグインが適用されていれば、
2. プロジェクトの全 classpath エントリに対して、
3. `"org.eclipse.jst.component.nondependency"=""` を設定する。
4. さらに、このプロジェクトが依存している他のプロジェクトに対して、
5. そのプロジェクトの全 classpath エントリに対して、
6. `"org.eclipse.jst.component.dependency"="../"` を設定する。

つまり、WTP と WAR の両方のプラグインが適用されているプロジェクトがあった場合、その WTP プロジェクトが依存している他のプロジェクトが持っている classpath エントリには、そのエントリが `exported=true` を持つかどうかに関わらず、問答無用で `"org.eclipse.jst.component.dependency"="../"` を設定しているっぽい。
（コメントの英語がまさに、「必要だから全部 WEB-INF/lib に配備するよ」って宣言してる）

#Gradle のコードを修正してみる
`exported` をチェックすればいいのかと以下のように実装を変更してみた。

```groovy:EclipseWtpPlugin.groovy
doLaterWithEachDependedUponEclipseProject(project) { Project otherProject ->
    otherProject.eclipse.classpath.file.whenMerged { Classpath classpath ->
        for (entry in classpath.entries) {
            if (entry instanceof AbstractLibrary) {
                if (entry.exported) { // ★ exported が true のときだけ attributes を設定するように修正
                    entry.entryAttributes[AbstractClasspathEntry.COMPONENT_DEPENDENCY_ATTRIBUTE] = '../'
                }
            }
        }
    }
}
```

これで `eclipse` タスクを実行すると `.classpath` は以下のようになる。

```xml:.classpath
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
	<classpathentry kind="output" path="bin"/>
	<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER" exported="true"/>
	<classpathentry sourcepath="<略>/commons-lang3-3.2.1-sources.jar" kind="lib" path="<略>/commons-lang3-3.2.1.jar" exported="true">
		<attributes>
			<attribute name="org.eclipse.jst.component.dependency" value="../"/>
		</attributes>
	</classpathentry>
	<classpathentry sourcepath="<略>/junit-4.11-sources.jar" kind="lib" path="<略>/junit-4.11.jar" exported="true">
		<attributes>
			<attribute name="org.eclipse.jst.component.dependency" value="../"/>
		</attributes>
	</classpathentry>
	<classpathentry sourcepath="<略>/hamcrest-all-1.3-sources.jar" kind="lib" path="<略>/hamcrest-all-1.3.jar" exported="true">
		<attributes>
			<attribute name="org.eclipse.jst.component.dependency" value="../"/>
		</attributes>
	</classpathentry>
	<classpathentry sourcepath="<略>/commons-io-1.3.2-sources.jar" kind="lib" path="<略>/commons-io-1.3.2.jar" exported="true">
		<attributes>
			<attribute name="org.eclipse.jst.component.dependency" value="../"/>
		</attributes>
	</classpathentry>
	<classpathentry sourcepath="<略>/javaee-api-7.0-sources.jar" kind="lib" path="<略>/javaee-api-7.0.jar"/>
	<classpathentry sourcepath="<略>/activation-1.1-sources.jar" kind="lib" path="<略>/activation-1.1.jar"/>
	<classpathentry sourcepath="<略>/javax.mail-1.5.0-sources.jar" kind="lib" path="<略>/javax.mail-1.5.0.jar"/>
</classpath>
```

`provided` 指定していた依存関係は、 "org.eclipse.jst.component.dependency" がなくなったが、テストで使用するライブラリが残ってしまっている。

なので、以下のようにしてテストで使用している依存関係も `noExportConfigurations` に追加してみる。

```groovy:build.gradle
eclipse {
    classpath {
        plusConfigurations += configurations.provided
        noExportConfigurations += [configurations.provided, configurations.testRuntime] // testRuntime も追加
    }
}
```

`.classpath` を出力すると、

```xml:.classpath
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
	<classpathentry kind="output" path="bin"/>
	<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER" exported="true"/>
	<classpathentry sourcepath="<略>/commons-lang3-3.2.1-sources.jar" kind="lib" path="<略>/commons-lang3-3.2.1.jar"/>
	<classpathentry sourcepath="<略>/junit-4.11-sources.jar" kind="lib" path="<略>/junit-4.11.jar"/>
	<classpathentry sourcepath="<略>/hamcrest-all-1.3-sources.jar" kind="lib" path="<略>/hamcrest-all-1.3.jar"/>
	<classpathentry sourcepath="<略>/commons-io-1.3.2-sources.jar" kind="lib" path="<略>/commons-io-1.3.2.jar"/>
	<classpathentry sourcepath="<略>/javaee-api-7.0-sources.jar" kind="lib" path="<略>/javaee-api-7.0.jar"/>
	<classpathentry sourcepath="<略>/activation-1.1-sources.jar" kind="lib" path="<略>/activation-1.1.jar"/>
	<classpathentry sourcepath="<略>/javax.mail-1.5.0-sources.jar" kind="lib" path="<略>/javax.mail-1.5.0.jar"/>
</classpath>
```

`compile` で指定していた分まで配備されなくなってしまった。

このままだと、 app プロジェクトが必要としている `commons-io` が WEB-INF/lib に配備されず、実行時にエラーが発生してしまう。

`exported` を `false` に切り替えているのは以下の部分。

```groovy:ExportedEntriesUpdater.groovy
void updateExported(List<ClasspathEntry> classpathEntries, List<String> noExportConfigNames) {
    classpathEntries.each { // ①
        if (it instanceof AbstractLibrary || it instanceof ProjectDependency) {
            if (noExportConfigNames.contains(it.declaredConfigurationName)) { // ②
                it.exported = false // ③
            }
        }
    }
}
```

1. クラスパスの各エントリに対して、
2. そのエントリの `declaredConfigurationName` が `noExportConfigNames` で指定した名前に含まれていたら、
3. `exported` を `false` にしている。

動作を確認したところ、 `compile` を指定しているエントリも、ここの処理に来た時点で `declaredConfigurationName` が `testRuntime` になっていた。

このため、 `noExportConfigNames` に `testRuntime` を指定すると、 `compile` を指定しているエントリも `export` 対象外になってしまった模様。

いろいろ試したけど、疲れたのでこの辺でコードを修正するのはあきらめた。