Skip to content

Commit

Permalink
Add 'getAutoCreateAutomaticModules' option
Browse files Browse the repository at this point in the history
Resolves #74
  • Loading branch information
jjohannes committed Nov 17, 2023
1 parent 2f90e74 commit 1bea128
Show file tree
Hide file tree
Showing 6 changed files with 304 additions and 9 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
package org.gradlex.javamodule.moduleinfo;

import java.io.File;
import java.util.Arrays;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
* Implementation based on 'jdk.internal.module.ModulePath#deriveModuleDescriptor' and related methods.
*/
class AutomaticModuleNameUtil {

private static final Pattern DASH_VERSION = Pattern.compile("-(\\d+(\\.|$))");
private static final Pattern NON_ALPHANUM = Pattern.compile("[^A-Za-z0-9]");
private static final Pattern REPEATING_DOTS = Pattern.compile("(\\.)(\\1)+");
private static final Pattern LEADING_DOTS = Pattern.compile("^\\.");
private static final Pattern TRAILING_DOTS = Pattern.compile("\\.$");

static String automaticModulNameFromFileName(File jarFile) {
// Derive the version, and the module name if needed, from JAR file name
String fn = jarFile.getName();
int i = fn.lastIndexOf(File.separator);
if (i != -1)
fn = fn.substring(i + 1);

// drop ".jar"
String name = fn.substring(0, fn.length() - 4);

// find first occurrence of -${NUMBER}. or -${NUMBER}$
Matcher matcher = DASH_VERSION.matcher(name);
if (matcher.find()) {
name = name.substring(0, matcher.start());
}
return checkValidModuleName(cleanModuleName(name));
}

private static String cleanModuleName(String mn) {
// replace non-alphanumeric
mn = NON_ALPHANUM.matcher(mn).replaceAll(".");

// collapse repeating dots
mn = REPEATING_DOTS.matcher(mn).replaceAll(".");

// drop leading dots
if (!mn.isEmpty() && mn.charAt(0) == '.')
mn = LEADING_DOTS.matcher(mn).replaceAll("");

// drop trailing dots
int len = mn.length();
if (len > 0 && mn.charAt(len-1) == '.')
mn = TRAILING_DOTS.matcher(mn).replaceAll("");

return mn;
}

public static String checkValidModuleName(String name) {
int next;
int off = 0;
while ((next = name.indexOf('.', off)) != -1) {
String id = name.substring(off, next);
if (!isJavaIdentifier(id)) {
throw new IllegalArgumentException(name + ": Invalid module name"
+ ": '" + id + "' is not a Java identifier");
}
off = next+1;
}
String last = name.substring(off);
if (!isJavaIdentifier(last)) {
throw new IllegalArgumentException(name + ": Invalid module name"
+ ": '" + last + "' is not a Java identifier");
}
return name;
}

@SuppressWarnings("BooleanMethodIsAlwaysInverted")
private static boolean isJavaIdentifier(String str) {
if (str.isEmpty() || RESERVED.contains(str))
return false;

int first = Character.codePointAt(str, 0);
if (!Character.isJavaIdentifierStart(first))
return false;

int i = Character.charCount(first);
while (i < str.length()) {
int cp = Character.codePointAt(str, i);
if (!Character.isJavaIdentifierPart(cp))
return false;
i += Character.charCount(cp);
}

return true;
}

// keywords, boolean and null literals, not allowed in identifiers
private static final List<String> RESERVED = Arrays.asList(
"abstract",
"assert",
"boolean",
"break",
"byte",
"case",
"catch",
"char",
"class",
"const",
"continue",
"default",
"do",
"double",
"else",
"enum",
"extends",
"final",
"finally",
"float",
"for",
"goto",
"if",
"implements",
"import",
"instanceof",
"int",
"interface",
"long",
"native",
"new",
"package",
"private",
"protected",
"public",
"return",
"short",
"static",
"strictfp",
"super",
"switch",
"synchronized",
"this",
"throw",
"throws",
"transient",
"try",
"void",
"volatile",
"while",
"true",
"false",
"null",
"_"
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ public void apply(Project project) {
ExtraJavaModuleInfoPluginExtension extension = project.getExtensions().create("extraJavaModuleInfo", ExtraJavaModuleInfoPluginExtension.class);
extension.getFailOnMissingModuleInfo().convention(true);
extension.getFailOnAutomaticModules().convention(false);
extension.getAutoCreateAutomaticModules().convention(false);

// setup the transform and the tasks for all projects in the build
project.getPlugins().withType(JavaPlugin.class).configureEach(javaPlugin -> {
Expand Down Expand Up @@ -173,6 +174,7 @@ private void registerTransform(String fileExtension, Project project, ExtraJavaM
p.getModuleSpecs().set(extension.getModuleSpecs());
p.getFailOnMissingModuleInfo().set(extension.getFailOnMissingModuleInfo());
p.getFailOnAutomaticModules().set(extension.getFailOnAutomaticModules());
p.getAutoCreateAutomaticModules().set(extension.getAutoCreateAutomaticModules());

// See: https://github.com/adammurdoch/dependency-graph-as-task-inputs/blob/main/plugins/src/main/java/TestPlugin.java
Provider<Set<ResolvedArtifactResult>> artifacts = project.provider(() ->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ public abstract class ExtraJavaModuleInfoPluginExtension {
public abstract MapProperty<String, ModuleSpec> getModuleSpecs();
public abstract Property<Boolean> getFailOnMissingModuleInfo();
public abstract Property<Boolean> getFailOnAutomaticModules();
public abstract Property<Boolean> getAutoCreateAutomaticModules();

/**
* Add full module information for a given Jar file.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,12 +48,12 @@
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.TreeSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.TreeSet;
import java.util.jar.JarEntry;
import java.util.jar.JarInputStream;
import java.util.jar.JarOutputStream;
Expand All @@ -64,6 +64,7 @@
import java.util.zip.ZipEntry;
import java.util.zip.ZipException;

import static org.gradlex.javamodule.moduleinfo.AutomaticModuleNameUtil.automaticModulNameFromFileName;
import static org.gradlex.javamodule.moduleinfo.FilePathToModuleCoordinates.gaCoordinatesFromFilePathMatch;
import static org.gradlex.javamodule.moduleinfo.FilePathToModuleCoordinates.versionFromFilePath;

Expand All @@ -84,18 +85,28 @@ public abstract class ExtraJavaModuleInfoTransform implements TransformAction<Ex
public interface Parameter extends TransformParameters {
@Input
MapProperty<String, ModuleSpec> getModuleSpecs();

@Input
Property<Boolean> getFailOnMissingModuleInfo();

@Input
Property<Boolean> getFailOnAutomaticModules();

@Input
Property<Boolean> getAutoCreateAutomaticModules();

@Input
ListProperty<String> getMergeJarIds();

@InputFiles
ListProperty<RegularFile> getMergeJars();

@Input
MapProperty<String, Set<String>> getCompileClasspathDependencies();

@Input
MapProperty<String, Set<String>> getRuntimeClasspathDependencies();

@Input
MapProperty<String, Set<String>> getAnnotationProcessorClasspathDependencies();
}
Expand Down Expand Up @@ -135,12 +146,13 @@ public void transform(TransformOutputs outputs) {
throw new RuntimeException("Found an automatic module: " + originalJar.getName());
}
outputs.file(originalJar);
} else if (parameters.getAutoCreateAutomaticModules().get()) {
String automaticName = automaticModulNameFromFileName(originalJar);
addAutomaticModuleName(originalJar, getModuleJar(outputs, originalJar), new AutomaticModuleName(originalJar.getName(), automaticName));
} else if (parameters.getFailOnMissingModuleInfo().get()) {
throw new RuntimeException("Not a module and no mapping defined: " + originalJar.getName());
} else {
if (parameters.getFailOnMissingModuleInfo().get()) {
throw new RuntimeException("Not a module and no mapping defined: " + originalJar.getName());
} else {
outputs.file(originalJar);
}
outputs.file(originalJar);
}
}

Expand Down Expand Up @@ -175,7 +187,7 @@ private boolean isModule(File jar) {
// - https://github.com/jjohannes/extra-java-module-info/issues/78
return true;
}
try (JarInputStream inputStream = new JarInputStream(Files.newInputStream(jar.toPath()))) {
try (JarInputStream inputStream = new JarInputStream(Files.newInputStream(jar.toPath()))) {
boolean isMultiReleaseJar = containsMultiReleaseJarEntry(inputStream);
ZipEntry next = inputStream.getNextEntry();
while (next != null) {
Expand Down Expand Up @@ -351,7 +363,7 @@ private byte[] addModuleInfo(ModuleInfo moduleInfo, Map<String, List<String>> pr
allDependencies.addAll(compileDependencies);
allDependencies.addAll(runtimeDependencies);
allDependencies.addAll(annotationProcessorDependencies);
for (String ga: allDependencies) {
for (String ga : allDependencies) {
String moduleName = gaToModuleName(ga);
if (compileDependencies.contains(ga) && !runtimeDependencies.contains(ga)) {
moduleVisitor.visitRequire(moduleName, Opcodes.ACC_STATIC_PHASE, null);
Expand Down Expand Up @@ -381,7 +393,7 @@ private byte[] addModuleInfo(ModuleInfo moduleInfo, Map<String, List<String>> pr
List<String> implementations = entry.getValue();
if (!moduleInfo.ignoreServiceProviders.contains(name)) {
moduleVisitor.visitProvide(name.replace('.', '/'),
implementations.stream().map(impl -> impl.replace('.','/')).toArray(String[]::new));
implementations.stream().map(impl -> impl.replace('.', '/')).toArray(String[]::new));
}
}
moduleVisitor.visitEnd();
Expand Down Expand Up @@ -461,4 +473,5 @@ private String gaToModuleName(String ga) {
private static boolean isModuleInfoClass(String jarEntryName) {
return "module-info.class".equals(jarEntryName) || MODULE_INFO_CLASS_MRJAR_PATH.matcher(jarEntryName).matches();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -693,4 +693,113 @@ org.apache.qpid.server.management.plugin.ConfiguredObjectRegistrationImpl''')
run().task(':run').outcome == TaskOutcome.SUCCESS
}

def "can add module information to legacy library"() {
given:
file("src/main/java/org/gradle/sample/app/Main.java") << """
package org.gradle.sample.app;
import com.google.gson.Gson;
import org.apache.commons.beanutils.BeanUtils;
import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.CommandLineParser;
import org.apache.commons.cli.DefaultParser;
import org.apache.commons.cli.Options;
import org.apache.commons.lang3.StringUtils;
import org.gradle.sample.app.data.Message;
public class Main {
public static void main(String[] args) throws Exception {
Options options = new Options();
options.addOption("json", true, "data to parse");
options.addOption("debug", false, "prints module infos");
CommandLineParser parser = new DefaultParser();
CommandLine cmd = parser.parse(options, args);
if (cmd.hasOption("debug")) {
printModuleDebug(Main.class);
printModuleDebug(Gson.class);
printModuleDebug(StringUtils.class);
printModuleDebug(CommandLine.class);
printModuleDebug(BeanUtils.class);
}
String json = cmd.getOptionValue("json");
Message message = new Gson().fromJson(json == null ? "{}" : json, Message.class);
Object copy = BeanUtils.cloneBean(message);
System.out.println();
System.out.println("Original: " + copy.toString());
System.out.println("Copy: " + copy.toString());
}
private static void printModuleDebug(Class<?> clazz) {
System.out.println(clazz.getModule().getName() + " - " + clazz.getModule().getDescriptor().version().get());
}
}
"""
file("src/main/java/org/gradle/sample/app/data/Message.java") << """
package org.gradle.sample.app.data;
import java.util.List;
import java.util.ArrayList;
public class Message {
private String message;
private List<String> receivers = new ArrayList<>();
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public List<String> getReceivers() {
return receivers;
}
public void setReceivers(List<String> receivers) {
this.receivers = receivers;
}
@Override
public String toString() {
return "Message{message='" + message + '\\'' +
", receivers=" + receivers + '}';
}
}
"""
file("src/main/java/module-info.java") << """
module org.gradle.sample.app {
exports org.gradle.sample.app;
opens org.gradle.sample.app.data; // allow Gson to access via reflection
requires com.google.gson;
requires org.apache.commons.lang3;
requires commons.cli;
requires commons.beanutils;
}
"""
buildFile << """
dependencies {
implementation("com.google.code.gson:gson:2.8.6") // real module
implementation("net.bytebuddy:byte-buddy:1.10.9") // real module with multi-release jar
implementation("org.apache.commons:commons-lang3:3.10") // automatic module
implementation("commons-beanutils:commons-beanutils:1.9.4") // plain library (also brings in other libraries transitively)
implementation("commons-cli:commons-cli:1.4") // plain library
}
extraJavaModuleInfo {
autoCreateAutomaticModules = true
}
"""

expect:
build().task(':compileJava').outcome == TaskOutcome.SUCCESS
}

}
Loading

0 comments on commit 1bea128

Please sign in to comment.