Skip to content

Commit 60c1a7d

Browse files
authored
Add 'deriveAutomaticModuleNamesFromFileNames' option (#84)
Resolves #74
1 parent 2f90e74 commit 60c1a7d

File tree

10 files changed

+337
-18
lines changed

10 files changed

+337
-18
lines changed

README.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,21 @@ extraJavaModuleInfo {
236236

237237
This opt-in behavior is designed to prevent over-patching real modules, especially during version upgrades. For example, when a newer version of a library already contains the proper `module-info.class`, the extra module info overrides should be removed.
238238

239+
## Can't things just work™ without all that configuration?
240+
241+
If you use legacy libraries and want to use the Java Module System with all its features, you should patch all Jars to include a `module-info`.
242+
However, if you get started and just want things to be put on the Module Path, you can set the following option:
243+
244+
```
245+
extraJavaModuleInfo {
246+
deriveAutomaticModuleNamesFromFileNames.set(true)
247+
}
248+
```
249+
250+
Now, also Jars that do not have a `module-info.class` and no `Automatic-Module-Name` entry will automatically be processed to get an `Automatic-Module-Name` based on the Jar file name.
251+
This feature is helpful if you start to migrate an existing project to the Module Path.
252+
The pivotal feature of this plugin though, is to add a complete `module-info.class` to all Jars using the `module(...)` patch option for each legacy Jar individually.
253+
239254
# Disclaimer
240255

241256
Gradle and the Gradle logo are trademarks of Gradle, Inc.

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ public void apply(Project project) {
6464
ExtraJavaModuleInfoPluginExtension extension = project.getExtensions().create("extraJavaModuleInfo", ExtraJavaModuleInfoPluginExtension.class);
6565
extension.getFailOnMissingModuleInfo().convention(true);
6666
extension.getFailOnAutomaticModules().convention(false);
67+
extension.getDeriveAutomaticModuleNamesFromFileNames().convention(false);
6768

6869
// setup the transform and the tasks for all projects in the build
6970
project.getPlugins().withType(JavaPlugin.class).configureEach(javaPlugin -> {
@@ -173,6 +174,7 @@ private void registerTransform(String fileExtension, Project project, ExtraJavaM
173174
p.getModuleSpecs().set(extension.getModuleSpecs());
174175
p.getFailOnMissingModuleInfo().set(extension.getFailOnMissingModuleInfo());
175176
p.getFailOnAutomaticModules().set(extension.getFailOnAutomaticModules());
177+
p.getDeriveAutomaticModuleNamesFromFileNames().set(extension.getDeriveAutomaticModuleNamesFromFileNames());
176178

177179
// See: https://github.com/adammurdoch/dependency-graph-as-task-inputs/blob/main/plugins/src/main/java/TestPlugin.java
178180
Provider<Set<ResolvedArtifactResult>> artifacts = project.provider(() ->

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ public abstract class ExtraJavaModuleInfoPluginExtension {
4040
public abstract MapProperty<String, ModuleSpec> getModuleSpecs();
4141
public abstract Property<Boolean> getFailOnMissingModuleInfo();
4242
public abstract Property<Boolean> getFailOnAutomaticModules();
43+
public abstract Property<Boolean> getDeriveAutomaticModuleNamesFromFileNames();
4344

4445
/**
4546
* Add full module information for a given Jar file.

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

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -48,12 +48,12 @@
4848
import java.util.ArrayList;
4949
import java.util.Collection;
5050
import java.util.Collections;
51-
import java.util.TreeSet;
5251
import java.util.LinkedHashMap;
5352
import java.util.List;
5453
import java.util.Map;
5554
import java.util.Optional;
5655
import java.util.Set;
56+
import java.util.TreeSet;
5757
import java.util.jar.JarEntry;
5858
import java.util.jar.JarInputStream;
5959
import java.util.jar.JarOutputStream;
@@ -64,6 +64,7 @@
6464
import java.util.zip.ZipEntry;
6565
import java.util.zip.ZipException;
6666

67+
import static org.gradlex.javamodule.moduleinfo.ModuleNameUtil.automaticModulNameFromFileName;
6768
import static org.gradlex.javamodule.moduleinfo.FilePathToModuleCoordinates.gaCoordinatesFromFilePathMatch;
6869
import static org.gradlex.javamodule.moduleinfo.FilePathToModuleCoordinates.versionFromFilePath;
6970

@@ -84,18 +85,28 @@ public abstract class ExtraJavaModuleInfoTransform implements TransformAction<Ex
8485
public interface Parameter extends TransformParameters {
8586
@Input
8687
MapProperty<String, ModuleSpec> getModuleSpecs();
88+
8789
@Input
8890
Property<Boolean> getFailOnMissingModuleInfo();
91+
8992
@Input
9093
Property<Boolean> getFailOnAutomaticModules();
94+
95+
@Input
96+
Property<Boolean> getDeriveAutomaticModuleNamesFromFileNames();
97+
9198
@Input
9299
ListProperty<String> getMergeJarIds();
100+
93101
@InputFiles
94102
ListProperty<RegularFile> getMergeJars();
103+
95104
@Input
96105
MapProperty<String, Set<String>> getCompileClasspathDependencies();
106+
97107
@Input
98108
MapProperty<String, Set<String>> getRuntimeClasspathDependencies();
109+
99110
@Input
100111
MapProperty<String, Set<String>> getAnnotationProcessorClasspathDependencies();
101112
}
@@ -135,12 +146,13 @@ public void transform(TransformOutputs outputs) {
135146
throw new RuntimeException("Found an automatic module: " + originalJar.getName());
136147
}
137148
outputs.file(originalJar);
149+
} else if (parameters.getDeriveAutomaticModuleNamesFromFileNames().get()) {
150+
String automaticName = automaticModulNameFromFileName(originalJar);
151+
addAutomaticModuleName(originalJar, getModuleJar(outputs, originalJar), new AutomaticModuleName(originalJar.getName(), automaticName));
152+
} else if (parameters.getFailOnMissingModuleInfo().get()) {
153+
throw new RuntimeException("Not a module and no mapping defined: " + originalJar.getName());
138154
} else {
139-
if (parameters.getFailOnMissingModuleInfo().get()) {
140-
throw new RuntimeException("Not a module and no mapping defined: " + originalJar.getName());
141-
} else {
142-
outputs.file(originalJar);
143-
}
155+
outputs.file(originalJar);
144156
}
145157
}
146158

@@ -175,7 +187,7 @@ private boolean isModule(File jar) {
175187
// - https://github.com/jjohannes/extra-java-module-info/issues/78
176188
return true;
177189
}
178-
try (JarInputStream inputStream = new JarInputStream(Files.newInputStream(jar.toPath()))) {
190+
try (JarInputStream inputStream = new JarInputStream(Files.newInputStream(jar.toPath()))) {
179191
boolean isMultiReleaseJar = containsMultiReleaseJarEntry(inputStream);
180192
ZipEntry next = inputStream.getNextEntry();
181193
while (next != null) {
@@ -351,7 +363,7 @@ private byte[] addModuleInfo(ModuleInfo moduleInfo, Map<String, List<String>> pr
351363
allDependencies.addAll(compileDependencies);
352364
allDependencies.addAll(runtimeDependencies);
353365
allDependencies.addAll(annotationProcessorDependencies);
354-
for (String ga: allDependencies) {
366+
for (String ga : allDependencies) {
355367
String moduleName = gaToModuleName(ga);
356368
if (compileDependencies.contains(ga) && !runtimeDependencies.contains(ga)) {
357369
moduleVisitor.visitRequire(moduleName, Opcodes.ACC_STATIC_PHASE, null);
@@ -381,7 +393,7 @@ private byte[] addModuleInfo(ModuleInfo moduleInfo, Map<String, List<String>> pr
381393
List<String> implementations = entry.getValue();
382394
if (!moduleInfo.ignoreServiceProviders.contains(name)) {
383395
moduleVisitor.visitProvide(name.replace('.', '/'),
384-
implementations.stream().map(impl -> impl.replace('.','/')).toArray(String[]::new));
396+
implementations.stream().map(impl -> impl.replace('.', '/')).toArray(String[]::new));
385397
}
386398
}
387399
moduleVisitor.visitEnd();
@@ -461,4 +473,5 @@ private String gaToModuleName(String ga) {
461473
private static boolean isModuleInfoClass(String jarEntryName) {
462474
return "module-info.class".equals(jarEntryName) || MODULE_INFO_CLASS_MRJAR_PATH.matcher(jarEntryName).matches();
463475
}
476+
464477
}

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

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,17 +19,10 @@
1919
class IdValidator {
2020
static private final String COORDINATES_PATTERN = "^[a-zA-Z0-9._-]+:[a-zA-Z0-9._-]+$";
2121
static private final String FILE_NAME_PATTERN = "^[a-zA-Z0-9._-]+\\.(jar|zip)$";
22-
static private final String MODULE_NAME_PATTERN = "^[a-zA-Z][a-zA-Z0-9_]*(\\.[a-zA-Z0-9_]+)*$";
2322

2423
static void validateIdentifier(String identifier) {
2524
if (!identifier.matches(COORDINATES_PATTERN) && !identifier.matches(FILE_NAME_PATTERN)) {
2625
throw new RuntimeException("'" + identifier + "' are not valid coordinates (group:name) / is not a valid file name (name-1.0.jar)");
2726
}
2827
}
29-
30-
static void validateModuleName(String moduleName) {
31-
if (!moduleName.matches(MODULE_NAME_PATTERN)) {
32-
throw new RuntimeException("'" + moduleName + "' is not a valid Java Module name");
33-
}
34-
}
3528
}
Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
/*
2+
* Copyright 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+
import java.io.File;
20+
import java.util.Arrays;
21+
import java.util.List;
22+
import java.util.regex.Matcher;
23+
import java.util.regex.Pattern;
24+
25+
/**
26+
* Implementation based on 'jdk.internal.module.ModulePath#deriveModuleDescriptor' and related methods.
27+
*/
28+
class ModuleNameUtil {
29+
30+
private static final Pattern DASH_VERSION = Pattern.compile("-(\\d+(\\.|$))");
31+
private static final Pattern NON_ALPHANUM = Pattern.compile("[^A-Za-z0-9]");
32+
private static final Pattern REPEATING_DOTS = Pattern.compile("(\\.)(\\1)+");
33+
private static final Pattern LEADING_DOTS = Pattern.compile("^\\.");
34+
private static final Pattern TRAILING_DOTS = Pattern.compile("\\.$");
35+
36+
static String automaticModulNameFromFileName(File jarFile) {
37+
// Derive the version, and the module name if needed, from JAR file name
38+
String fn = jarFile.getName();
39+
int i = fn.lastIndexOf(File.separator);
40+
if (i != -1)
41+
fn = fn.substring(i + 1);
42+
43+
// drop ".jar"
44+
String name = fn.substring(0, fn.length() - 4);
45+
46+
// find first occurrence of -${NUMBER}. or -${NUMBER}$
47+
Matcher matcher = DASH_VERSION.matcher(name);
48+
if (matcher.find()) {
49+
name = name.substring(0, matcher.start());
50+
}
51+
return validateModuleName(cleanModuleName(name));
52+
}
53+
54+
static String validateModuleName(String name) {
55+
int next;
56+
int off = 0;
57+
while ((next = name.indexOf('.', off)) != -1) {
58+
String id = name.substring(off, next);
59+
if (!isJavaIdentifier(id)) {
60+
throw new IllegalArgumentException(name + ": Invalid module name"
61+
+ ": '" + id + "' is not a Java identifier");
62+
}
63+
off = next+1;
64+
}
65+
String last = name.substring(off);
66+
if (!isJavaIdentifier(last)) {
67+
throw new IllegalArgumentException(name + ": Invalid module name"
68+
+ ": '" + last + "' is not a Java identifier");
69+
}
70+
return name;
71+
}
72+
73+
private static String cleanModuleName(String mn) {
74+
// replace non-alphanumeric
75+
mn = NON_ALPHANUM.matcher(mn).replaceAll(".");
76+
77+
// collapse repeating dots
78+
mn = REPEATING_DOTS.matcher(mn).replaceAll(".");
79+
80+
// drop leading dots
81+
if (!mn.isEmpty() && mn.charAt(0) == '.')
82+
mn = LEADING_DOTS.matcher(mn).replaceAll("");
83+
84+
// drop trailing dots
85+
int len = mn.length();
86+
if (len > 0 && mn.charAt(len-1) == '.')
87+
mn = TRAILING_DOTS.matcher(mn).replaceAll("");
88+
89+
return mn;
90+
}
91+
92+
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
93+
private static boolean isJavaIdentifier(String str) {
94+
if (str.isEmpty() || RESERVED.contains(str))
95+
return false;
96+
97+
int first = Character.codePointAt(str, 0);
98+
if (!Character.isJavaIdentifierStart(first))
99+
return false;
100+
101+
int i = Character.charCount(first);
102+
while (i < str.length()) {
103+
int cp = Character.codePointAt(str, i);
104+
if (!Character.isJavaIdentifierPart(cp))
105+
return false;
106+
i += Character.charCount(cp);
107+
}
108+
109+
return true;
110+
}
111+
112+
// keywords, boolean and null literals, not allowed in identifiers
113+
private static final List<String> RESERVED = Arrays.asList(
114+
"abstract",
115+
"assert",
116+
"boolean",
117+
"break",
118+
"byte",
119+
"case",
120+
"catch",
121+
"char",
122+
"class",
123+
"const",
124+
"continue",
125+
"default",
126+
"do",
127+
"double",
128+
"else",
129+
"enum",
130+
"extends",
131+
"final",
132+
"finally",
133+
"float",
134+
"for",
135+
"goto",
136+
"if",
137+
"implements",
138+
"import",
139+
"instanceof",
140+
"int",
141+
"interface",
142+
"long",
143+
"native",
144+
"new",
145+
"package",
146+
"private",
147+
"protected",
148+
"public",
149+
"return",
150+
"short",
151+
"static",
152+
"strictfp",
153+
"super",
154+
"switch",
155+
"synchronized",
156+
"this",
157+
"throw",
158+
"throws",
159+
"transient",
160+
"try",
161+
"void",
162+
"volatile",
163+
"while",
164+
"true",
165+
"false",
166+
"null",
167+
"_"
168+
);
169+
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
import java.util.List;
2525

2626
import static org.gradlex.javamodule.moduleinfo.IdValidator.validateIdentifier;
27-
import static org.gradlex.javamodule.moduleinfo.IdValidator.validateModuleName;
27+
import static org.gradlex.javamodule.moduleinfo.ModuleNameUtil.validateModuleName;
2828

2929
/**
3030
* Details that real Modules and Automatic-Module-Names share.

0 commit comments

Comments
 (0)