Skip to content

Commit d0b5c28

Browse files
committed
Add 'requiresDirectivesFromMetadata' option for module() patching
See #40
1 parent 7742c70 commit d0b5c28

File tree

7 files changed

+275
-0
lines changed

7 files changed

+275
-0
lines changed

src/main/java/org/gradlex/javamodule/moduleinfo/ExtraJavaModuleInfoPlugin.java

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,9 @@
2323
import org.gradle.api.artifacts.Configuration;
2424
import org.gradle.api.artifacts.component.ComponentIdentifier;
2525
import org.gradle.api.artifacts.component.ModuleComponentIdentifier;
26+
import org.gradle.api.artifacts.result.DependencyResult;
2627
import org.gradle.api.artifacts.result.ResolvedArtifactResult;
28+
import org.gradle.api.artifacts.result.ResolvedDependencyResult;
2729
import org.gradle.api.attributes.Attribute;
2830
import org.gradle.api.attributes.Category;
2931
import org.gradle.api.attributes.Usage;
@@ -104,6 +106,8 @@ private void configureTransform(Project project, ExtraJavaModuleInfoPluginExtens
104106
private void registerTransform(String fileExtension, Project project, ExtraJavaModuleInfoPluginExtension extension, Configuration javaModulesMergeJars, Attribute<String> artifactType, Attribute<Boolean> javaModule) {
105107
// all Jars have a javaModule=false attribute by default; the transform also recognizes modules and returns them without modification
106108
project.getDependencies().getArtifactTypes().maybeCreate(fileExtension).getAttributes().attribute(javaModule, false);
109+
Configuration compileClasspath = project.getConfigurations().getByName(JavaPlugin.COMPILE_CLASSPATH_CONFIGURATION_NAME);
110+
Configuration runtimeClasspath = project.getConfigurations().getByName(JavaPlugin.RUNTIME_CLASSPATH_CONFIGURATION_NAME);
107111

108112
// register the transform for Jars and "javaModule=false -> javaModule=true"; the plugin extension object fills the input parameter
109113
project.getDependencies().registerTransform(ExtraJavaModuleInfoTransform.class, t -> {
@@ -116,12 +120,36 @@ private void registerTransform(String fileExtension, Project project, ExtraJavaM
116120
javaModulesMergeJars.getIncoming().artifactView(v -> v.lenient(true)).getArtifacts().getArtifacts());
117121
p.getMergeJarIds().set(artifacts.map(new IdExtractor()));
118122
p.getMergeJars().set(artifacts.map(new FileExtractor(project.getLayout())));
123+
p.getCompileClasspathDependencies().set(project.provider(() ->
124+
compileClasspath.getIncoming().getResolutionResult().getAllComponents().stream().collect(Collectors.toMap(
125+
c -> ga(c.getId()),
126+
c -> c.getDependencies().stream().map(ExtraJavaModuleInfoPlugin::ga).collect(Collectors.toSet())
127+
))));
128+
p.getRuntimeClasspathDependencies().set(project.provider(() ->
129+
runtimeClasspath.getIncoming().getResolutionResult().getAllComponents().stream().collect(Collectors.toMap(
130+
c -> ga(c.getId()),
131+
c -> c.getDependencies().stream().map(ExtraJavaModuleInfoPlugin::ga).collect(Collectors.toSet())
132+
))));
119133
});
120134
t.getFrom().attribute(artifactType, fileExtension).attribute(javaModule, false);
121135
t.getTo().attribute(artifactType, "jar").attribute(javaModule, true);
122136
});
123137
}
124138

139+
private static String ga(DependencyResult d) {
140+
if (d instanceof ResolvedDependencyResult) {
141+
return ga(((ResolvedDependencyResult) d).getSelected().getId());
142+
}
143+
return d.getRequested().getDisplayName();
144+
}
145+
146+
private static String ga(ComponentIdentifier id) {
147+
if (id instanceof ModuleComponentIdentifier) {
148+
return ((ModuleComponentIdentifier) id).getGroup() + ":" + ((ModuleComponentIdentifier) id).getModule();
149+
}
150+
return id.getDisplayName();
151+
}
152+
125153
private static class IdExtractor implements Transformer<List<String>, Collection<ResolvedArtifactResult>> {
126154
@Override
127155
public List<String> transform(Collection<ResolvedArtifactResult> artifacts) {

src/main/java/org/gradlex/javamodule/moduleinfo/ExtraJavaModuleInfoPluginExtension.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,4 +105,15 @@ public void automaticModule(String identifier, String moduleName, @Nullable Acti
105105
}
106106
getModuleSpecs().put(identifier, automaticModuleName);
107107
}
108+
109+
/**
110+
* Let the plugin know about an existing module on the module path.
111+
* This may be needed when 'requiresDirectivesFromMetadata(true)' is used.
112+
*
113+
* @param coordinates group:name coordinates
114+
* @param moduleName the Module Name of the Module referred to by the coordinates
115+
*/
116+
public void knownModule(String coordinates, String moduleName) {
117+
getModuleSpecs().put(coordinates, new KnownModule(coordinates, moduleName));
118+
}
108119
}

src/main/java/org/gradlex/javamodule/moduleinfo/ExtraJavaModuleInfoTransform.java

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,10 @@ public interface Parameter extends TransformParameters {
8888
ListProperty<String> getMergeJarIds();
8989
@InputFiles
9090
ListProperty<RegularFile> getMergeJars();
91+
@Input
92+
MapProperty<String, Set<String>> getCompileClasspathDependencies();
93+
@Input
94+
MapProperty<String, Set<String>> getRuntimeClasspathDependencies();
9195
}
9296

9397
@InputArtifact
@@ -283,13 +287,41 @@ private byte[] addModuleInfo(ModuleInfo moduleInfo, Map<String, List<String>> pr
283287
classWriter.visit(Opcodes.V9, Opcodes.ACC_MODULE, "module-info", null, null, null);
284288
String moduleVersion = moduleInfo.getModuleVersion() == null ? version : moduleInfo.getModuleVersion();
285289
ModuleVisitor moduleVisitor = classWriter.visitModule(moduleInfo.getModuleName(), Opcodes.ACC_OPEN, moduleVersion);
290+
286291
for (String packageName : autoExportedPackages) {
287292
moduleVisitor.visitExport(packageName, 0);
288293
}
289294
for (String packageName : moduleInfo.exports) {
290295
moduleVisitor.visitExport(packageName.replace('.', '/'), 0);
291296
}
297+
292298
moduleVisitor.visitRequire("java.base", 0, null);
299+
300+
if (moduleInfo.getRequiresDirectivesFromMetadata()) {
301+
Set<String> compileDependencies = getParameters().getCompileClasspathDependencies().get().get(moduleInfo.getIdentifier());
302+
Set<String> runtimeDependencies = getParameters().getRuntimeClasspathDependencies().get().get(moduleInfo.getIdentifier());
303+
304+
if (compileDependencies == null || runtimeDependencies == null) {
305+
throw new RuntimeException("[requires directives from metadata] " +
306+
"Cannot find dependencies for '" + moduleInfo.getModuleName() + "'. " +
307+
"Are '" + moduleInfo.getIdentifier() + "' the correct component coordinates?");
308+
}
309+
310+
Set<String> allDependencies = new TreeSet<>();
311+
allDependencies.addAll(compileDependencies);
312+
allDependencies.addAll(runtimeDependencies );
313+
for (String ga: allDependencies) {
314+
String moduleName = gaToModuleName(ga);
315+
if (compileDependencies.contains(ga) && runtimeDependencies.contains(ga)) {
316+
moduleVisitor.visitRequire(moduleName, Opcodes.ACC_TRANSITIVE, null);
317+
} else if (runtimeDependencies.contains(ga)) {
318+
moduleVisitor.visitRequire(moduleName, 0, null);
319+
} else if (compileDependencies.contains(ga)) {
320+
moduleVisitor.visitRequire(moduleName, Opcodes.ACC_STATIC_PHASE, null);
321+
}
322+
}
323+
}
324+
293325
for (String requireName : moduleInfo.requires) {
294326
moduleVisitor.visitRequire(requireName, 0, null);
295327
}
@@ -369,4 +401,15 @@ private byte[] readAllBytes(InputStream inputStream) throws IOException {
369401
return outputStream.toByteArray();
370402
}
371403
}
404+
405+
private String gaToModuleName(String ga) {
406+
ModuleSpec moduleSpec = getParameters().getModuleSpecs().get().get(ga);
407+
if (moduleSpec == null) {
408+
throw new RuntimeException("[requires directives from metadata] " +
409+
"The module name of the following component is not known: " + ga +
410+
"\n - If it is already a module, make the module name known using 'knownModule(\"" + ga + "\", \"<module name>\")'" +
411+
"\n - If it is not a module, patch it using 'module()' or 'automaticModule()'");
412+
}
413+
return moduleSpec.getModuleName();
414+
}
372415
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
/*
2+
* Copyright 2022 the GradleX team.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.gradlex.javamodule.moduleinfo;
18+
19+
public class KnownModule extends ModuleSpec {
20+
21+
KnownModule(String identifier, String moduleName) {
22+
super(identifier, moduleName);
23+
}
24+
}

src/main/java/org/gradlex/javamodule/moduleinfo/ModuleInfo.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ public class ModuleInfo extends ModuleSpec {
3434
final Set<String> ignoreServiceProviders = new LinkedHashSet<>();
3535

3636
private boolean exportAllPackages;
37+
private boolean requiresDirectivesFromMetadata;
3738

3839
ModuleInfo(String identifier, String moduleName, String moduleVersion) {
3940
super(identifier, moduleName);
@@ -95,4 +96,12 @@ public void exportAllPackages(boolean exportAllPackages) {
9596
public boolean getExportAllPackages() {
9697
return exportAllPackages;
9798
}
99+
100+
public void requiresDirectivesFromMetadata(boolean requiresDirectivesFromMetadata) {
101+
this.requiresDirectivesFromMetadata = requiresDirectivesFromMetadata;
102+
}
103+
104+
public boolean getRequiresDirectivesFromMetadata() {
105+
return requiresDirectivesFromMetadata;
106+
}
98107
}
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
package org.gradlex.javamodule.moduleinfo.test
2+
3+
import org.gradle.testkit.runner.TaskOutcome
4+
import org.gradlex.javamodule.moduleinfo.test.fixture.GradleBuild
5+
import org.gradlex.javamodule.moduleinfo.test.fixture.LegacyLibraries
6+
import spock.lang.Specification
7+
8+
class RequiresFromMetadataFunctionalTest extends Specification {
9+
10+
@Delegate
11+
GradleBuild build = new GradleBuild()
12+
13+
LegacyLibraries libs = new LegacyLibraries(false)
14+
15+
def setup() {
16+
settingsFile << 'rootProject.name = "test-project"'
17+
buildFile << '''
18+
plugins {
19+
id("application")
20+
id("org.gradlex.extra-java-module-info")
21+
}
22+
application {
23+
mainModule.set("org.gradle.sample.app")
24+
mainClass.set("org.gradle.sample.app.Main")
25+
}
26+
'''
27+
}
28+
29+
def "can automatically add requires directives based on component metadata"() {
30+
given:
31+
file("src/main/java/org/gradle/sample/app/Main.java") << """
32+
package org.gradle.sample.app;
33+
34+
import org.apache.http.NameValuePair;
35+
import org.apache.http.client.methods.HttpPost;
36+
import org.apache.http.client.entity.UrlEncodedFormEntity;
37+
import org.apache.http.message.BasicNameValuePair;
38+
39+
import java.util.List;
40+
import java.util.ArrayList;
41+
42+
public class Main {
43+
public static void main(String[] args) throws Exception {
44+
HttpPost httpPost = new HttpPost("http://targethost/login");
45+
List<NameValuePair> nvps = new ArrayList<NameValuePair>();
46+
nvps.add(new BasicNameValuePair("username", "vip"));
47+
nvps.add(new BasicNameValuePair("password", "secret"));
48+
httpPost.setEntity(new UrlEncodedFormEntity(nvps));
49+
}
50+
}
51+
"""
52+
file("src/main/java/module-info.java") << """
53+
module org.gradle.sample.app {
54+
exports org.gradle.sample.app;
55+
56+
requires org.apache.httpcomponents.httpclient;
57+
}
58+
"""
59+
buildFile << """
60+
dependencies {
61+
implementation("org.apache.httpcomponents:httpclient:4.5.14")
62+
}
63+
64+
extraJavaModuleInfo {
65+
module("${libs.commonsHttpClient}", "org.apache.httpcomponents.httpclient") {
66+
exportAllPackages(true)
67+
requiresDirectivesFromMetadata(true)
68+
}
69+
module("${libs.commonsLogging}", "org.apache.commons.logging") {
70+
exportAllPackages(true)
71+
requiresDirectivesFromMetadata(true)
72+
}
73+
knownModule("commons-codec:commons-codec", "org.apache.commons.codec")
74+
knownModule("org.apache.httpcomponents:httpcore", "org.apache.httpcomponents.httpcore")
75+
}
76+
"""
77+
78+
expect:
79+
run().task(':run').outcome == TaskOutcome.SUCCESS
80+
}
81+
82+
def "gives error if dependencies cannot be discovered"() {
83+
given:
84+
file("src/main/java/org/gradle/sample/app/Main.java") << """
85+
package org.gradle.sample.app;
86+
public class Main {
87+
public static void main(String[] args) { }
88+
}
89+
"""
90+
file("src/main/java/module-info.java") << """
91+
module org.gradle.sample.app {
92+
exports org.gradle.sample.app;
93+
94+
requires org.apache.httpcomponents.httpclient;
95+
}
96+
"""
97+
buildFile << """
98+
dependencies {
99+
implementation("org.apache.httpcomponents:httpclient:4.5.14")
100+
}
101+
102+
extraJavaModuleInfo {
103+
module("${new LegacyLibraries(true).commonsHttpClient}", "org.apache.httpcomponents.httpclient") {
104+
exportAllPackages(true)
105+
requiresDirectivesFromMetadata(true)
106+
}
107+
module("${libs.commonsLogging}", "org.apache.commons.logging") {
108+
exportAllPackages(true)
109+
requiresDirectivesFromMetadata(true)
110+
}
111+
knownModule("commons-codec:commons-codec", "org.apache.commons.codec")
112+
knownModule("org.apache.httpcomponents:httpcore", "org.apache.httpcomponents.httpcore")
113+
}
114+
"""
115+
116+
expect:
117+
fail().output.contains("[requires directives from metadata] " +
118+
"Cannot find dependencies for 'org.apache.httpcomponents.httpclient'. " +
119+
"Are 'httpclient-4.5.14.jar' the correct component coordinates?")
120+
}
121+
122+
def "gives error if the module name for certain ga coordinates is not known"() {
123+
given:
124+
file("src/main/java/org/gradle/sample/app/Main.java") << """
125+
package org.gradle.sample.app;
126+
public class Main {
127+
public static void main(String[] args) { }
128+
}
129+
"""
130+
file("src/main/java/module-info.java") << """
131+
module org.gradle.sample.app {
132+
exports org.gradle.sample.app;
133+
134+
requires org.apache.httpcomponents.httpclient;
135+
}
136+
"""
137+
buildFile << """
138+
dependencies {
139+
implementation("org.apache.httpcomponents:httpclient:4.5.14")
140+
}
141+
142+
extraJavaModuleInfo {
143+
module("${libs.commonsHttpClient}", "org.apache.httpcomponents.httpclient") {
144+
exportAllPackages(true)
145+
requiresDirectivesFromMetadata(true)
146+
}
147+
module("${libs.commonsLogging}", "org.apache.commons.logging") {
148+
exportAllPackages(true)
149+
requiresDirectivesFromMetadata(true)
150+
}
151+
knownModule("commons-codec:commons-codec", "org.apache.commons.codec")
152+
}
153+
"""
154+
155+
expect:
156+
fail().output.contains("[requires directives from metadata] " +
157+
"The module name of the following component is not known: org.apache.httpcomponents:httpcore")
158+
}
159+
}

src/test/groovy/org/gradlex/javamodule/moduleinfo/test/fixture/LegacyLibraries.groovy

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ class LegacyLibraries {
1212
def commonsBeanutils = jarNameOnly ? "commons-beanutils-1.9.4.jar" : "commons-beanutils:commons-beanutils"
1313
def commonsCli = jarNameOnly ? "commons-cli-1.4.jar" : "commons-cli:commons-cli"
1414
def commonsCollections = jarNameOnly ? "commons-collections-3.2.2.jar" : "commons-collections:commons-collections"
15+
def commonsHttpClient = jarNameOnly ? "httpclient-4.5.14.jar" : "org.apache.httpcomponents:httpclient"
1516
def commonsLogging = jarNameOnly ? "commons-logging-1.2.jar" : "commons-logging:commons-logging"
1617
def groovyAll = jarNameOnly ? "groovy-all-2.4.15.jar" : "org.codehaus.groovy:groovy-all"
1718
def javaxInject = jarNameOnly ? "javax.inject-1.jar" : "javax.inject:javax.inject"

0 commit comments

Comments
 (0)