Skip to content

Support @Spec-annotated members in ArgGroup classes (was: Question: Throwing a ParameterException inside an ArgGroup) #1260

@jhemelhof

Description

@jhemelhof

For our application we make heavy use of the ArgGroup feature, which works really well. Due to the amount of parameters present in said groups these classes are stored in separate files, rather than inside the command class where they are used.

One such parameter is a String where we want to limit the possible options. As these depend on the location where the application is deployed & its configuration we cannot use an enum and the build-in validation for that type offered by picocli. We therefore have to perform some manual validation, as in following (stripped-down) example:

import picocli.CommandLine;

import java.util.Arrays;
import java.util.List;

public class CriteriaWithEnvironment {

    private static final List<String> DYNAMIC_LIST = Arrays.asList("FOO", "BAR");

    private String environment;

    @CommandLine.Option(names = {"-e", "--environment"})
    public void setEnvironment(String environment) {
        if (!DYNAMIC_LIST.contains(environment)) {
            // Should throw a ParameterException
            throw new IllegalArgumentException("Should be one of...");
        }
        this.environment = environment;
    }

    public String getEnvironment() {
        return environment;
    }
}

Ideally we want to throw a ParameterException, however for such an exception we need access to the CommandLine object of the command. Injecting the CommandSpec in the ArgGroup sadly leads to NPE's:

  • @CommandLine.Spec CommandLine.Model.CommandSpec commandSpec;
    Application does not launch at all, printing following stacktrace:
Exception in thread "main" picocli.CommandLine$InitializationException: Could not inject spec
	at picocli.CommandLine$Model$CommandReflection.initFromAnnotatedTypedMembers(CommandLine.java:10880)
	at picocli.CommandLine$Model$CommandReflection.initFromAnnotatedFields(CommandLine.java:10818)
	at picocli.CommandLine$Model$CommandReflection.extractArgGroupSpec(CommandLine.java:10696)
	at picocli.CommandLine$Model$CommandReflection.buildArgGroupForMember(CommandLine.java:10969)
	at picocli.CommandLine$Model$CommandReflection.initFromAnnotatedTypedMembers(CommandLine.java:10854)
	at picocli.CommandLine$Model$CommandReflection.initFromAnnotatedFields(CommandLine.java:10818)
	at picocli.CommandLine$Model$CommandReflection.extractCommandSpec(CommandLine.java:10749)
	at picocli.CommandLine$Model$CommandSpec.forAnnotatedObject(CommandLine.java:5879)
	at picocli.CommandLine.<init>(CommandLine.java:223)
	at picocli.CommandLine.<init>(CommandLine.java:196)
	at Search.main(Search.java:25)
Caused by: picocli.CommandLine$PicocliException: Could not set value for field picocli.CommandLine$Model$CommandSpec CriteriaWithEnvironment.commandSpec to command 'search' (user object: Search@28ac3dc3)
	at picocli.CommandLine$Model$FieldBinding.set(CommandLine.java:11015)
	... 11 more
Caused by: java.lang.NullPointerException
	at java.base/jdk.internal.reflect.UnsafeFieldAccessorImpl.ensureObj(UnsafeFieldAccessorImpl.java:57)
	at java.base/jdk.internal.reflect.UnsafeObjectFieldAccessorImpl.get(UnsafeObjectFieldAccessorImpl.java:36)
	at java.base/java.lang.reflect.Field.get(Field.java:418)
	at picocli.CommandLine$Model$FieldBinding.set(CommandLine.java:11011)
	... 11 more
  • @CommandLine.Spec(CommandLine.Spec.Target.MIXEE) CommandLine.Model.CommandSpec commandSpec;
    Application launches, but field remains null. As the ArgGroup isn't injected in the command as a Mixee this makes sense but it was worth a shot.

The documentation does not mention that it is possible to inject a Spec inside an ArgGroup so I wonder if it's supported in any way? If not, what would be the preferred way of throwing a ParameterException inside an ArgGroup?

Our current implementation lets the ArgGroup define a validate() function, which verifies the arguments inside the ArgGroup and throws an IllegalArgumentException. Before the actual command logic is then run we call said function, and map the IllegalArgumentException to a ParameterException. This is however rather convoluted, and we'd prefer to perform the actual validation where it makes the most sense (i.e. when & where the parameter is set).

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions