rvesse / airline

Java annotation-based framework for parsing Git like command line structures with deep extensibility

Home Page:https://rvesse.github.io/airline/

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

...llowedValuesRestrictionFactory not found

usergoodvery opened this issue · comments

Hi
Just started toying with this in an android project. I setup a basic skeleton, using your examples. I seem to get java.util.ServiceConfigurationError: com.github.rvesse.airline.restrictions.factories.OptionRestrictionFactory: Provider com.github.rvesse.airline.restrictions.factories.AllowedValuesRestrictionFactory not found

during: new com.github.rvesse.airline.Cli<>(CliParser.class);

My class is very simple:

@Cli(name = "basic",
        description = "Provides a basic example CLI",
        defaultCommand = CliACommand.class,
        commands = { CliACommand.class, CliGroupCommand.class })
public class CliParser {
  private static CliParser instance = null;
  com.github.rvesse.airline.Cli<CliRunnable> cli;

  public static CliParser getInstance ()
  {
    if (instance != null) {
      return instance;
    } else {
      instance = new CliParser();
      instance.cli = new com.github.rvesse.airline.Cli<>(CliParser.class);
      return instance;
    }

  }

  public int parseCli (String args) {
    CliRunnable cmd = instance.cli.parse(args);
    return cmd.run();
  }

and I have defined the one of the sample commands as follows:

@Command(name = "msg", description = "We're just getting started")
public class CliACommand implements CliRunnable {
  
  @Option(name = { "-f", "--flag" }, description = "An option that requires no values")
  private boolean flag = false;
  @Arguments(description = "Additional arguments")
  private List<String> args;

  @Override
  public int run()  {
    Log.d(TAG, String.format("CliCommand: '%s'",  Stream.of(args).collect(Collectors.joining(" " ))));

    return 0;
  }
}

I've never used this library in an Android environment so cannot guarantee that it will work there. The Android JDK does have some odd quirks compared to a normal JDK.

Providing a complete stack trace of the error would help in narrowing down where the problem lies.

I notice that the Android docs for ServiceLoader states the following:

Added in API Level 9

And from https://developer.android.com/guide/topics/manifest/uses-sdk-element.html#ApiLevels Level 9 is the Gingerbread release i.e. Android 2.3-2.3.2 so if you are targeting too old a version of Android this would be unsupported.

The docs also appear to say that if you don't explicitly state a target version it will assume Level 1 so you may simply need to update your Android project to declare a minimum SDK Level per https://developer.android.com/guide/topics/manifest/uses-sdk-element.html#apilevel

Thanks for looking into this.
I am doing this in an otherwise fully functional app with minSdkVersion 19
and targetSdkVersion 26. That puts me at Android 4.4 as a baseline

The crash happens as I try to initialise the parser at
com.xx.cli.CliParserRegistry.getInstance(CliParserRegistry.java:22)
which is

instance.cli = new com.github.rvesse.airline.Cli<>(CliParser.class);

Full trace below:

 java.util.ServiceConfigurationError: com.github.rvesse.airline.restrictions.factories.OptionRestrictionFactory: Provider com.github.rvesse.airline.restrictions.factories.AllowedValuesRestrictionFactory not found
        at java.util.ServiceLoader.fail(ServiceLoader.java:233)
        at java.util.ServiceLoader.access$100(ServiceLoader.java:183)
        at java.util.ServiceLoader$LazyIterator.nextService(ServiceLoader.java:373)
        at java.util.ServiceLoader$LazyIterator.next(ServiceLoader.java:416)
        at java.util.ServiceLoader$1.next(ServiceLoader.java:494)
        at com.github.rvesse.airline.restrictions.factories.RestrictionRegistry.init(RestrictionRegistry.java:54)
        at com.github.rvesse.airline.restrictions.factories.RestrictionRegistry.<clinit>(RestrictionRegistry.java:40)
        at com.github.rvesse.airline.restrictions.factories.RestrictionRegistry.getArgumentsRestrictionAnnotationClasses(RestrictionRegistry.java:113)
        at com.github.rvesse.airline.model.MetadataLoader.loadInjectionMetadata(MetadataLoader.java:757)
        at com.github.rvesse.airline.model.MetadataLoader.loadInjectionMetadata(MetadataLoader.java:559)
        at com.github.rvesse.airline.model.MetadataLoader.loadCommand(MetadataLoader.java:477)
        at com.github.rvesse.airline.model.MetadataLoader.loadGlobal(MetadataLoader.java:204)
        at com.github.rvesse.airline.model.MetadataLoader.loadGlobal(MetadataLoader.java:170)
        at com.github.rvesse.airline.Cli.<init>(Cli.java:63)
        at com.xx.cli.CliParserRegistry.getInstance(CliParserRegistry.java:22)
        at org.xx.getMessage(Activity.java:2398)

That’s a rather strange stack trace. The classes being referenced are present in the same JAR file as the rest of Airline so not sure why it is unable to resolve the class. I would guess this is some kind of packaging related bug specific to Android but as I don’t do any Android dev I’m not sure what else to suggest

Actually you just gave me a useful hint.. I'll fiddle about with the proguard settings, as my current packaging settings are a bit on the aggressive side

@usergoodvery Yes if you're using something like ProGuard to remove unreferenced classes then anything dynamically loaded via ServiceLoader is likely going to be removed because it isn't explicitly referenced anywhere.

If that is the case I would suggest just keeping the entire com.github.rvesse.airline.restrictions package and it's sub packages

@usergoodvery Did changing the ProGuard settings resolve the issue, if so I'll add some text to the docs calling this out

yes I just (lazily) added:
-keep class com.github.rvesse.airline.** { *; }
to a proguard file and that resolved the issue. Above not best practice, as it is better to be bit more specific about classes to keep, but I needed to get moving...

it seems the parser is not recognising commands, and would only invoke the command annotated as "default". For example, when I pass 'zip it', I get the error below:

D/CliParserRegistry: Error 1: Command 'zip it' not recognized D/CliParserRegistry: Error 2: Found unexpected parameters: [zip it]

The "zip" command is given as per:

public class CliZipCommand implements CliRunnable {
  static private final String TAG = CliZipCommand.class.getSimpleName();

  @Option(name = { "-f", "--flag" }, description = "An option that requires no values")
  private boolean flag = false;

  @Arguments(description = "Additional arguments")
  private List<String> args;

  @Override
  public int run()  {
    Log.d(TAG, String.format("CliCommand ZIP (flag:'%s': '%s'", this.flag ? "set" : "not set", Stream.of(args).collect(Collectors.joining(" " ))));
    return 0;
  }
}

And the parser:

@Cli(name = "basic",
        description = "Provides a basic example CLI",
        defaultCommand = CliACommand.class,
        commands = { CliACommand.class, CliZipCommand.class })
public class CliParser {
  private static CliParser instance = null;
  com.github.rvesse.airline.Cli<CliRunnable> cli;

  public static CliParser getInstance ()
  {
    if (instance != null) {
      return instance;
    } else {
      instance = new CliParser();
      instance.cli = new com.github.rvesse.airline.Cli<>(CliParser.class);
      return instance;
    }

  }

  public int parseCli (String args) {
    CliRunnable cmd = instance.cli.parse(args);
    return cmd.run();
  }

The CliZipCommand class should have an appropriate @Command annotation which should cause Airline to fail completely if that isn't the case so maybe you just omitted it in your sample?

I inadvertently left that out from the copy&paste I definitely have the annotation on all commands:
@Command(name = CliParserRegistry.CLI_ZIP, description = "Zip")
In this instance it is a static string, but I also had literal string as well.

It seems commands are being processed and the parser invokes.
As you can see I setup the parser as singleton, but I don't think that should matter.
I am using this inside android, not as standalone java program, so when I invoke the parser I go:

CliParserRegistry.getInstance(context).parseCli ("zip this");

Expecting "this" to be passed to my parser as arg.

Okay, I see what's wrong, the parse() method of Cli expects to get an iterable/array of arguments but you pass in a single string so where the parser expects to get zip it instead receives zip this which it doesn't recognise as a command

That's interesting, I don't recall seeing a compiler error or warning.. I'll have to check the method's signature, as it seemed content with the single string... So what you are suggesting is break up the line in two parts, one part for the command name and another part for the rest of arguments?

Well it's a vargs method i.e. parse(String... args) so passing a single string is a valid call. The problem is that Airline assumes that at the point you call parse() basic parsing into arguments has happened (typically at the JVM level) so it treats each string passed as a single argument (which will potentially be further parsed and interpreted depending on your OptionParser config).

In your case it's trying to identify the command to invoke and there is no zip this command so it bails out