Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support binding options and parameters without reflection #1003

Open
jameskleeh opened this issue Apr 22, 2020 · 5 comments
Open

Support binding options and parameters without reflection #1003

jameskleeh opened this issue Apr 22, 2020 · 5 comments
Labels
theme: annotation-proc An issue or change related to the annotation processor theme: build An issue or change related to the build system theme: codegen An issue or change related to the picocli-codegen module

Comments

@jameskleeh
Copy link

Currently something like the following requires special notation to work with GraalVM:

@CommandLine.Command(name = "create-controller", description = "Creates a controller and associated test")
public class CreateControllerCommand extends CodeGenCommand {

    @CommandLine.Parameters(paramLabel = "CONTROLLER-NAME", description = "The name of the controller to create")
    String controllerName;

The controllerName field is bound via reflection. There should be a hook to allow for the user to control how the property is bound to avoid reflection.

@remkop
Copy link
Owner

remkop commented Apr 22, 2020

Hi James, thanks for your question!

Yes, this facility already exists in picocli's programmatic API, see the Programmatic API and Bindings part of my answer below, but I do wonder why you need it.

Annotations and reflection in picocli

The example you gave uses annotations. When an application uses picocli's annotations framework, picocli uses reflection in multiple places:

  • first to read these annotations when constructing a CommandSpec model,
  • then for some built-in type converters for types that are not available in Java 5 (like java.nio.file.Path, the java.time classes and some java.sql classes),
  • and finally to apply values that were matched on the command line to @Option- and @Parameters-annotated fields and methods.

You mention this last usage of reflection in picocli but be aware this is not the only usage for applications that use the annotations API.

Current Solution

The solution that picocli offers out of the box for creating native executables with GraalVM (as I am sure you are aware) is to generate a reflect-config.json file. (The picocli-codegen module contains a ReflectionConfigGenerator class that can be invoked directly or invoked by picocli's annotation processor for this.)

Future Solutions

I am considering a different approach for processing annotations, without reflection, in a future release of picocli. The basic idea would be to generate code at compile time; I believe this is similar to the approach Micronaut takes. Some early thoughts on this topic are in this ticket. This is at the conceptual stage and would require quite a bit of work to design, implement, test and document.

Programmatic API

Applications can avoid reflection altogether by using picocli's programmatic API. This is a different programming model that does not use the annotations.

Bindings

And now we finally get to answer your question! :-)

The programmatic API manual has a section on bindings, but in a nutshell:

An option or positional parameter is represented in picocli with the ArgSpec class (superclass of OptionSpec and PositionalParamSpec). Every ArgSpec instance has customizable getter and setter bindings. These bindings are invoked when the picocli parser matches a value for that option or positional parameter on the command line.

Applications can provide custom IGetter and ISetter implementations, to get complete control over what happens when a value is matched.

You would use it as follows:

CommandSpec spec = CommandSpec.create();
spec.addPositional(PositionalParamSpec.builder()
        .paramLabel("CONTROLLER-NAME")
        .description("The name of the controller to create")
        .getter(() -> { return getCreateControllerCommandInstance().controllerName; })
        .setter((value) -> { getCreateControllerCommandInstance().controllerName = value; })
        .type(String.class).auxiliaryTypes(String.class) // for type conversion
        .build());

// assumes there is a getCreateControllerCommandInstance() method
// that returns your CreateControllerCommand instance

This example uses CommandSpec.create() to create an "empty" CommandSpec, and programmatically/manually add options and positional parameters to it. This CommandSpec object can then be used to construct a CommandLine instance, on which the application can then call parseArgs or execute.

If the CommandLine constructor is called with a CommandSpec instance, it will not use reflection to create a model, but just use the specified CommandSpec.

By contrast, if the CommandLine constructor is called with any other object, it will use reflection to contruct a CommandSpec instance. (If you want to use reflection to contruct a CommandSpec instance from an annotated class via the programmatic API, then pass that annotated class or an instance of it to CommandSpec::forAnnotatedObject.)

@remkop
Copy link
Owner

remkop commented Apr 23, 2020

@jameskleeh Did this answer your question?

@jameskleeh
Copy link
Author

@remkop Yes - I appreciate the detailed response! Going to consider the programmatic approach

@remkop
Copy link
Owner

remkop commented Apr 23, 2020

Ok, let me know if you run into any snags or have questions. The programmatic API docs are not as detailed as the main user manual; let me know if it is missing stuff.

@remkop
Copy link
Owner

remkop commented Apr 23, 2020

Wild idea: depending on how many commands and options your application has, it may be just as easy to generate the source code. Building such a code generator is on the picocli roadmap: see #539 (also related: #750).

Picocli can already build a CommandSpec from the annotations at compile time.

What is missing is to generate the source code (using the programmatic API) from this CommandSpec that would build the same CommandSpec again at runtime without reflection.

So, basically, given an annotated class:

@CommandLine.Command(name = "create-controller", description = "Creates a controller and associated test")
public class CreateControllerCommand extends CodeGenCommand {

    @CommandLine.Parameters(paramLabel = "CONTROLLER-NAME", description = "The name of the controller to create")
    String controllerName;

Generate something like this at compile time:

CommandSpec spec = CommandSpec.create();
spec.name("create-controller").description("Creates a controller and associated test");
spec.addPositional(PositionalParamSpec.builder()
        .paramLabel("CONTROLLER-NAME")
        .description("The name of the controller to create")
        .getter(() -> { return getCreateControllerCommandInstance().controllerName; })
        .setter((value) -> { getCreateControllerCommandInstance().controllerName = value; })
        .type(String.class).auxiliaryTypes(String.class) // for type conversion
        .build());

The Micronaut team may have more experience in this than me. Would you be interested in collaborating on #539?

@remkop remkop added theme: annotation-proc An issue or change related to the annotation processor theme: build An issue or change related to the build system theme: codegen An issue or change related to the picocli-codegen module labels Feb 15, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
theme: annotation-proc An issue or change related to the annotation processor theme: build An issue or change related to the build system theme: codegen An issue or change related to the picocli-codegen module
Projects
None yet
Development

No branches or pull requests

2 participants