package picocli.codegen.aot.graalvm; import picocli.CommandLine; import picocli.CommandLine.Command; import picocli.CommandLine.Mixin; import picocli.CommandLine.Model.ArgGroupSpec; import picocli.CommandLine.Model.ArgSpec; import picocli.CommandLine.Model.CommandSpec; import picocli.CommandLine.Model.IAnnotatedElement; import picocli.CommandLine.Model.IGetter; import picocli.CommandLine.Model.IScope; import picocli.CommandLine.Model.ISetter; import picocli.CommandLine.Model.OptionSpec; import picocli.CommandLine.Model.PositionalParamSpec; import picocli.CommandLine.Model.UnmatchedArgsBinding; import picocli.CommandLine.Option; import picocli.CommandLine.Parameters; import picocli.codegen.annotation.processing.ITypeMetaData; import picocli.codegen.annotation.processing.AnnotatedElementHolder; import picocli.codegen.util.Util; import javax.lang.model.element.Element; import javax.lang.model.element.ElementKind; import javax.lang.model.element.ExecutableElement; import javax.lang.model.element.TypeElement; import javax.lang.model.element.VariableElement; import javax.lang.model.type.*; import javax.lang.model.util.SimpleElementVisitor6; import javax.lang.model.util.SimpleTypeVisitor6; import java.io.File; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.lang.reflect.Proxy; import java.util.ArrayList; import java.util.Arrays; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.TreeMap; import java.util.TreeSet; import java.util.concurrent.Callable; /** * {@code ReflectionConfigGenerator} generates a JSON String with the program elements that will be accessed * reflectively in a picocli-based application, in order to compile this application ahead-of-time into a native * executable with GraalVM. *

* GraalVM has limited support for Java * reflection and it needs to know ahead of time the reflectively accessed program elements. *

* The output of {@code ReflectionConfigGenerator} is intended to be passed to the {@code -H:ReflectionConfigurationFiles=/path/to/reflect-config.json} * option of the {@code native-image} GraalVM utility. * This allows picocli-based applications to be compiled to a native image. *

* 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. *

* If necessary, it is possible to exclude classes with system property {@code picocli.codegen.excludes}, * which accepts a comma-separated list of regular expressions of the fully qualified class names that should * not be included in the resulting JSON String. *

* * @since 3.7.0 */ public class ReflectionConfigGenerator { private static final String SYSPROP_CODEGEN_EXCLUDES = "picocli.codegen.excludes"; private static final String REFLECTED_FIELD_BINDING_CLASS = "picocli.CommandLine$Model$FieldBinding"; private static final String REFLECTED_METHOD_BINDING_CLASS = "picocli.CommandLine$Model$MethodBinding"; private static final String REFLECTED_PROXY_METHOD_BINDING_CLASS = "picocli.CommandLine$Model$PicocliInvocationHandler$ProxyBinding"; private static final String REFLECTED_FIELD_BINDING_FIELD = "field"; private static final String REFLECTED_METHOD_BINDING_METHOD = "method"; private static final String REFLECTED_BINDING_FIELD_SCOPE = "scope"; @Command(name = "gen-reflect-config", showAtFileInUsageHelp = true, description = {"Generates a JSON file with the program elements that will be " + "accessed reflectively for the specified `@Command` classes.", "The generated JSON file can be passed to the `-H:ReflectionConfigurationFiles=/path/to/reflect-config.json` " + "option of the `native-image` GraalVM utility.", "See https://github.com/oracle/graal/blob/master/substratevm/Reflection.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.ReflectionConfigGenerator my.pkg.MyClass" }, mixinStandardHelpOptions = true, sortOptions = false, version = "picocli-codegen ${COMMAND-NAME} " + CommandLine.VERSION) private static class App implements Callable { @Parameters(arity = "1..*", description = "One or more classes to generate a GraalVM ReflectionConfiguration for.") Class[] classes = new Class[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 = ReflectionConfigGenerator.generateReflectionConfig(specs.toArray(new CommandSpec[0])); 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 program elements that will be accessed reflectively for the specified * {@code CommandSpec} objects. * * @param specs one or more {@code CommandSpec} objects to inspect * @return a JSON String in the format * required by the {@code -H:ReflectionConfigurationFiles=/path/to/reflect-config.json} option of the GraalVM {@code native-image} utility. * @throws NoSuchFieldException if a problem occurs while processing the specified specs * @throws IllegalAccessException if a problem occurs while processing the specified specs */ public static String generateReflectionConfig(CommandSpec... specs) throws Exception { Visitor visitor = new Visitor(); for (CommandSpec spec : specs) { visitor.visitCommandSpec(spec); } return generateReflectionConfig(visitor).toString(); } private static StringBuilder generateReflectionConfig(Visitor visited) { StringBuilder result = new StringBuilder(1024); String prefix = String.format("[%n"); String suffix = String.format("%n]%n"); for (ReflectedClass cls : visited.visited.values()) { result.append(prefix).append(cls); prefix = String.format(",%n"); } return result.append(suffix); } static final class Visitor { static Set excluded = new HashSet(Arrays.asList( boolean.class.getName(), boolean[].class.getName(), boolean[].class.getCanonicalName(), byte.class.getName(), byte[].class.getName(), byte[].class.getCanonicalName(), char.class.getName(), char[].class.getName(), char[].class.getCanonicalName(), double.class.getName(), double[].class.getName(), double[].class.getCanonicalName(), float.class.getName(), float[].class.getName(), float[].class.getCanonicalName(), int.class.getName(), int[].class.getName(), int[].class.getCanonicalName(), long.class.getName(), long[].class.getName(), long[].class.getCanonicalName(), short.class.getName(), short[].class.getName(), short[].class.getCanonicalName(), CommandSpec.class.getName(), Method.class.getName(), Object.class.getName(), String.class.getName(), String[].class.getName(), String[].class.getCanonicalName(), File.class.getName(), File[].class.getName(), File[].class.getCanonicalName(), List.class.getName(), Set.class.getName(), Map.class.getName(), Class.class.getName(), Class[].class.getName(), Class[].class.getCanonicalName(), "java.lang.Class", "java.lang.Class[]", "java.lang.reflect.Executable", // addMethod("getParameters") "java.lang.reflect.Parameter", // addMethod("getName"); "org.fusesource.jansi.AnsiConsole", // addField("out", false); "java.util.ResourceBundle", // addMethod("getBaseBundleName"); "java.time.Duration", // addMethod("parse", CharSequence.class); "java.time.Instant", // addMethod("parse", CharSequence.class); "java.time.LocalDate", // addMethod("parse", CharSequence.class); "java.time.LocalDateTime", // addMethod("parse", CharSequence.class); "java.time.LocalTime", // addMethod("parse", CharSequence.class); "java.time.MonthDay", // addMethod("parse", CharSequence.class); "java.time.OffsetDateTime", // addMethod("parse", CharSequence.class); "java.time.OffsetTime", // addMethod("parse", CharSequence.class); "java.time.Period", // addMethod("parse", CharSequence.class); "java.time.Year", // addMethod("parse", CharSequence.class); "java.time.YearMonth", // addMethod("parse", CharSequence.class); "java.time.ZonedDateTime", // addMethod("parse", CharSequence.class); "java.time.ZoneId", // addMethod("of", String.class); "java.time.ZoneOffset", // addMethod("of", String.class); "java.nio.file.Path", "java.nio.file.Paths", // addMethod("get", String.class, String[].class); "java.sql.Connection", "java.sql.Driver", "java.sql.DriverManager", // .addMethod("getConnection", String.class) .addMethod("getDriver", String.class); "java.sql.Time", // constructor long "java.sql.Timestamp" // addMethod("valueOf", String.class); )); Map visited = new TreeMap(); void visitCommandSpec(CommandSpec spec) throws Exception { Object userObject = spec.userObject(); if (userObject != null) { if (userObject instanceof Method) { Method method = (Method) spec.userObject(); ReflectedClass cls = getOrCreateClass(method.getDeclaringClass()); cls.addMethod(method); } else if (userObject instanceof Element) { visitElement((Element) userObject); } else if (Proxy.isProxyClass(spec.userObject().getClass())) { // do nothing: requires DynamicProxyConfigGenerator } else { visitAnnotatedFields(spec.userObject().getClass()); } } visitObjectType(spec.versionProvider()); visitObjectType(spec.defaultValueProvider()); for (UnmatchedArgsBinding binding : spec.unmatchedArgsBindings()) { visitGetter(binding.getter()); visitSetter(binding.setter()); } for (IAnnotatedElement specElement : spec.specElements()) { visitGetter(specElement.getter()); visitSetter(specElement.setter()); } for (IAnnotatedElement parentCommandElement : spec.parentCommandElements()) { visitGetter(parentCommandElement.getter()); visitSetter(parentCommandElement.setter()); } for (OptionSpec option : spec.options()) { visitArgSpec(option); } for (PositionalParamSpec positional : spec.positionalParameters()) { visitArgSpec(positional); } for (ArgGroupSpec group : spec.argGroups()) { visitGroupSpec(group); } for (Map.Entry entry : spec.mixins().entrySet()) { CommandSpec mixin = entry.getValue(); visitCommandSpec(mixin); String name = entry.getKey(); IAnnotatedElement annotatedElement = spec.mixinAnnotatedElements().get(name); if (annotatedElement != null) { visitGetter(annotatedElement.getter()); } } for (CommandLine sub : spec.subcommands().values()) { visitCommandSpec(sub.getCommandSpec()); } } @SuppressWarnings("deprecation") // SimpleElementVisitor6 is deprecated in Java 9 private void visitElement(Element element) { element.accept(new SimpleElementVisitor6() { @Override public Void visitVariable(VariableElement e, Void aVoid) { if (!e.asType().getKind().isPrimitive() && !(e.getKind() == ElementKind.INTERFACE && e.asType().toString().startsWith("java"))) { getOrCreateClassByName(elementTypeName(e.asType())); } if (e.getKind() == ElementKind.FIELD) { TypeElement type = (TypeElement) e.getEnclosingElement(); ReflectedClass cls = getOrCreateClassByName(elementTypeName(type.asType())); cls.addField(e.getSimpleName().toString(), e.getModifiers().contains(javax.lang.model.element.Modifier.FINAL)); } return null; } @Override public Void visitType(TypeElement e, Void aVoid) { if (!e.asType().getKind().isPrimitive() && !(e.getKind() == ElementKind.INTERFACE && e.getQualifiedName().toString().startsWith("java"))) { getOrCreateClassByName(elementTypeName(e.asType())); } return null; } @Override public Void visitExecutable(ExecutableElement method, Void aVoid) { TypeElement type = (TypeElement) method.getEnclosingElement(); ReflectedClass cls = getOrCreateClassByName(elementTypeName(type.asType())); List parameters = method.getParameters(); List paramTypeNames = new ArrayList(); for (VariableElement param : parameters) { param.asType().accept(new SimpleTypeVisitor6>() { @Override public Void visitPrimitive(PrimitiveType t, List collect) { collect.add(t.toString()); return null; } @Override public Void visitArray(ArrayType t, List collect) { collect.add(t.toString()); return null; } @Override public Void visitDeclared(DeclaredType t, List collect) { collect.add(elementTypeName(t)); // t.toString() would give "java.util.List" return null; } }, paramTypeNames); } cls.addMethod(method.getSimpleName().toString(), paramTypeNames); return null; } }, null); } // convert canonical type names (picocli.AutoComplete.App) to class name (picocli.AutoComplete$App) // convert generic (java.util.List) to raw (java.util.List) private String elementTypeName(TypeMirror typeMirror) { @SuppressWarnings("deprecation") // SimpleElementVisitor6 is deprecated in Java 9 String result = typeMirror.accept(new SimpleTypeVisitor6() { @Override public String visitDeclared(DeclaredType declaredType, Void aVoid) { TypeElement typeElement = (TypeElement) declaredType.asElement(); if (typeElement.getNestingKind().isNested()) { return elementTypeName(typeElement.getEnclosingElement().asType()) + "$" + typeElement.getSimpleName(); } String raw = typeElement.getQualifiedName().toString(); return raw; } @Override public String visitArray(ArrayType arrayType, Void aVoid) { return elementTypeName(arrayType.getComponentType()) + "[]"; } }, null); if (result == null) { return typeMirror.toString(); } return result; } private void visitAnnotatedFields(Class cls) { if (cls == null) { return; } ReflectedClass reflectedClass = getOrCreateClass(cls); Field[] declaredFields = cls.getDeclaredFields(); for (Field f : declaredFields) { if (f.isAnnotationPresent(CommandLine.Spec.class)) { reflectedClass.addField(f); } if (f.isAnnotationPresent(CommandLine.ParentCommand.class)) { reflectedClass.addField(f); } if (f.isAnnotationPresent(Mixin.class)) { reflectedClass.addField(f); } if (f.isAnnotationPresent(CommandLine.Unmatched.class)) { reflectedClass.addField(f); } } visitAnnotatedFields(cls.getSuperclass()); } private void visitArgSpec(ArgSpec argSpec) throws Exception { visitGetter(argSpec.getter()); visitSetter(argSpec.setter()); visitTypeInfo(argSpec.typeInfo()); visitObjectType(argSpec.completionCandidates()); visitObjectType(argSpec.parameterConsumer()); visitObjectTypes(argSpec.converters()); } private void visitGroupSpec(ArgGroupSpec group) throws Exception { IScope scope = group.scope(); if (scope != null) { Object scopeValue = scope.get(); if (scopeValue != null) { getOrCreateClass(scopeValue.getClass()); } } visitGetter(group.getter()); visitSetter(group.setter()); for (ArgSpec argSpec : group.args()) { visitArgSpec(argSpec); } for (ArgGroupSpec subGroup : group.subgroups()) { visitGroupSpec(subGroup); } } private void visitTypeInfo(CommandLine.Model.ITypeInfo typeInfo) { getOrCreateClassByName(typeInfo.getClassName()); for (CommandLine.Model.ITypeInfo aux : typeInfo.getAuxiliaryTypeInfos()) { getOrCreateClassByName(aux.getClassName()); } } private void visitType(Class type) { if (type != null) { getOrCreateClass(type); } } private void visitObjectType(Object object) { if (object == null) { return; } if (object instanceof ITypeMetaData) { visitElement(((ITypeMetaData) object).getTypeElement()); } else { visitType(object.getClass()); } } private void visitObjectTypes(Object[] array) { if (array != null) { for (Object element : array) { visitObjectType(element); } } } private void visitGetter(IGetter getter) throws Exception { if (getter == null) { return; } if (getter instanceof AnnotatedElementHolder) { AnnotatedElementHolder metaData = (AnnotatedElementHolder) getter; visitElement(metaData.getElement()); } else if (REFLECTED_FIELD_BINDING_CLASS.equals(getter.getClass().getName())) { visitFieldBinding(getter); } else if (REFLECTED_METHOD_BINDING_CLASS.equals(getter.getClass().getName())) { visitMethodBinding(getter); } else if (REFLECTED_PROXY_METHOD_BINDING_CLASS.equals(getter.getClass().getName())) { visitProxyMethodBinding(getter); } } private void visitSetter(ISetter setter) throws Exception { if (setter == null) { return; } if (setter instanceof AnnotatedElementHolder) { AnnotatedElementHolder metaData = (AnnotatedElementHolder) setter; visitElement(metaData.getElement()); } else if (REFLECTED_FIELD_BINDING_CLASS.equals(setter.getClass().getName())) { visitFieldBinding(setter); } else if (REFLECTED_METHOD_BINDING_CLASS.equals(setter.getClass().getName())) { visitMethodBinding(setter); } } private void visitFieldBinding(Object fieldBinding) throws Exception { Field field = (Field) accessibleField(fieldBinding.getClass(), REFLECTED_FIELD_BINDING_FIELD).get(fieldBinding); getOrCreateClass(field.getDeclaringClass()) .addField(field); IScope scope = (IScope) accessibleField(fieldBinding.getClass(), REFLECTED_BINDING_FIELD_SCOPE).get(fieldBinding); Object scopeValue = scope.get(); if (scopeValue != null) { getOrCreateClass(scopeValue.getClass()); } } private void visitMethodBinding(Object methodBinding) throws Exception { Method method = (Method) accessibleField(methodBinding.getClass(), REFLECTED_METHOD_BINDING_METHOD).get(methodBinding); ReflectedClass cls = getOrCreateClass(method.getDeclaringClass()); cls.addMethod(method); } private void visitProxyMethodBinding(Object methodBinding) throws Exception { Method method = (Method) accessibleField(methodBinding.getClass(), REFLECTED_METHOD_BINDING_METHOD).get(methodBinding); ReflectedClass cls = getOrCreateClass(method.getDeclaringClass()); cls.addMethod(method); } private static Field accessibleField(Class cls, String fieldName) throws NoSuchFieldException { Field field = cls.getDeclaredField(fieldName); field.setAccessible(true); return field; } ReflectedClass getOrCreateClass(Class cls) { if (cls.isPrimitive() || (cls.isInterface() && cls.getName().startsWith("java"))) { return new ReflectedClass(cls); // don't store } return getOrCreateClassByName(cls); } private ReflectedClass getOrCreateClassByName(Class cls) { return getOrCreateClassByName(cls.getName()); } private ReflectedClass getOrCreateClassByName(String name) { ReflectedClass result = visited.get(name); if (result == null) { result = new ReflectedClass(name); if (!excluded(name)) { visited.put(name, result); } } return result; } static boolean excluded(String fqcn) { if (excluded.contains(fqcn)) { return true; } String[] excludes = System.getProperty(SYSPROP_CODEGEN_EXCLUDES, "").split(","); for (String regex : excludes) { if (fqcn.matches(regex)) { System.err.printf("Class %s is excluded: (%s=%s)%n", fqcn, SYSPROP_CODEGEN_EXCLUDES, System.getProperty(SYSPROP_CODEGEN_EXCLUDES)); return true; } } return false; } } static class ReflectedClass { private final String name; private final Set fields = new TreeSet(); private final Set methods = new TreeSet(); ReflectedClass(Class cls) { this(cls.getName()); } ReflectedClass(String name) { this.name = name; } private boolean isFinal(Field f) { return (f.getModifiers() & Modifier.FINAL) == Modifier.FINAL; } ReflectedClass addField(Field field) { return addField(field.getName(), isFinal(field)); } ReflectedClass addField(String fieldName, boolean isFinal) { fields.add(new ReflectedField(fieldName, isFinal)); return this; } ReflectedClass addMethod0(String methodName, String... paramTypes) { methods.add(new ReflectedMethod(methodName, paramTypes)); return this; } ReflectedClass addMethod(Method method) { return addMethod(method.getName(), method.getParameterTypes()); } ReflectedClass addMethod(String methodName, Class... paramClasses) { String[] paramTypes = new String[paramClasses.length]; for (int i = 0; i < paramClasses.length; i++) { paramTypes[i] = paramClasses[i].getName(); } return addMethod0(methodName, paramTypes); } public ReflectedClass addMethod(String methodName, List paramTypeNames) { return addMethod0(methodName, paramTypeNames.toArray(new String[0])); } @Override public String toString() { String result = String.format("" + " {%n" + " \"name\" : \"%s\",%n" + " \"allDeclaredConstructors\" : true,%n" + " \"allPublicConstructors\" : true,%n" + " \"allDeclaredMethods\" : true,%n" + " \"allPublicMethods\" : true", name); if (!fields.isEmpty()) { result += String.format(",%n \"fields\" : "); String prefix = String.format("[%n"); // start JSON array for (ReflectedField field : fields) { result += String.format("%s %s", prefix, field); prefix = String.format(",%n"); } result += String.format("%n ]"); // end JSON array } if (!methods.isEmpty()) { result += String.format(",%n \"methods\" : "); String prefix = String.format("[%n"); // start JSON array for (ReflectedMethod method : methods) { result += String.format("%s %s", prefix, method); prefix = String.format(",%n"); } result += String.format("%n ]"); // end JSON array } result += String.format("%n }"); return result; } } static class ReflectedMethod implements Comparable { private final String name; private final String[] paramTypes; ReflectedMethod(String name, String... paramTypes) { this.name = name; this.paramTypes = paramTypes.clone(); } @Override public int hashCode() { return name.hashCode() * Arrays.hashCode(paramTypes); } @Override public boolean equals(Object o) { return o instanceof ReflectedMethod && ((ReflectedMethod) o).name.equals(name) && Arrays.equals(((ReflectedMethod) o).paramTypes, paramTypes); } @Override public String toString() { return String.format("{ \"name\" : \"%s\", \"parameterTypes\" : [%s] }", name, formatParamTypes()); } private String formatParamTypes() { StringBuilder result = new StringBuilder(); for (String type : paramTypes) { if (result.length() > 0) { result.append(", "); } result.append('"').append(type).append('"'); } return result.toString(); } public int compareTo(ReflectedMethod o) { int result = name.compareTo(o.name); if (result == 0) { result = Arrays.toString(this.paramTypes).compareTo(Arrays.toString(o.paramTypes)); } return result; } } static class ReflectedField implements Comparable { private final String name; private final boolean isFinal; ReflectedField(String name, boolean isFinal) { this.name = name; this.isFinal = isFinal; } @Override public int hashCode() { return name.hashCode(); } @Override public boolean equals(Object o) { return o instanceof ReflectedField && ((ReflectedField) o).name.equals(name); } @Override public String toString() { return isFinal ? String.format("{ \"name\" : \"%s\", \"allowWrite\" : true }", name) : String.format("{ \"name\" : \"%s\" }", name); } public int compareTo(ReflectedField o) { return name.compareTo(o.name); } } }