package picocli.codegen.aot.graalvm;
import picocli.CommandLine;
import picocli.CommandLine.Command;
import picocli.CommandLine.Mixin;
import picocli.CommandLine.Model.CommandSpec;
import picocli.CommandLine.Option;
import picocli.CommandLine.Parameters;
import picocli.codegen.util.Util;
import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import java.lang.reflect.Proxy;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.Callable;
/**
* {@code DynamicProxyConfigGenerator} generates a JSON String with the fully qualified interface names for which
* dynamic proxy classes should be generated at native image build time.
*
* Substrate VM doesn't provide machinery for generating and interpreting bytecodes at run time.
* Therefore all dynamic proxy classes
* need to be generated
* at native image build time.
*
* The output of {@code DynamicProxyConfigGenerator} is intended to be passed to the {@code -H:DynamicProxyConfigurationFiles=/path/to/proxy-config.json}
* option of the {@code native-image} GraalVM utility.
* This allows picocli-based native image applications that use {@code @Command}-annotated interfaces with
* {@code @Option} and {@code @Parameters}-annotated methods to define options and positional parameters.
*
* Alternatively, the generated configuration
* files can be supplied to the {@code native-image} tool by placing them in a
* {@code META-INF/native-image/} directory on the class path, for example, in a JAR file used in the image build.
* This directory (or any of its subdirectories) is searched for files with the names {@code jni-config.json},
* {@code reflect-config.json}, {@code proxy-config.json} and {@code resource-config.json}, which are then automatically
* included in the build. Not all of those files must be present.
* When multiple files with the same name are found, all of them are included.
*
*
* @since 4.0
*/
public class DynamicProxyConfigGenerator {
@Command(name = "gen-proxy-config", showAtFileInUsageHelp = true, sortOptions = false,
description = {"Generates a JSON file with the interface names to generate dynamic proxy classes for in the native image.",
"The generated JSON file can be passed to the `-H:DynamicProxyConfigurationFiles=/path/to/proxy-config.json` " +
"option of the `native-image` GraalVM utility.",
"See https://github.com/oracle/graal/blob/master/substratevm/DynamicProxy.md"},
exitCodeListHeading = "%nExit Codes (if enabled with `--exit`)%n",
exitCodeList = {
"0:Successful program execution.",
"1:A runtime exception occurred while generating man pages.",
"2:Usage error: user input for the command was incorrect, " +
"e.g., the wrong number of arguments, a bad flag, " +
"a bad syntax in a parameter, etc."
},
footerHeading = "%nExample%n",
footer = {
" java -cp \"myapp.jar;picocli-4.6.1.jar;picocli-codegen-4.6.1.jar\" " +
"picocli.codegen.aot.graalvm.DynamicProxyConfigGenerator my.pkg.MyClass"
},
mixinStandardHelpOptions = true, version = "picocli-codegen gen-proxy-config " + CommandLine.VERSION)
private static class App implements Callable {
@Parameters(arity = "0..*", description = "Zero or more `@Command` interfaces or classes with `@Command` interface subcommands to generate a Graal SubstrateVM proxy-config for.")
Class>[] classes = new Class>[0];
@Option(names = {"-i", "--interface"}, description = "Other fully qualified interface names to generate dynamic proxy classes for in the native image." +
"This option may be specified multiple times with different interface names. " +
"Specify multiple comma-separated interface names for dynamic proxies that implement multiple interfaces.")
String[] interfaces = new String[0];
@Option(names = {"-c", "--factory"}, description = "Optionally specify the fully qualified class name of the custom factory to use to instantiate the command class. " +
"When omitted, the default picocli factory is used.")
String factoryClass;
@Mixin
OutputFileMixin outputFile = new OutputFileMixin();
@Option(names = "--exit", negatable = true,
description = "Specify `--exit` if you want the application to call `System.exit` when finished. " +
"By default, `System.exit` is not called.")
boolean exit;
public Integer call() throws Exception {
List specs = Util.getCommandSpecs(factoryClass, classes);
String result = DynamicProxyConfigGenerator.generateProxyConfig(specs.toArray(new CommandSpec[0]), interfaces);
outputFile.write(result);
return 0;
}
}
/**
* Runs this class as a standalone application, printing the resulting JSON String to a file or to {@code System.out}.
* @param args one or more fully qualified class names of {@code @Command}-annotated classes.
*/
public static void main(String... args) {
App app = new App();
int exitCode = new CommandLine(app).execute(args);
if (app.exit) {
System.exit(exitCode);
}
}
/**
* Returns a JSON String with the interface names to generate dynamic proxy classes for in the native image,
* used by the specified {@code CommandSpec} objects.
*
* @param specs one or more {@code CommandSpec} objects to inspect for dynamic proxies
* @param interfaceClasses other (non-{@code @Command}) fully qualified interface names to generate dynamic proxy classes for
* @return a JSON String in the format
* required by the {@code -H:DynamicProxyConfigurationFiles=/path/to/proxy-config.json} option of the GraalVM {@code native-image} utility.
*/
public static String generateProxyConfig(CommandSpec[] specs, String[] interfaceClasses) {
Visitor visitor = new Visitor(interfaceClasses);
for (CommandSpec spec : specs) {
visitor.visitCommandSpec(spec);
}
return visitor.toString();
}
static final class Visitor {
List interfaces = new ArrayList();
List commandInterfaces = new ArrayList();
Visitor(String[] interfaceClasses) {
interfaces.addAll(Arrays.asList(interfaceClasses));
}
void visitCommandSpec(CommandSpec spec) {
Object userObject = spec.userObject();
if (Proxy.isProxyClass(userObject.getClass())) {
Class>[] interfaces = userObject.getClass().getInterfaces();
String names = "";
for (Class> interf : interfaces) {
if (names.length() > 0) { names += ","; }
names += interf.getCanonicalName(); // TODO or Class.getName()?
}
if (names.length() > 0) {
commandInterfaces.add(names);
}
} else if (spec.userObject() instanceof Element && ((Element) spec.userObject()).getKind() == ElementKind.INTERFACE) {
commandInterfaces.add(((Element) spec.userObject()).asType().toString());
}
for (CommandSpec mixin : spec.mixins().values()) {
visitCommandSpec(mixin);
}
for (CommandLine sub : spec.subcommands().values()) {
visitCommandSpec(sub.getCommandSpec());
}
}
@Override
public String toString() {
return String.format("" +
"[%s%n" +
"]%n", all());
}
@SuppressWarnings("unchecked")
private StringBuilder all() {
return json(commandInterfaces, interfaces);
}
private static StringBuilder json(List... stringLists) {
StringBuilder result = new StringBuilder(1024);
for (List list : stringLists) {
for (String str : list) {
if (result.length() > 0) {
result.append(",");
}
String[] names = str.split(",");
String formatted = "";
for (String name : names) {
if (formatted.length() > 0) {
formatted += ", ";
}
formatted += '"' + name + '"';
}
result.append(String.format("%n [%s]", formatted));
}
}
return result;
}
}
}