Skip to content

Commit 3292934

Browse files
committed
Support 'group:name' notation and automatic version detection
The plugin is now able to identify a Jar by 'group:name' coordinates. This information is used in the _merge Jar_ functionality to automatically configure the 'javaModulesMergeJars' configuration with all required dependencies. The plugin can now also determine the version of the library to use that version in the 'module-info.class'. This all makes the DSL in 'extraJavaModuleInfo {}' significantly easier to use.
1 parent e201c89 commit 3292934

File tree

3 files changed

+189
-32
lines changed

3 files changed

+189
-32
lines changed

README.md

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ plugins {
3636
// add module information for all direct and transitive dependencies that are not modules
3737
extraJavaModuleInfo {
3838
// failOnMissingModuleInfo.set(false)
39-
module("commons-beanutils-1.9.4.jar", "org.apache.commons.beanutils", "1.9.4") {
39+
module("commons-beanutils:commons-beanutils", "org.apache.commons.beanutils") {
4040
exports("org.apache.commons.beanutils")
4141
4242
requires("org.apache.commons.logging")
@@ -46,11 +46,11 @@ extraJavaModuleInfo {
4646
// requiresTransitive(...)
4747
// requiresStatic(...)
4848
}
49-
module("commons-cli-1.4.jar", "org.apache.commons.cli", "3.2.2") {
49+
module("commons-cli:commons-cli", "org.apache.commons.cli") {
5050
exports("org.apache.commons.cli")
5151
}
52-
module("commons-collections-3.2.2.jar", "org.apache.commons.collections", "3.2.2")
53-
automaticModule("commons-logging-1.2.jar", "org.apache.commons.logging")
52+
module("commons-collections:commons-collections", "org.apache.commons.collections")
53+
automaticModule("commons-logging:commons-logging", "org.apache.commons.logging")
5454
}
5555
```
5656

@@ -75,7 +75,7 @@ Sample uses Gradle's Kotlin DSL (`build.gradle.kts` file). The Groovy DSL syntax
7575

7676
## How do I deactivate the plugin functionality for a certain classpath?
7777

78-
This is can be useful for the test classpath if it should be used for unit testing on the classpath (rather than the module path).
78+
This can be useful for the test classpath if it should be used for unit testing on the classpath (rather than the module path).
7979
If you use the [shadow plugin](https://plugins.gradle.org/plugin/com.github.johnrengelman.shadow) and [encounter this issue](https://github.com/jjohannes/extra-java-module-info/issues/7),
8080
you can deactivate it for the runtime classpath as the module information is irrelevant for a fat Jar in any case.
8181

@@ -115,3 +115,24 @@ extraJavaModuleInfo {
115115
}
116116
}
117117
```
118+
119+
## What do I do in a 'split package' situation?
120+
121+
The Java Module System does not allow the same package to be used in more than one _module_.
122+
This is an issue with legacy libraries, where it was common practice to use the same package in multiple Jars.
123+
This plugin offers the option to merge multiple Jars into one in such situations:
124+
125+
```
126+
extraJavaModuleInfo {
127+
module("org.apache.zookeeper:zookeeper", "org.apache.zookeeper") {
128+
mergeJar("org.apache.zookeeper:zookeeper-jute")
129+
130+
// ...
131+
}
132+
automaticModule("org.slf4j:slf4j-api", "org.slf4j") {
133+
mergeJar("org.slf4j:slf4j-ext")
134+
}
135+
}
136+
```
137+
138+
Note: The merged Jar will include the *first* appearance of duplicated files (like the `MANIFEST.MF`).

src/main/java/de/jjohannes/gradle/javamodules/ExtraModuleInfoPlugin.java

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,29 @@
11
package de.jjohannes.gradle.javamodules;
22

3+
import org.gradle.api.NonNullApi;
34
import org.gradle.api.Plugin;
45
import org.gradle.api.Project;
6+
import org.gradle.api.Transformer;
57
import org.gradle.api.artifacts.Configuration;
8+
import org.gradle.api.artifacts.component.ComponentIdentifier;
9+
import org.gradle.api.artifacts.component.ModuleComponentIdentifier;
10+
import org.gradle.api.artifacts.result.ResolvedArtifactResult;
611
import org.gradle.api.attributes.Attribute;
712
import org.gradle.api.attributes.Category;
813
import org.gradle.api.attributes.Usage;
14+
import org.gradle.api.file.Directory;
15+
import org.gradle.api.file.ProjectLayout;
16+
import org.gradle.api.file.RegularFile;
917
import org.gradle.api.plugins.JavaPlugin;
18+
import org.gradle.api.provider.Provider;
1019
import org.gradle.api.tasks.SourceSetContainer;
1120
import org.gradle.util.GradleVersion;
1221

22+
import java.util.Collection;
23+
import java.util.List;
24+
import java.util.Set;
25+
import java.util.stream.Collectors;
26+
1327
/**
1428
* Entry point of the plugin.
1529
*/
@@ -38,6 +52,18 @@ private void configureTransform(Project project, ExtraModuleInfoPluginExtension
3852
c.setCanBeResolved(true);
3953
c.getAttributes().attribute(Usage.USAGE_ATTRIBUTE, project.getObjects().named(Usage.class, Usage.JAVA_RUNTIME));
4054
c.getAttributes().attribute(Category.CATEGORY_ATTRIBUTE, project.getObjects().named(Category.class, Category.LIBRARY));
55+
56+
// Automatically add dependencies for Jars where we know the coordinates
57+
// Note: User still needs to provide versions in through constraints/platforms or consistent resolution.
58+
c.withDependencies(d -> extension.getModuleSpecs().get().values().stream().flatMap(m ->
59+
m.getMergedJars().stream()).filter(s -> s.contains(":")).forEach(s ->
60+
d.add(project.getDependencies().create(s))));
61+
62+
// Automatically get versions from the runtime classpath
63+
if (GradleVersion.current().compareTo(GradleVersion.version("6.8")) >= 0) {
64+
//noinspection UnstableApiUsage
65+
c.shouldResolveConsistentlyWith(project.getConfigurations().getByName(JavaPlugin.RUNTIME_CLASSPATH_CONFIGURATION_NAME));
66+
}
4167
});
4268

4369
Attribute<String> artifactType = Attribute.of("artifactType", String.class);
@@ -62,10 +88,42 @@ private void configureTransform(Project project, ExtraModuleInfoPluginExtension
6288
t.parameters(p -> {
6389
p.getModuleSpecs().set(extension.getModuleSpecs());
6490
p.getFailOnMissingModuleInfo().set(extension.getFailOnMissingModuleInfo());
65-
p.getMergeJars().from(javaModulesMergeJars);
91+
92+
// See: https://github.com/adammurdoch/dependency-graph-as-task-inputs/blob/main/plugins/src/main/java/TestPlugin.java
93+
Provider<Set<ResolvedArtifactResult>> artifacts = project.provider(() -> javaModulesMergeJars.getIncoming().getArtifacts().getArtifacts());
94+
p.getMergeJarIds().set(artifacts.map(new IdExtractor()));
95+
p.getMergeJars().set(artifacts.map(new FileExtractor(project.getLayout())));
6696
});
6797
t.getFrom().attribute(artifactType, "jar").attribute(javaModule, false);
6898
t.getTo().attribute(artifactType, "jar").attribute(javaModule, true);
6999
});
70100
}
101+
102+
private static class IdExtractor implements Transformer<List<String>, Collection<ResolvedArtifactResult>> {
103+
@Override
104+
public List<String> transform(Collection<ResolvedArtifactResult> artifacts) {
105+
return artifacts.stream().map(a -> {
106+
ComponentIdentifier componentIdentifier = a.getId().getComponentIdentifier();
107+
if (componentIdentifier instanceof ModuleComponentIdentifier) {
108+
return ((ModuleComponentIdentifier) componentIdentifier).getModuleIdentifier().toString();
109+
} else {
110+
return componentIdentifier.getDisplayName();
111+
}
112+
}).collect(Collectors.toList());
113+
}
114+
}
115+
116+
private static class FileExtractor implements Transformer<List<RegularFile>, Collection<ResolvedArtifactResult>> {
117+
private final ProjectLayout projectLayout;
118+
119+
public FileExtractor(ProjectLayout projectLayout) {
120+
this.projectLayout = projectLayout;
121+
}
122+
123+
@Override
124+
public List<RegularFile> transform(Collection<ResolvedArtifactResult> artifacts) {
125+
Directory projectDirectory = projectLayout.getProjectDirectory();
126+
return artifacts.stream().map(a -> projectDirectory.file(a.getFile().getAbsolutePath())).collect(Collectors.toList());
127+
}
128+
}
71129
}

src/main/java/de/jjohannes/gradle/javamodules/ExtraModuleInfoTransform.java

Lines changed: 104 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
package de.jjohannes.gradle.javamodules;
22

3+
import org.gradle.api.NonNullApi;
34
import org.gradle.api.artifacts.CacheableRule;
45
import org.gradle.api.artifacts.transform.InputArtifact;
56
import org.gradle.api.artifacts.transform.TransformAction;
67
import org.gradle.api.artifacts.transform.TransformOutputs;
78
import org.gradle.api.artifacts.transform.TransformParameters;
8-
import org.gradle.api.file.ConfigurableFileCollection;
99
import org.gradle.api.file.FileSystemLocation;
10+
import org.gradle.api.file.RegularFile;
11+
import org.gradle.api.provider.ListProperty;
1012
import org.gradle.api.provider.MapProperty;
1113
import org.gradle.api.provider.Property;
1214
import org.gradle.api.provider.Provider;
@@ -16,17 +18,25 @@
1618
import org.objectweb.asm.ModuleVisitor;
1719
import org.objectweb.asm.Opcodes;
1820

19-
import javax.annotation.Nonnull;
20-
import java.io.*;
21+
import javax.annotation.Nullable;
22+
import java.io.BufferedReader;
23+
import java.io.ByteArrayInputStream;
24+
import java.io.File;
25+
import java.io.IOException;
26+
import java.io.InputStreamReader;
27+
import java.io.OutputStream;
2128
import java.nio.charset.StandardCharsets;
2229
import java.nio.file.Files;
23-
import java.util.LinkedHashMap;
30+
import java.nio.file.Path;
2431
import java.util.Collection;
32+
import java.util.LinkedHashMap;
33+
import java.util.List;
2534
import java.util.Map;
26-
import java.util.Optional;
27-
import java.util.jar.*;
35+
import java.util.jar.JarEntry;
36+
import java.util.jar.JarInputStream;
37+
import java.util.jar.JarOutputStream;
38+
import java.util.jar.Manifest;
2839
import java.util.regex.Pattern;
29-
import java.util.stream.Stream;
3040
import java.util.zip.ZipEntry;
3141
import java.util.zip.ZipException;
3242

@@ -36,6 +46,7 @@
3646
* was defined for it. This way we make sure that all Jars are turned into modules.
3747
*/
3848
@CacheableRule
49+
@NonNullApi
3950
abstract public class ExtraModuleInfoTransform implements TransformAction<ExtraModuleInfoTransform.Parameter> {
4051

4152
private static final Pattern MODULE_INFO_CLASS_MRJAR_PATH = Pattern.compile("META-INF/versions/\\d+/module-info.class");
@@ -47,39 +58,90 @@ public interface Parameter extends TransformParameters {
4758
MapProperty<String, ModuleSpec> getModuleSpecs();
4859
@Input
4960
Property<Boolean> getFailOnMissingModuleInfo();
61+
@Input
62+
ListProperty<String> getMergeJarIds();
5063
@InputFiles
51-
ConfigurableFileCollection getMergeJars();
64+
ListProperty<RegularFile> getMergeJars();
5265
}
5366

5467
@InputArtifact
5568
protected abstract Provider<FileSystemLocation> getInputArtifact();
5669

5770
@Override
58-
public void transform(@Nonnull TransformOutputs outputs) {
71+
public void transform(TransformOutputs outputs) {
5972
Map<String, ModuleSpec> moduleSpecs = getParameters().getModuleSpecs().get();
6073
File originalJar = getInputArtifact().get().getAsFile();
61-
String originalJarName = originalJar.getName();
6274

63-
if (moduleSpecs.containsKey(originalJarName) && moduleSpecs.get(originalJarName) instanceof ModuleInfo) {
64-
addModuleDescriptor(originalJar, getModuleJar(outputs, originalJar), (ModuleInfo) moduleSpecs.get(originalJarName));
65-
} else if (moduleSpecs.containsKey(originalJarName) && moduleSpecs.get(originalJarName) instanceof AutomaticModuleName) {
66-
addAutomaticModuleName(originalJar, getModuleJar(outputs, originalJar), (AutomaticModuleName) moduleSpecs.get(originalJarName));
75+
ModuleSpec moduleSpec = findModuleSpec(originalJar);
76+
77+
if (moduleSpec instanceof ModuleInfo) {
78+
addModuleDescriptor(originalJar, getModuleJar(outputs, originalJar), (ModuleInfo) moduleSpec);
79+
} else if (moduleSpec instanceof AutomaticModuleName) {
80+
addAutomaticModuleName(originalJar, getModuleJar(outputs, originalJar), (AutomaticModuleName) moduleSpec);
6781
} else if (isModule(originalJar)) {
6882
outputs.file(originalJar);
6983
} else if (isAutoModule(originalJar)) {
7084
outputs.file(originalJar);
7185
} else if (!willBeMerged(originalJar, moduleSpecs.values())) { // No output if this Jar will be merged
7286
if (getParameters().getFailOnMissingModuleInfo().get()) {
73-
throw new RuntimeException("Not a module and no mapping defined: " + originalJarName);
87+
throw new RuntimeException("Not a module and no mapping defined: " + originalJar.getName());
7488
} else {
7589
outputs.file(originalJar);
7690
}
7791
}
7892
}
7993

94+
@Nullable
95+
private ModuleSpec findModuleSpec(File originalJar) {
96+
Map<String, ModuleSpec> moduleSpecs = getParameters().getModuleSpecs().get();
97+
98+
String gaCoordinates = gaCoordinatesFromFilePath(originalJar.toPath());
99+
String originalJarName = originalJar.getName();
100+
101+
if (moduleSpecs.containsKey(gaCoordinates)) {
102+
return moduleSpecs.get(gaCoordinates);
103+
}
104+
if (moduleSpecs.containsKey(originalJarName)) {
105+
return moduleSpecs.get(originalJarName);
106+
}
107+
108+
return null;
109+
}
110+
80111
private boolean willBeMerged(File originalJar, Collection<ModuleSpec> modules) {
81-
return modules.stream().anyMatch(module ->
82-
module.getMergedJars().stream().anyMatch(toMerge -> toMerge.equals(originalJar.getName())));
112+
return modules.stream().anyMatch(module -> module.getMergedJars().stream().anyMatch(toMerge ->
113+
toMerge.equals(gaCoordinatesFromFilePath(originalJar.toPath())) || toMerge.equals(originalJar.getName())));
114+
}
115+
116+
/**
117+
* Attempts to parse 'group' and 'name' coordinates from a path like:
118+
* .gradle/caches/modules-2/files-2.1/org.slf4j/slf4j-api/1.7.36/6c62681a2f655b49963a5983b8b0950a6120ae14/slf4j-api-1.7.36.jar
119+
*/
120+
@Nullable
121+
private String gaCoordinatesFromFilePath(Path path) {
122+
String version = versionFromFilePath(path);
123+
if (version == null) {
124+
return null;
125+
}
126+
127+
String group = path.getName(path.getNameCount() - 5).toString();
128+
String name = path.getName(path.getNameCount() - 4).toString();
129+
130+
return group + ":" + name;
131+
}
132+
133+
@Nullable
134+
private String versionFromFilePath(Path path) {
135+
if (path.getNameCount() < 5) {
136+
return null;
137+
}
138+
139+
String version = path.getName(path.getNameCount() - 3).toString();
140+
if (!path.getFileName().toString().contains(version)) {
141+
return null;
142+
}
143+
144+
return version;
83145
}
84146

85147
private boolean isModule(File jar) {
@@ -156,15 +218,15 @@ private void addModuleDescriptor(File originalJar, File moduleJar, ModuleInfo mo
156218
Map<String, String[]> providers = copyAndExtractProviders(inputStream, outputStream);
157219
mergeJars(moduleInfo, outputStream);
158220
outputStream.putNextEntry(new JarEntry("module-info.class"));
159-
outputStream.write(addModuleInfo(moduleInfo, providers));
221+
outputStream.write(addModuleInfo(moduleInfo, providers, versionFromFilePath(originalJar.toPath())));
160222
outputStream.closeEntry();
161223
}
162224
} catch (IOException e) {
163225
throw new RuntimeException(e);
164226
}
165227
}
166228

167-
private static JarOutputStream newJarOutputStream(OutputStream out, Manifest manifest) throws IOException {
229+
private static JarOutputStream newJarOutputStream(OutputStream out, @Nullable Manifest manifest) throws IOException {
168230
return manifest == null ? new JarOutputStream(out) : new JarOutputStream(out, manifest);
169231
}
170232

@@ -199,10 +261,11 @@ private static String[] extractImplementations(byte[] content) {
199261
.toArray(String[]::new);
200262
}
201263

202-
private static byte[] addModuleInfo(ModuleInfo moduleInfo, Map<String, String[]> providers) {
264+
private byte[] addModuleInfo(ModuleInfo moduleInfo, Map<String, String[]> providers, @Nullable String version) {
203265
ClassWriter classWriter = new ClassWriter(0);
204266
classWriter.visit(Opcodes.V9, Opcodes.ACC_MODULE, "module-info", null, null, null);
205-
ModuleVisitor moduleVisitor = classWriter.visitModule(moduleInfo.getModuleName(), Opcodes.ACC_OPEN, moduleInfo.moduleVersion);
267+
String moduleVersion = moduleInfo.getModuleVersion() == null ? version : moduleInfo.getModuleVersion();
268+
ModuleVisitor moduleVisitor = classWriter.visitModule(moduleInfo.getModuleName(), Opcodes.ACC_OPEN, moduleVersion);
206269
for (String packageName : moduleInfo.exports) {
207270
moduleVisitor.visitExport(packageName.replace('.', '/'), 0);
208271
}
@@ -229,14 +292,29 @@ private static byte[] addModuleInfo(ModuleInfo moduleInfo, Map<String, String[]>
229292
}
230293

231294
private void mergeJars(ModuleSpec moduleSpec, JarOutputStream outputStream) throws IOException {
232-
for (String mergeJar : moduleSpec.getMergedJars()) {
233-
Optional<File> mergeJarFile = getParameters().getMergeJars().getFiles().stream().filter(f -> f.getName().equals(mergeJar)).findFirst();
234-
if (mergeJarFile.isPresent()) {
235-
try (JarInputStream toMergeInputStream = new JarInputStream(Files.newInputStream(mergeJarFile.get().toPath()))) {
295+
RegularFile mergeJarFile = null;
296+
for (String identifier : moduleSpec.getMergedJars()) {
297+
List<String> ids = getParameters().getMergeJarIds().get();
298+
List<RegularFile> jarFiles = getParameters().getMergeJars().get();
299+
for (int i = 0; i < ids.size(); i++) {
300+
// referenced by 'group:version'
301+
if (ids.get(i).equals(identifier)) {
302+
mergeJarFile = jarFiles.get(i);
303+
break;
304+
}
305+
// referenced by 'jar file name'
306+
if (jarFiles.get(i).getAsFile().getName().equals(identifier)) {
307+
mergeJarFile = jarFiles.get(i);
308+
break;
309+
}
310+
}
311+
312+
if (mergeJarFile != null) {
313+
try (JarInputStream toMergeInputStream = new JarInputStream(Files.newInputStream(mergeJarFile.getAsFile().toPath()))) {
236314
copyEntries(toMergeInputStream, outputStream);
237315
}
238316
} else {
239-
throw new RuntimeException("Jar not found: " + mergeJar);
317+
throw new RuntimeException("Jar not found: " + identifier);
240318
}
241319
}
242320
}

0 commit comments

Comments
 (0)