diff --git a/gradle.properties b/gradle.properties index f20b19757..46ea4a99f 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,5 @@ asciidoctorGradlePluginVersion = 1.5.3 +compileTestingVersion = 0.15 gradleBintrayPluginVersion = 1.+ groovyVersion = 2.4.10 hamcrestCoreVersion = 1.3 @@ -14,7 +15,7 @@ junitVersion = 4.12 projectPreviousReleaseVersion = 3\\.9\\.2 # projectPreviousVersionRegex may be a SNAPSHOT projectPreviousVersionRegex = 3\\.9\\.2 -projectVersion = 3.9.3-SNAPSHOT +projectVersion = 4.0.0-alpha-1-SNAPSHOT releaseDate = 2019-01-20 releaseDatePreviousRegex = 2019\\-01\\-20 diff --git a/logging.properties b/logging.properties new file mode 100644 index 000000000..b570f767f --- /dev/null +++ b/logging.properties @@ -0,0 +1,59 @@ +############################################################ +# Default Logging Configuration File +# +# You can use a different file by specifying a filename +# with the java.util.logging.config.file system property. +# For example java -Djava.util.logging.config.file=myfile +############################################################ + +############################################################ +# Global properties +############################################################ + +# "handlers" specifies a comma separated list of log Handler +# classes. These handlers will be installed during VM startup. +# Note that these classes must be on the system classpath. +# By default we only configure a ConsoleHandler, which will only +# show messages at the INFO and above levels. +handlers= java.util.logging.ConsoleHandler + +# To also add the FileHandler, use the following line instead. +#handlers= java.util.logging.FileHandler, java.util.logging.ConsoleHandler + +# Default global logging level. +# This specifies which kinds of events are logged across +# all loggers. For any given facility this global level +# can be overriden by a facility specific level +# Note that the ConsoleHandler also has a separate level +# setting to limit messages printed to the console. +.level= FINE + +############################################################ +# Handler specific properties. +# Describes specific configuration info for Handlers. +############################################################ + +# default file output is in user's home directory. +java.util.logging.FileHandler.pattern = %h/java%u.log +java.util.logging.FileHandler.limit = 50000 +java.util.logging.FileHandler.count = 1 +java.util.logging.FileHandler.formatter = java.util.logging.XMLFormatter + +# Limit the message that are printed on the console to INFO and above. +java.util.logging.ConsoleHandler.level = FINE +java.util.logging.ConsoleHandler.formatter = java.util.logging.SimpleFormatter + +# Example to customize the SimpleFormatter output format +# to print one-line log message like this: +# : [] +# +# java.util.logging.SimpleFormatter.format=%4$s: %5$s [%1$tc]%n + +############################################################ +# Facility specific properties. +# Provides extra control for each logger. +############################################################ + +# For example, set the com.xyz.foo logger to only log SEVERE +# messages: +com.xyz.foo.level = SEVERE diff --git a/picocli-annotation-processing-tests/build.gradle b/picocli-annotation-processing-tests/build.gradle new file mode 100644 index 000000000..b57b80f2e --- /dev/null +++ b/picocli-annotation-processing-tests/build.gradle @@ -0,0 +1,28 @@ +plugins { + id 'java' +} + +group 'info.picocli' +description 'Picocli Annotation Processing Tests - Tests Annotation Processors for picocli Annotations.' +version "$projectVersion" +sourceCompatibility = 1.8 +targetCompatibility = 1.8 + +dependencies { + compile rootProject + compile project(':picocli-codegen') + testCompile "junit:junit:$junitVersion", + "com.google.testing.compile:compile-testing:$compileTestingVersion", + files(org.gradle.internal.jvm.Jvm.current().getToolsJar()) // workaround https://github.com/google/compile-testing/issues/102 (and #28) +} +jar { + manifest { + attributes 'Specification-Title': 'Picocli Annotation Processing Tests', + 'Specification-Vendor' : 'Remko Popma', + 'Specification-Version' : version, + 'Implementation-Title' : 'Picocli Annotation Processing Tests', + 'Implementation-Vendor' : 'Remko Popma', + 'Implementation-Version': version, + 'Automatic-Module-Name' : 'info.picocli.annotation.processing.tests' + } +} diff --git a/picocli-annotation-processing-tests/src/main/java/picocli/annotation/processing/tests/CommandSpec2YamlProcessor.java b/picocli-annotation-processing-tests/src/main/java/picocli/annotation/processing/tests/CommandSpec2YamlProcessor.java new file mode 100644 index 000000000..30e2d16a2 --- /dev/null +++ b/picocli-annotation-processing-tests/src/main/java/picocli/annotation/processing/tests/CommandSpec2YamlProcessor.java @@ -0,0 +1,36 @@ +package picocli.annotation.processing.tests; + +import picocli.CommandLine.Model.CommandSpec; +import picocli.codegen.annotation.processing.AbstractCommandSpecProcessor; + +import javax.annotation.processing.RoundEnvironment; +import javax.lang.model.element.Element; +import javax.lang.model.element.TypeElement; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Set; + + +public class CommandSpec2YamlProcessor extends AbstractCommandSpecProcessor { + + public List strings = new ArrayList(); + public Map commands; + + @Override + protected boolean handleCommands(Map commands, + Set annotations, + RoundEnvironment roundEnv) { + System.out.println(commands); + this.commands = commands; + CommandSpecYamlPrinter printer = new CommandSpecYamlPrinter(); + for (Map.Entry entry : commands.entrySet()) { + StringWriter sw = new StringWriter(); + printer.print(entry.getValue(), new PrintWriter(sw)); + strings.add(sw.toString()); + } + return false; + } +} diff --git a/picocli-annotation-processing-tests/src/main/java/picocli/annotation/processing/tests/CommandSpecYamlPrinter.java b/picocli-annotation-processing-tests/src/main/java/picocli/annotation/processing/tests/CommandSpecYamlPrinter.java new file mode 100644 index 000000000..e377bf92c --- /dev/null +++ b/picocli-annotation-processing-tests/src/main/java/picocli/annotation/processing/tests/CommandSpecYamlPrinter.java @@ -0,0 +1,234 @@ +package picocli.annotation.processing.tests; + +import picocli.CommandLine; +import picocli.CommandLine.Command; +import picocli.CommandLine.Model.ArgSpec; +import picocli.CommandLine.Model.CommandSpec; +import picocli.CommandLine.Model.OptionSpec; +import picocli.CommandLine.Model.ParserSpec; +import picocli.CommandLine.Model.PositionalParamSpec; +import picocli.CommandLine.Model.UnmatchedArgsBinding; +import picocli.CommandLine.Model.UsageMessageSpec; +import picocli.CommandLine.Parameters; + +import java.io.PrintWriter; +import java.io.StringWriter; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.Enumeration; +import java.util.List; +import java.util.Map; +import java.util.ResourceBundle; + +/** + * Dumps a {@code CommandSpec} in YAML format. + */ +public class CommandSpecYamlPrinter { + + public static void main(String... args) { + CommandLine.run(new App(), args); + } + + static void print(Object userObject) { + print(CommandSpec.forAnnotatedObject(userObject)); + } + + static void print(CommandSpec spec) { + StringWriter sw = new StringWriter(); + new CommandSpecYamlPrinter().print(spec, new PrintWriter(sw)); + System.out.println(sw); + } + + public void print(CommandSpec spec, PrintWriter pw) { + pw.println("---"); + printCommandSpec(spec, "CommandSpec:", pw, " ", " "); + } + private void printCommandSpec(CommandSpec spec, String label, PrintWriter pw, + String initialIndent, String indent) { + pw.printf("%s%n", label); + pw.printf("%sname: '%s'%n", initialIndent, spec.name()); + pw.printf("%saliases: %s%n", indent, Arrays.toString(spec.aliases())); + pw.printf("%suserObject: %s%n", indent, spec.userObject()); + pw.printf("%shelpCommand: %s%n", indent, spec.helpCommand()); + pw.printf("%sdefaultValueProvider: %s%n", indent, spec.defaultValueProvider()); + pw.printf("%sversionProvider: %s%n", indent, spec.versionProvider()); + pw.printf("%sversion: %s%n", indent, Arrays.toString(spec.version())); + + List options = new ArrayList(spec.options()); + Collections.sort(options, new Comparator() { + public int compare(OptionSpec o1, OptionSpec o2) { + return o1.shortestName().compareTo(o2.shortestName()); + } + }); + printOptionList(options, pw, indent); + printPositionalList(spec.positionalParameters(), pw, indent); + printUnmatchedArgsBindingList(spec.unmatchedArgsBindings(), pw, indent); + printMixinList(spec.mixins(), pw, indent); + + printUsageMessage(spec.usageMessage(), pw, indent); + printParser(spec.parser(), pw, indent); + printResourceBundle(spec.resourceBundle(), pw, indent); + + printSubcommandList(spec.subcommands(), pw, indent); + } + + private void printResourceBundle(ResourceBundle resourceBundle, PrintWriter pw, String indent) { + if (resourceBundle == null) { + return; + } + pw.printf("%sResourceBundle:%n", indent); + indent += " "; + for (Enumeration keys = resourceBundle.getKeys(); keys.hasMoreElements();) { + String key = keys.nextElement(); + pw.printf("%s%s: '%s'%n", indent, key, resourceBundle.getString(key)); + } + } + + private void printParser(ParserSpec parser, PrintWriter pw, String indent) { + pw.printf("%sParserSpec:%n", indent); + indent += " "; + pw.printf("%sseparator: '%s'%n", indent, parser.separator()); + pw.printf("%sendOfOptionsDelimiter: '%s'%n", indent, parser.endOfOptionsDelimiter()); + pw.printf("%sexpandAtFiles: %s%n", indent, parser.expandAtFiles()); + pw.printf("%satFileCommentChar: '%s'%n", indent, parser.atFileCommentChar()); + pw.printf("%soverwrittenOptionsAllowed: %s%n", indent, parser.overwrittenOptionsAllowed()); + pw.printf("%sunmatchedArgumentsAllowed: %s%n", indent, parser.unmatchedArgumentsAllowed()); + pw.printf("%sunmatchedOptionsArePositionalParams: %s%n", indent, parser.unmatchedOptionsArePositionalParams()); + pw.printf("%sstopAtUnmatched: %s%n", indent, parser.stopAtUnmatched()); + pw.printf("%sstopAtPositional: %s%n", indent, parser.stopAtPositional()); + pw.printf("%sposixClusteredShortOptionsAllowed: %s%n", indent, parser.posixClusteredShortOptionsAllowed()); + pw.printf("%saritySatisfiedByAttachedOptionParam: %s%n", indent, parser.aritySatisfiedByAttachedOptionParam()); + pw.printf("%scaseInsensitiveEnumValuesAllowed: %s%n", indent, parser.caseInsensitiveEnumValuesAllowed()); + pw.printf("%scollectErrors: %s%n", indent, parser.collectErrors()); + pw.printf("%slimitSplit: %s%n", indent, parser.limitSplit()); + pw.printf("%stoggleBooleanFlags: %s%n", indent, parser.toggleBooleanFlags()); + } + + private void printUsageMessage(UsageMessageSpec usageMessage, PrintWriter pw, String indent) { + pw.printf("%sUsageMessageSpec:%n", indent); + indent += " "; + pw.printf("%swidth: %s%n", indent, usageMessage.width()); + pw.printf("%sabbreviateSynopsis: %s%n", indent, usageMessage.abbreviateSynopsis()); + pw.printf("%shidden: %s%n", indent, usageMessage.hidden()); + pw.printf("%sshowDefaultValues: %s%n", indent, usageMessage.showDefaultValues()); + pw.printf("%ssortOptions: %s%n", indent, usageMessage.sortOptions()); + pw.printf("%srequiredOptionMarker: '%s'%n", indent, usageMessage.requiredOptionMarker()); + pw.printf("%sheaderHeading: '%s'%n", indent, usageMessage.headerHeading()); + pw.printf("%sheader: %s%n", indent, Arrays.toString(usageMessage.header())); + pw.printf("%ssynopsisHeading: '%s'%n", indent, usageMessage.synopsisHeading()); + pw.printf("%scustomSynopsis: %s%n", indent, Arrays.toString(usageMessage.customSynopsis())); + pw.printf("%sdescriptionHeading: '%s'%n", indent, usageMessage.descriptionHeading()); + pw.printf("%sdescription: %s%n", indent, Arrays.toString(usageMessage.description())); + pw.printf("%sparameterListHeading: '%s'%n", indent, usageMessage.parameterListHeading()); + pw.printf("%soptionListHeading: '%s'%n", indent, usageMessage.optionListHeading()); + pw.printf("%scommandListHeading: '%s'%n", indent, usageMessage.commandListHeading()); + pw.printf("%sfooterHeading: '%s'%n", indent, usageMessage.footerHeading()); + pw.printf("%sfooter: %s%n", indent, Arrays.toString(usageMessage.footer())); + } + + + private void printUnmatchedArgsBindingList(List unmatchedArgsBindings, PrintWriter pw, String indent) { + pw.printf("%sUnmatchedArgsBindings:", indent); + pw.println(unmatchedArgsBindings.isEmpty() ? " []" : ""); + for (UnmatchedArgsBinding unmatched : unmatchedArgsBindings) { + pw.printf("%sgetter: %s%n", indent + "- ", unmatched.getter()); + pw.printf("%ssetter: %s%n", indent + " ", unmatched.setter()); + } + } + + private void printMixinList(Map mixins, PrintWriter pw, String indent) { + pw.printf("%sMixins:", indent); + pw.println(mixins.isEmpty() ? " []" : ""); + for (Map.Entry entry : mixins.entrySet()) { + printCommandSpec(entry.getValue(), indent + "# " + entry.getKey(), pw, indent + "- ", indent + " "); + } + } + + private void printSubcommandList(Map subcommands, PrintWriter pw, String indent) { + pw.printf("%sSubcommands:", indent); + pw.println(subcommands.isEmpty() ? " []" : ""); + for (Map.Entry entry : subcommands.entrySet()) { + printCommandSpec(entry.getValue().getCommandSpec(), + indent + "# " + entry.getKey(), pw, indent + "- ", indent + " "); + } + } + + private void printOptionList(List options, PrintWriter pw, String indent) { + pw.printf("%sOptions:", indent); + pw.println(options.isEmpty() ? " []" : ""); + for (OptionSpec option : options) { + printOption(option, pw, indent); + } + } + private void printOption(OptionSpec option, PrintWriter pw, String indent) { + pw.printf("%snames: %s%n", indent + "- ", Arrays.toString(option.names())); + indent += " "; + pw.printf("%susageHelp: %s%n", indent, option.usageHelp()); + pw.printf("%sversionHelp: %s%n", indent, option.versionHelp()); + printArg(option, pw, indent); + } + + private void printPositionalList(List positionals, PrintWriter pw, String indent) { + pw.printf("%sPositionalParams:", indent); + pw.println(positionals.isEmpty() ? " []" : ""); + for (PositionalParamSpec positional : positionals) { + printPositional(positional, pw, indent); + } + } + private void printPositional(PositionalParamSpec positional, PrintWriter pw, String indent) { + pw.printf("%sindex: %s%n", indent + "- ", positional.index()); + indent += " "; + printArg(positional, pw, indent); + } + private void printArg(ArgSpec arg, PrintWriter pw, String indent) { + pw.printf("%sdescription: %s%n", indent, Arrays.toString(arg.description())); + pw.printf("%sdescriptionKey: '%s'%n", indent, arg.descriptionKey()); + pw.printf("%stypeInfo: %s%n", indent, arg.typeInfo()); + pw.printf("%sarity: %s%n", indent, arg.arity()); + pw.printf("%ssplitRegex: '%s'%n", indent, arg.splitRegex()); + pw.printf("%sinteractive: %s%n", indent, arg.interactive()); + pw.printf("%srequired: %s%n", indent, arg.required()); + pw.printf("%shidden: %s%n", indent, arg.hidden()); + pw.printf("%shideParamSyntax: %s%n", indent, arg.hideParamSyntax()); + pw.printf("%sdefaultValue: '%s'%n", indent, arg.defaultValue()); + pw.printf("%sshowDefaultValue: %s%n", indent, arg.showDefaultValue()); + pw.printf("%shasInitialValue: %s%n", indent, arg.hasInitialValue()); + pw.printf("%sinitialValue: '%s'%n", indent, arg.initialValue()); + pw.printf("%sparamLabel: '%s'%n", indent, arg.paramLabel()); + pw.printf("%sconverters: %s%n", indent, Arrays.toString(arg.converters())); + pw.printf("%scompletionCandidates: %s%n", indent, iter(arg.completionCandidates())); + pw.printf("%sgetter: %s%n", indent, arg.getter()); + pw.printf("%ssetter: %s%n", indent, arg.setter()); + } + + private String iter(Iterable iterable) { + if (iterable == null) { return "null"; } + StringBuilder sb = new StringBuilder(); + sb.append("["); + String sep = ""; + for (String str : iterable) { + sb.append(sep).append(str); + sep = ", "; + } + return sb.append("]").toString(); + } + + @Command(name = "CommandSpecYamlPrinter", mixinStandardHelpOptions = true, + description = "Prints details of a CommandSpec") + private static class App implements Runnable { + + @Parameters(arity = "1..*") + Class[] classes = new Class[0]; + + // @Override (requires Java 6) + public void run() { + for (Class cls : classes) { + StringWriter sw = new StringWriter(); + new CommandSpecYamlPrinter().print(CommandSpec.forAnnotatedObject(cls), new PrintWriter(sw)); + System.out.println(sw); + } + } + } +} diff --git a/picocli-annotation-processing-tests/src/main/resources/META-INF/services/javax.annotation.processing.Processor b/picocli-annotation-processing-tests/src/main/resources/META-INF/services/javax.annotation.processing.Processor new file mode 100644 index 000000000..635f8e981 --- /dev/null +++ b/picocli-annotation-processing-tests/src/main/resources/META-INF/services/javax.annotation.processing.Processor @@ -0,0 +1 @@ +picocli.annotation.processing.tests.CommandSpec2YamlProcessor diff --git a/picocli-annotation-processing-tests/src/test/java/picocli/annotation/processing/tests/AbstractCommandSpecProcessorTest.java b/picocli-annotation-processing-tests/src/test/java/picocli/annotation/processing/tests/AbstractCommandSpecProcessorTest.java new file mode 100644 index 000000000..7203d812d --- /dev/null +++ b/picocli-annotation-processing-tests/src/test/java/picocli/annotation/processing/tests/AbstractCommandSpecProcessorTest.java @@ -0,0 +1,269 @@ +package picocli.annotation.processing.tests; + +import com.google.common.collect.ImmutableList; +import com.google.testing.compile.Compilation; +import com.google.testing.compile.JavaFileObjects; +import org.hamcrest.MatcherAssert; +import org.junit.Assert; +import org.junit.Test; + +import javax.tools.Diagnostic; +import javax.tools.JavaFileObject; +import java.util.Arrays; +import java.util.List; +import java.util.Locale; +import java.util.Scanner; +import java.util.Set; +import java.util.TreeSet; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import static com.google.testing.compile.CompilationSubject.assertThat; +import static com.google.testing.compile.Compiler.javac; +import static org.hamcrest.CoreMatchers.containsString; +import static org.hamcrest.CoreMatchers.startsWith; +import static org.junit.Assert.*; +import static picocli.codegen.util.Resources.slurp; +import static picocli.codegen.util.Resources.slurpAll; + +public class AbstractCommandSpecProcessorTest { + + @Test + public void testCommandWithMixin() { + Compilation compilation = compareCommandDump(slurp("/picocli/examples/mixin/CommandWithMixin.yaml"), + JavaFileObjects.forResource("picocli/examples/mixin/CommandWithMixin.java"), + JavaFileObjects.forResource("picocli/examples/mixin/CommonOption.java")); + + assertOnlySourceVersionWarning(compilation); + } + + @Test + public void testSubcommands() { + Compilation compilation = compareCommandDump(slurp("/picocli/examples/subcommands/FileUtils.yaml"), + JavaFileObjects.forResource("picocli/examples/subcommands/ParentCommandDemo.java")); + + assertOnlySourceVersionWarning(compilation); + } + + @Test + public void testSetterMethodOnClass() { + List expected = slurpAll("/picocli/examples/annotatedmethods/CPrimitives.yaml", + "/picocli/examples/annotatedmethods/CPrimitivesWithDefault.yaml", + "/picocli/examples/annotatedmethods/CObjectsWithDefaults.yaml", + "/picocli/examples/annotatedmethods/CObjects.yaml" + ); + + Compilation compilation = compareCommandDump(expected, + JavaFileObjects.forResource( + "picocli/examples/annotatedmethods/AnnotatedClassMethodOptions.java")); + + assertOnlySourceVersionWarning(compilation); + } + + @Test + public void testGetterMethodOnInterface() { + List expected = slurpAll("/picocli/examples/annotatedmethods/IFPrimitives.yaml", + "/picocli/examples/annotatedmethods/IFPrimitivesWithDefault.yaml", + "/picocli/examples/annotatedmethods/IFObjects.yaml", + "/picocli/examples/annotatedmethods/IFObjectsWithDefault.yaml"); + + Compilation compilation = compareCommandDump(expected, + JavaFileObjects.forResource( + "picocli/examples/annotatedmethods/AnnotatedInterfaceMethodOptions.java")); + + assertOnlySourceVersionWarning(compilation); + } + + @Test + public void testInvalidAnnotationsOnInterface() { + CommandSpec2YamlProcessor processor = new CommandSpec2YamlProcessor(); + Compilation compilation = + javac() + .withProcessors(processor) + .compile(JavaFileObjects.forResource( + "picocli/examples/annotatedmethods/InvalidAnnotatedInterfaceMethodOptions.java")); + + assertThat(compilation).failed(); + ImmutableList> errors = compilation.errors(); + assertEquals("expected error count", 3, errors.size()); + + for (Diagnostic diag : errors) { + MatcherAssert.assertThat(diag.getMessage(Locale.ENGLISH), + containsString("Invalid picocli annotation on interface field")); + } + //assertThat(compilation).hadErrorContaining("Invalid picocli annotation on interface field"); + } + + @Test + public void testInvalidAnnotationCombinations() { + CommandSpec2YamlProcessor processor = new CommandSpec2YamlProcessor(); + Compilation compilation = + javac() + .withProcessors(processor) + .compile(JavaFileObjects.forResource( + "picocli/examples/validation/Invalid.java")); + + assertThat(compilation).failed(); + + Set expected = new TreeSet<>(Arrays.asList( + "Subcommand is missing @Command annotation with a name attribute", + "Subcommand @Command annotation should have a name attribute", + "@Mixin must have a declared type, not int", + "invalidOptionAndMixin cannot have both @picocli.CommandLine.Mixin and @picocli.CommandLine.Option annotations", + "invalidParametersAndMixin cannot have both @picocli.CommandLine.Mixin and @picocli.CommandLine.Parameters annotations", + "invalidUnmatchedAndMixin cannot have both @picocli.CommandLine.Mixin and @picocli.CommandLine.Unmatched annotations", + "invalidSpecAndMixin cannot have both @picocli.CommandLine.Mixin and @picocli.CommandLine.Spec annotations", + "invalidOptionAndUnmatched cannot have both @picocli.CommandLine.Unmatched and @picocli.CommandLine.Option annotations", + "invalidParametersAndUnmatched cannot have both @picocli.CommandLine.Unmatched and @picocli.CommandLine.Parameters annotations", + "invalidOptionAndSpec cannot have both @picocli.CommandLine.Spec and @picocli.CommandLine.Option annotations", + "invalidParametersAndSpec cannot have both @picocli.CommandLine.Spec and @picocli.CommandLine.Parameters annotations", + "invalidUnmatchedAndSpec cannot have both @picocli.CommandLine.Spec and @picocli.CommandLine.Unmatched annotations", + "invalidOptionAndParameters cannot have both @picocli.CommandLine.Option and @picocli.CommandLine.Parameters annotations" + )); + ImmutableList> errors = compilation.errors(); + for (Diagnostic diag : errors) { + assertTrue("Unexpected error: " + diag.getMessage(Locale.ENGLISH), + expected.remove(diag.getMessage(Locale.ENGLISH))); + } + assertTrue("Expected errors: " + expected, expected.isEmpty()); + } + + @Test + public void testCommandWithBundle() { + Compilation compilation = compareCommandDump(slurp("/picocli/examples/messages/CommandWithBundle.yaml"), + JavaFileObjects.forResource("picocli/examples/messages/CommandWithBundle.java")); + + assertOnlySourceVersionWarning(compilation); + } + + private Compilation compareCommandDump(String expected, JavaFileObject ... sourceFiles) { + return compareCommandDump(Arrays.asList(expected), sourceFiles); + } + + private Compilation compareCommandDump(List expected, JavaFileObject ... sourceFiles) { + CommandSpec2YamlProcessor processor = new CommandSpec2YamlProcessor(); + Compilation compilation = + javac() + .withProcessors(processor) + .compile(sourceFiles); + + assertTrue("Expected at least " + expected.size() + " commands but found " + processor.strings.size(), + expected.size() <= processor.strings.size()); + for (int i = 0; i < expected.size(); i++) { + assertEqualCommand(expected.get(i), processor.strings.get(i)); + } + return compilation; + } + + private void assertOnlySourceVersionWarning(Compilation compilation) { + assertThat(compilation).hadWarningCount(1); + assertThat(compilation).hadWarningContaining("Supported source version 'RELEASE_6' from annotation processor 'picocli.annotation.processing.tests"); + } + + private void assertEqualCommand(String expected, String actual) { + List exceptions = Arrays.asList("userObject: ", "hasInitialValue: ", "initialValue: ", "# "); + Scanner expectedScanner = new Scanner(expected); + Scanner actualScanner = new Scanner(actual); + int count = 0; + NEXT_LINE: while (actualScanner.hasNextLine()) { + String actualLine = actualScanner.nextLine(); + assertTrue("Unexpected actual line: " + actualLine, expectedScanner.hasNextLine()); + String expectedLine = expectedScanner.nextLine(); + count++; + + String actLine = actualLine.trim(); + String expLine = expectedLine.trim(); + + for (String exception : exceptions) { + if (expLine.startsWith(exception) && actLine.startsWith(exception)) { + continue NEXT_LINE; + } + } + if (expLine.startsWith("Mixin: ") && actLine.startsWith("Mixin: ")) { + continue NEXT_LINE; + } + if (expLine.startsWith("paramLabel: '" + expected2.substring(pos); + } + assertEquals(expected2, actual); + } + + private void assertSimilarDefaultValueProvider(String expectedLine, String actualLine) { + if ("defaultValueProvider: null".equals(expectedLine.trim())) { + assertEquals("defaultValueProvider: DefaultValueProviderMetaData[default]", actualLine.trim()); + } else { + assertEquals("Not implemented yet", expectedLine, actualLine); + } + } + + private void assertSimilarVersionProvider(String expectedLine, String actualLine) { + if ("versionProvider: null".equals(expectedLine.trim())) { + assertEquals("versionProvider: VersionProviderMetaData[default]", actualLine.trim()); + } else { + assertEquals("Not implemented yet", expectedLine, actualLine); + } + } + + private void assertSimilarGetterSetter(String expectedLine, String actualLine) { + String expect = expectedLine.trim().substring(1); + String actual = actualLine.trim().substring(1); + if (expect.startsWith("etter: picocli.CommandLine.Model.FieldBinding")) { + Assert.assertThat(actual, startsWith( + "etter: picocli.codegen.annotation.processing.internal.GetterSetterMetaData(FIELD")); + } else if (expect.startsWith("etter: picocli.CommandLine.Model.MethodBinding")) { + Assert.assertThat(actual, startsWith( + "etter: picocli.codegen.annotation.processing.internal.GetterSetterMetaData(METHOD")); + } else if (expect.startsWith("etter: picocli.CommandLine$Model$PicocliInvocationHandler$ProxyBinding")) { + Assert.assertThat(actual, startsWith( + "etter: picocli.codegen.annotation.processing.internal.GetterSetterMetaData(METHOD")); + } else if (expect.startsWith("etter: picocli.CommandLine.Model.ObjectBinding")) { + Assert.assertThat(actual, startsWith( + "etter: picocli.codegen.annotation.processing.internal.GetterSetterMetaData(PARAMETER")); + } else { + assertEquals("Not implemented yet", expect, actual); + } + } +} \ No newline at end of file diff --git a/picocli-annotation-processing-tests/src/test/java/picocli/annotation/processing/tests/AnnotatedCommandSourceGeneratorProcessorTest.java b/picocli-annotation-processing-tests/src/test/java/picocli/annotation/processing/tests/AnnotatedCommandSourceGeneratorProcessorTest.java new file mode 100644 index 000000000..8a8f1924c --- /dev/null +++ b/picocli-annotation-processing-tests/src/test/java/picocli/annotation/processing/tests/AnnotatedCommandSourceGeneratorProcessorTest.java @@ -0,0 +1,74 @@ +package picocli.annotation.processing.tests; + +import com.google.testing.compile.Compilation; +import com.google.testing.compile.JavaFileObjects; +import org.junit.Ignore; +import org.junit.Test; +import picocli.codegen.annotation.processing.AnnotatedCommandSourceGeneratorProcessor; + +import javax.tools.StandardLocation; + +import static com.google.testing.compile.CompilationSubject.assertThat; +import static com.google.testing.compile.Compiler.javac; + +public class AnnotatedCommandSourceGeneratorProcessorTest { + + @Ignore + @Test + public void generate() { + AnnotatedCommandSourceGeneratorProcessor processor = new AnnotatedCommandSourceGeneratorProcessor(); + Compilation compilation = + javac() + .withProcessors(processor) + .compile(JavaFileObjects.forResource( + "picocli/examples/subcommands/ParentCommandDemo.java")); + assertThat(compilation).succeeded(); + assertThat(compilation) + .generatedSourceFile("GeneratedHelloWorld") + .hasSourceEquivalentTo(JavaFileObjects.forResource("GeneratedHelloWorld.java")); + } + + @Test + public void generate1() { + AnnotatedCommandSourceGeneratorProcessor processor = new AnnotatedCommandSourceGeneratorProcessor(); + Compilation compilation = + javac() + .withProcessors(processor) + .compile(JavaFileObjects.forResource( + "picocli/codegen/aot/graalvm/Example.java")); + assertThat(compilation).succeeded(); + assertThat(compilation) + .generatedFile(StandardLocation.SOURCE_OUTPUT, "generated/picocli/codegen/aot/graalvm/Example.java") + .hasSourceEquivalentTo(JavaFileObjects.forResource("generated/picocli/codegen/aot/graalvm/Example.java")); + } + + //@Ignore("TODO field constant values") + @Test + public void generateNested() { + AnnotatedCommandSourceGeneratorProcessor processor = new AnnotatedCommandSourceGeneratorProcessor(); + Compilation compilation = + javac() + .withProcessors(processor) + .compile(JavaFileObjects.forResource( + "picocli/examples/PopulateFlagsMain.java")); + assertThat(compilation).succeeded(); + assertThat(compilation) + .generatedFile(StandardLocation.SOURCE_OUTPUT, "generated/picocli/examples/PopulateFlagsMain.java") + .hasSourceEquivalentTo(JavaFileObjects.forResource("generated/picocli/examples/PopulateFlagsMain.java")); + } + + @Ignore + @Test + public void generateNested2() { + AnnotatedCommandSourceGeneratorProcessor processor = new AnnotatedCommandSourceGeneratorProcessor(); + Compilation compilation = + javac() + .withProcessors(processor) + .compile(JavaFileObjects.forResource( + "picocli/examples/subcommands/ParentCommandDemo.java")); + assertThat(compilation).succeeded(); + assertThat(compilation) + .generatedFile(StandardLocation.SOURCE_OUTPUT, "generated/examples/subcommands/ParentCommandDemo.java") + .hasSourceEquivalentTo(JavaFileObjects.forResource("generated/examples/subcommands/ParentCommandDemo.java")); + } +} \ No newline at end of file diff --git a/picocli-annotation-processing-tests/src/test/resources/command-method-demo.properties b/picocli-annotation-processing-tests/src/test/resources/command-method-demo.properties new file mode 100644 index 000000000..8d9e1e6e7 --- /dev/null +++ b/picocli-annotation-processing-tests/src/test/resources/command-method-demo.properties @@ -0,0 +1,17 @@ +usage.description = Version control system +usage.headerHeading=the headerHeading +usage.header.0=header0 +usage.header.1=header1 +usage.header.2=header2 +usage.header.3=header3 +usage.descriptionHeading=the descriptionHeading +usage.description.0=description0 +usage.description.1=description1 +usage.description.2=description2 +usage.description.3=description3 +usage.footerHeading=the footerHeading +usage.footer.0=footer0 +usage.footer.1=footer1 +usage.footer.2=footer2 +usage.footer.3=footer3 + diff --git a/picocli-annotation-processing-tests/src/test/resources/generated/picocli/codegen/aot/graalvm/Example.java b/picocli-annotation-processing-tests/src/test/resources/generated/picocli/codegen/aot/graalvm/Example.java new file mode 100644 index 000000000..b68a2800f --- /dev/null +++ b/picocli-annotation-processing-tests/src/test/resources/generated/picocli/codegen/aot/graalvm/Example.java @@ -0,0 +1,49 @@ +package generated.picocli.codegen.aot.graalvm; + +import java.io.File; +import java.util.List; +import java.util.concurrent.TimeUnit; +import picocli.CommandLine.Command; +import picocli.CommandLine.HelpCommand; +import picocli.CommandLine.Mixin; +import picocli.CommandLine.Option; +import picocli.CommandLine.Parameters; +import picocli.codegen.aot.graalvm.Example.ExampleMixin; + +@Command(name = "example", + mixinStandardHelpOptions = true, + subcommands = HelpCommand.class, + version = "3.7.0") +public class Example { + @Mixin ExampleMixin mixin; + + @Option(names = "-t") + TimeUnit timeUnit; + + @Option(names = "--minimum") + public void setMinimum(int min) { + // TODO replace the stored value with the new value + } + + @Parameters(index = "0") + File file; + + @Parameters(index = "1..*", + type = File.class) + public void setOtherFiles(List otherFiles) { + // TODO replace the stored value with the new value + } + + @Command + public static class ExampleMixin { + @Option(names = "-l") + int length; + } + + @Command(name = "multiply") + int multiply( + @Option(names = "--count") int count, + @Parameters(index = "0") int multiplier) { + // TODO implement commandSpec + } +} diff --git a/picocli-annotation-processing-tests/src/test/resources/generated/picocli/examples/PopulateFlagsMain.java b/picocli-annotation-processing-tests/src/test/resources/generated/picocli/examples/PopulateFlagsMain.java new file mode 100644 index 000000000..69882a917 --- /dev/null +++ b/picocli-annotation-processing-tests/src/test/resources/generated/picocli/examples/PopulateFlagsMain.java @@ -0,0 +1,18 @@ +package generated.picocli.examples; + +import picocli.CommandLine.Command; +import picocli.CommandLine.Option; + +public class PopulateFlagsMain { + @Command + private static class Options { + @Option(names = "-b") + private boolean buffered; + + @Option(names = "-o") + private boolean overwriteOutput; // TODO = true; + + @Option(names = "-v") + private boolean verbose; + } +} diff --git a/picocli-annotation-processing-tests/src/test/resources/picocli/codegen/aot/graalvm/Example.java b/picocli-annotation-processing-tests/src/test/resources/picocli/codegen/aot/graalvm/Example.java new file mode 100644 index 000000000..a75cf7abd --- /dev/null +++ b/picocli-annotation-processing-tests/src/test/resources/picocli/codegen/aot/graalvm/Example.java @@ -0,0 +1,78 @@ +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.ParameterException; +import picocli.CommandLine.Parameters; +import picocli.CommandLine.Spec; +import picocli.CommandLine.Unmatched; + +import java.io.File; +import java.util.List; +import java.util.concurrent.TimeUnit; + +@Command(name = "example", version = "3.7.0", + mixinStandardHelpOptions = true, subcommands = CommandLine.HelpCommand.class) +public class Example implements Runnable { + + @Command public static class ExampleMixin { + + @Option(names = "-l") + int length; + } + + @Option(names = "-t") + TimeUnit timeUnit; + + @Parameters(index = "0") + File file; + + @Spec + CommandSpec spec; + + @Mixin + ExampleMixin mixin; + + @Unmatched + List unmatched; + + private int minimum; + private List otherFiles; + + @Command + int multiply(@Option(names = "--count") int count, + @Parameters int multiplier) { + System.out.println("Result is " + count * multiplier); + return count * multiplier; + } + + @Option(names = "--minimum") + public void setMinimum(int min) { + if (min < 0) { + throw new ParameterException(spec.commandLine(), "Minimum must be a positive integer"); + } + minimum = min; + } + + @Parameters(index = "1..*") + public void setOtherFiles(List otherFiles) { + for (File f : otherFiles) { + if (!f.exists()) { + throw new ParameterException(spec.commandLine(), "File " + f.getAbsolutePath() + " must exist"); + } + } + this.otherFiles = otherFiles; + } + + public void run() { + System.out.printf("timeUnit=%s, length=%s, file=%s, unmatched=%s, minimum=%s, otherFiles=%s%n", + timeUnit, mixin.length, file, unmatched, minimum, otherFiles); + } + + public static void main(String[] args) { + CommandLine.run(new Example(), args); + } +} diff --git a/picocli-annotation-processing-tests/src/test/resources/picocli/examples/PopulateFlagsMain.java b/picocli-annotation-processing-tests/src/test/resources/picocli/examples/PopulateFlagsMain.java new file mode 100644 index 000000000..2981e753a --- /dev/null +++ b/picocli-annotation-processing-tests/src/test/resources/picocli/examples/PopulateFlagsMain.java @@ -0,0 +1,128 @@ +/* + Copyright 2017 Robert 'Bobby' Zenz + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +package picocli.examples; + +import picocli.CommandLine; +import picocli.CommandLine.Option; +import picocli.CommandLine.ParameterException; + +/** + * This example demonstrates the usage with a simple flag class. + * + *

+ * + * When no arguments are provided, the flags will have their default values: + * + *

+ * Arguments:
+ *   
+ * 
+ * Options:
+ *         buffered: false
+ *  overwriteOutput: true
+ *          verbose: false
+ * 
+ * + * When the flag is provided in the arguments, the default value of the flag + * will be inverted and set. So if we can provide all flags as arguments: + * + *
+ * Arguments:
+ *   "-b" "-o" "-v"
+ * 
+ * Options:
+ *         buffered: true
+ *  overwriteOutput: false
+ *          verbose: true
+ * 
+ * + * Because these flags are single letter names, we can also provide them + * concatenated in one argument: + * + *
+ * Arguments:
+ *   "-bov"
+ * 
+ * Options:
+ *         buffered: true
+ *  overwriteOutput: false
+ *          verbose: true
+ * 
+ * + * Or in any derivation there of. + * + * @author Robert 'Bobby' Zenz + */ +public class PopulateFlagsMain { + public static void main(String[] args) { + // Create a new Options class, which holds our flags. + Options options = new Options(); + + try { + // Populate the created class from the command line arguments. + CommandLine.populateCommand(options, args); + } catch (ParameterException e) { + // The given command line arguments are invalid, for example there + // are options specified which do not exist or one of the options + // is malformed (missing a value, for example). + System.out.println(e.getMessage()); + CommandLine.usage(options, System.out); + return; + } + + // Print the state. + System.out.println("Arguments:"); + System.out.print(" "); + for (String arg : args) { + System.out.print("\"" + arg + "\" "); + } + System.out.println(); + System.out.println(); + + System.out.println("Options:"); + System.out.println(" buffered: " + options.isBuffered()); + System.out.println(" overwriteOutput: " + options.isOverwriteOutput()); + System.out.println(" verbose: " + options.isVerbose()); + } + + /** + * This is the main container which will be populated by picocli with values + * from the arguments. + */ + private static class Options { + @Option(names = "-b") + private boolean buffered = false; + + @Option(names = "-o") + private boolean overwriteOutput; //TODO = true; + + @Option(names = "-v") + private boolean verbose = false; + + public boolean isBuffered() { + return buffered; + } + + public boolean isOverwriteOutput() { + return overwriteOutput; + } + + public boolean isVerbose() { + return verbose; + } + } +} diff --git a/picocli-annotation-processing-tests/src/test/resources/picocli/examples/annotatedmethods/AnnotatedClassMethodOptions.java b/picocli-annotation-processing-tests/src/test/resources/picocli/examples/annotatedmethods/AnnotatedClassMethodOptions.java new file mode 100644 index 000000000..b56243ec3 --- /dev/null +++ b/picocli-annotation-processing-tests/src/test/resources/picocli/examples/annotatedmethods/AnnotatedClassMethodOptions.java @@ -0,0 +1,115 @@ +package picocli.examples.annotatedmethods; + +import picocli.CommandLine.ParameterException; +import picocli.CommandLine.PicocliException; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.*; + +import static picocli.CommandLine.Option; + +public class AnnotatedClassMethodOptions { + + static class Primitives { + boolean aBoolean; + byte aByte; + short aShort; + int anInt; + long aLong; + float aFloat; + double aDouble; + + @Option(names = "-b") void setBoolean(boolean val) { aBoolean = val; } + @Option(names = "-y") void setByte(byte val) { aByte = val; } + @Option(names = "-s") void setShort(short val) { aShort = val; } + @Option(names = "-i") void setInt(int val) { anInt = val; } + @Option(names = "-l") void setLong(long val) { aLong = val; } + @Option(names = "-f") void setFloat(float val) { aFloat = val; } + @Option(names = "-d") void setDouble(double val) { aDouble = val; } + } + + static class PrimitivesWithDefault { + boolean aBoolean; + byte aByte; + short aShort; + int anInt; + long aLong; + float aFloat; + double aDouble; + + @Option(names = "-b", defaultValue = "true") void setBoolean(boolean val) { aBoolean = val; } + @Option(names = "-y", defaultValue = "11") void setByte(byte val) { aByte = val; } + @Option(names = "-s", defaultValue = "12") void setShort(short val) { aShort = val; } + @Option(names = "-i", defaultValue = "13") void setInt(int val) { anInt = val; } + @Option(names = "-l", defaultValue = "14") void setLong(long val) { aLong = val; } + @Option(names = "-f", defaultValue = "15.5") void setFloat(float val) { aFloat = val; } + @Option(names = "-d", defaultValue = "16.6") void setDouble(double val) { aDouble = val; } + } + + static class ObjectsWithDefaults { + Boolean aBoolean; + Byte aByte; + Short aShort; + Integer anInt; + Long aLong; + Float aFloat; + Double aDouble; + BigDecimal aBigDecimal; + String aString; + List aList; + Map aMap; + SortedSet aSet; + + @Option(names = "-b", defaultValue = "true") void setBoolean(Boolean val) { aBoolean = val; } + @Option(names = "-y", defaultValue = "123") void setByte(Byte val) { aByte = val; } + @Option(names = "-s", defaultValue = "11") void setShort(Short val) { aShort = val; } + @Option(names = "-i", defaultValue = "12") void setInt(Integer val) { anInt = val; } + @Option(names = "-l", defaultValue = "13") void setLong(Long val) { aLong = val; } + @Option(names = "-f", defaultValue = "14.4") void setFloat(Float val) { aFloat = val; } + @Option(names = "-d", defaultValue = "15.5") void setDouble(Double val) { aDouble = val; } + + @Option(names = "-bigint", defaultValue = "16.6") void setBigDecimal(BigDecimal val) { aBigDecimal = val; } + @Option(names = "-string", defaultValue = "abc") void setString(String val) { aString = val; } + @Option(names = "-list", defaultValue = "a,b,c", split = ",") void setList(List val) { aList = val; } + + @Option(names = "-map", defaultValue = "1=1,2=2,3=3", split = ",") + void setMap(Map val) { aMap = val; } + + @Option(names = "-set", defaultValue = "1,2,3", split = ",") + void setSortedSet(SortedSet val) { aSet = val; } + } + + static class Objects { + Boolean aBoolean; + Byte aByte; + Short aShort; + Integer anInt; + Long aLong; + Float aFloat; + Double aDouble; + BigInteger aBigInteger; + String aString; + List aList; + Map aMap; + SortedSet aSet; + + @Option(names = "-b") void setBoolean(Boolean val) { aBoolean = val; } + @Option(names = "-y") void setByte(Byte val) { aByte = val; } + @Option(names = "-s") void setShort(Short val) { aShort = val; } + @Option(names = "-i") void setInt(Integer val) { anInt = val; } + @Option(names = "-l") void setLong(Long val) { aLong = val; } + @Option(names = "-f") void setFloat(Float val) { aFloat = val; } + @Option(names = "-d") void setDouble(Double val) { aDouble = val; } + + @Option(names = "-bigint") void setBigInteger(BigInteger val) { aBigInteger = val; } + @Option(names = "-string") void setString(String val) { aString = val; } + @Option(names = "-list") void setList(List val) { aList = val; } + + @Option(names = "-map") + void setMap(Map val) { aMap = val; } + + @Option(names = "-set") + void setSortedSet(SortedSet val) { aSet = val; } + } +} diff --git a/picocli-annotation-processing-tests/src/test/resources/picocli/examples/annotatedmethods/AnnotatedInterfaceMethodOptions.java b/picocli-annotation-processing-tests/src/test/resources/picocli/examples/annotatedmethods/AnnotatedInterfaceMethodOptions.java new file mode 100644 index 000000000..202c6f7e7 --- /dev/null +++ b/picocli-annotation-processing-tests/src/test/resources/picocli/examples/annotatedmethods/AnnotatedInterfaceMethodOptions.java @@ -0,0 +1,140 @@ +package picocli.examples.annotatedmethods; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.SortedSet; +import java.util.TreeSet; + +import static picocli.CommandLine.*; + +public class AnnotatedInterfaceMethodOptions { + + interface Primitives { + @Option(names = "-b") + boolean aBoolean(); + + @Option(names = "-y") + byte aByte(); + + @Option(names = "-s") + short aShort(); + + @Option(names = "-i") + int anInt(); + + @Option(names = "-l") + long aLong(); + + @Option(names = "-f") + float aFloat(); + + @Option(names = "-d") + double aDouble(); + } + + interface PrimitivesWithDefault { + @Option(names = "-b", defaultValue = "true") + boolean aBoolean(); + + @Option(names = "-y", defaultValue = "11") + byte aByte(); + + @Option(names = "-s", defaultValue = "12") + short aShort(); + + @Option(names = "-i", defaultValue = "13") + int anInt(); + + @Option(names = "-l", defaultValue = "14") + long aLong(); + + @Option(names = "-f", defaultValue = "15.5") + float aFloat(); + + @Option(names = "-d", defaultValue = "16.6") + double aDouble(); + } + + interface Objects { + @Option(names = "-b") + Boolean aBoolean(); + + @Option(names = "-y") + Byte aByte(); + + @Option(names = "-s") + Short aShort(); + + @Option(names = "-i") + Integer anInt(); + + @Option(names = "-l") + Long aLong(); + + @Option(names = "-f") + Float aFloat(); + + @Option(names = "-d") + Double aDouble(); + + @Option(names = "-bigint") + BigInteger aBigInteger(); + + @Option(names = "-string") + String aString(); + + @Option(names = "-list") + List getList(); + + @Option(names = "-map") + Map getMap(); + + @Option(names = "-set") + SortedSet getSortedSet(); + } + + interface ObjectsWithDefault { + @Option(names = "-b", defaultValue = "true") + Boolean aBoolean(); + + @Option(names = "-y", defaultValue = "123") + Byte aByte(); + + @Option(names = "-s", defaultValue = "11") + Short aShort(); + + @Option(names = "-i", defaultValue = "12") + Integer anInt(); + + @Option(names = "-l", defaultValue = "13") + Long aLong(); + + @Option(names = "-f", defaultValue = "14.4") + Float aFloat(); + + @Option(names = "-d", defaultValue = "15.5") + Double aDouble(); + + @Option(names = "-bigint", defaultValue = "16.6") + BigDecimal aBigDecimal(); + + @Option(names = "-string", defaultValue = "abc") + String aString(); + + @Option(names = "-list", defaultValue = "a,b,c", split = ",") + List getList(); + + @Option(names = "-map", defaultValue = "1=1,2=2,3=3", split = ",") + Map getMap(); + + @Option(names = "-set", defaultValue = "1,2,3", split = ",") + SortedSet getSortedSet(); + } + +} diff --git a/picocli-annotation-processing-tests/src/test/resources/picocli/examples/annotatedmethods/CObjects.yaml b/picocli-annotation-processing-tests/src/test/resources/picocli/examples/annotatedmethods/CObjects.yaml new file mode 100644 index 000000000..3279dd1ef --- /dev/null +++ b/picocli-annotation-processing-tests/src/test/resources/picocli/examples/annotatedmethods/CObjects.yaml @@ -0,0 +1,300 @@ +--- +CommandSpec: + name: '
' + aliases: [] + userObject: picocli.CommandLineAnnotatedMethodImplTest$Objects@2626b418 + helpCommand: false + defaultValueProvider: null + versionProvider: null + version: [] + Options: + - names: [-b] + usageHelp: false + versionHelp: false + description: [] + descriptionKey: '' + typeInfo: RuntimeTypeInfo(java.lang.Boolean, aux=[class java.lang.Boolean], collection=false, map=false) + arity: 0 + splitRegex: '' + interactive: false + required: false + hidden: false + hideParamSyntax: false + defaultValue: 'null' + showDefaultValue: ON_DEMAND + hasInitialValue: false + initialValue: 'null' + paramLabel: '' + converters: [] + completionCandidates: null + getter: picocli.CommandLine.Model.MethodBinding(void picocli.CommandLineAnnotatedMethodImplTest$Objects.setBoolean(java.lang.Boolean)) + setter: picocli.CommandLine.Model.MethodBinding(void picocli.CommandLineAnnotatedMethodImplTest$Objects.setBoolean(java.lang.Boolean)) + - names: [-bigint] + usageHelp: false + versionHelp: false + description: [] + descriptionKey: '' + typeInfo: RuntimeTypeInfo(java.math.BigInteger, aux=[class java.math.BigInteger], collection=false, map=false) + arity: 1 + splitRegex: '' + interactive: false + required: false + hidden: false + hideParamSyntax: false + defaultValue: 'null' + showDefaultValue: ON_DEMAND + hasInitialValue: false + initialValue: 'null' + paramLabel: '' + converters: [] + completionCandidates: null + getter: picocli.CommandLine.Model.MethodBinding(void picocli.CommandLineAnnotatedMethodImplTest$Objects.setBigInteger(java.math.BigInteger)) + setter: picocli.CommandLine.Model.MethodBinding(void picocli.CommandLineAnnotatedMethodImplTest$Objects.setBigInteger(java.math.BigInteger)) + - names: [-d] + usageHelp: false + versionHelp: false + description: [] + descriptionKey: '' + typeInfo: RuntimeTypeInfo(java.lang.Double, aux=[class java.lang.Double], collection=false, map=false) + arity: 1 + splitRegex: '' + interactive: false + required: false + hidden: false + hideParamSyntax: false + defaultValue: 'null' + showDefaultValue: ON_DEMAND + hasInitialValue: false + initialValue: 'null' + paramLabel: '' + converters: [] + completionCandidates: null + getter: picocli.CommandLine.Model.MethodBinding(void picocli.CommandLineAnnotatedMethodImplTest$Objects.setDouble(java.lang.Double)) + setter: picocli.CommandLine.Model.MethodBinding(void picocli.CommandLineAnnotatedMethodImplTest$Objects.setDouble(java.lang.Double)) + - names: [-f] + usageHelp: false + versionHelp: false + description: [] + descriptionKey: '' + typeInfo: RuntimeTypeInfo(java.lang.Float, aux=[class java.lang.Float], collection=false, map=false) + arity: 1 + splitRegex: '' + interactive: false + required: false + hidden: false + hideParamSyntax: false + defaultValue: 'null' + showDefaultValue: ON_DEMAND + hasInitialValue: false + initialValue: 'null' + paramLabel: '' + converters: [] + completionCandidates: null + getter: picocli.CommandLine.Model.MethodBinding(void picocli.CommandLineAnnotatedMethodImplTest$Objects.setFloat(java.lang.Float)) + setter: picocli.CommandLine.Model.MethodBinding(void picocli.CommandLineAnnotatedMethodImplTest$Objects.setFloat(java.lang.Float)) + - names: [-i] + usageHelp: false + versionHelp: false + description: [] + descriptionKey: '' + typeInfo: RuntimeTypeInfo(java.lang.Integer, aux=[class java.lang.Integer], collection=false, map=false) + arity: 1 + splitRegex: '' + interactive: false + required: false + hidden: false + hideParamSyntax: false + defaultValue: 'null' + showDefaultValue: ON_DEMAND + hasInitialValue: false + initialValue: 'null' + paramLabel: '' + converters: [] + completionCandidates: null + getter: picocli.CommandLine.Model.MethodBinding(void picocli.CommandLineAnnotatedMethodImplTest$Objects.setInt(java.lang.Integer)) + setter: picocli.CommandLine.Model.MethodBinding(void picocli.CommandLineAnnotatedMethodImplTest$Objects.setInt(java.lang.Integer)) + - names: [-l] + usageHelp: false + versionHelp: false + description: [] + descriptionKey: '' + typeInfo: RuntimeTypeInfo(java.lang.Long, aux=[class java.lang.Long], collection=false, map=false) + arity: 1 + splitRegex: '' + interactive: false + required: false + hidden: false + hideParamSyntax: false + defaultValue: 'null' + showDefaultValue: ON_DEMAND + hasInitialValue: false + initialValue: 'null' + paramLabel: '' + converters: [] + completionCandidates: null + getter: picocli.CommandLine.Model.MethodBinding(void picocli.CommandLineAnnotatedMethodImplTest$Objects.setLong(java.lang.Long)) + setter: picocli.CommandLine.Model.MethodBinding(void picocli.CommandLineAnnotatedMethodImplTest$Objects.setLong(java.lang.Long)) + - names: [-list] + usageHelp: false + versionHelp: false + description: [] + descriptionKey: '' + typeInfo: RuntimeTypeInfo(java.util.List, aux=[class java.lang.String], collection=true, map=false) + arity: 1 + splitRegex: '' + interactive: false + required: false + hidden: false + hideParamSyntax: false + defaultValue: 'null' + showDefaultValue: ON_DEMAND + hasInitialValue: false + initialValue: 'null' + paramLabel: '' + converters: [] + completionCandidates: null + getter: picocli.CommandLine.Model.MethodBinding(void picocli.CommandLineAnnotatedMethodImplTest$Objects.setList(java.util.List)) + setter: picocli.CommandLine.Model.MethodBinding(void picocli.CommandLineAnnotatedMethodImplTest$Objects.setList(java.util.List)) + - names: [-map] + usageHelp: false + versionHelp: false + description: [] + descriptionKey: '' + typeInfo: RuntimeTypeInfo(java.util.Map, aux=[class java.lang.Integer, class java.lang.Double], collection=false, map=true) + arity: 1 + splitRegex: '' + interactive: false + required: false + hidden: false + hideParamSyntax: false + defaultValue: 'null' + showDefaultValue: ON_DEMAND + hasInitialValue: false + initialValue: 'null' + paramLabel: '' + converters: [] + completionCandidates: null + getter: picocli.CommandLine.Model.MethodBinding(void picocli.CommandLineAnnotatedMethodImplTest$Objects.setMap(java.util.Map)) + setter: picocli.CommandLine.Model.MethodBinding(void picocli.CommandLineAnnotatedMethodImplTest$Objects.setMap(java.util.Map)) + - names: [-s] + usageHelp: false + versionHelp: false + description: [] + descriptionKey: '' + typeInfo: RuntimeTypeInfo(java.lang.Short, aux=[class java.lang.Short], collection=false, map=false) + arity: 1 + splitRegex: '' + interactive: false + required: false + hidden: false + hideParamSyntax: false + defaultValue: 'null' + showDefaultValue: ON_DEMAND + hasInitialValue: false + initialValue: 'null' + paramLabel: '' + converters: [] + completionCandidates: null + getter: picocli.CommandLine.Model.MethodBinding(void picocli.CommandLineAnnotatedMethodImplTest$Objects.setShort(java.lang.Short)) + setter: picocli.CommandLine.Model.MethodBinding(void picocli.CommandLineAnnotatedMethodImplTest$Objects.setShort(java.lang.Short)) + - names: [-set] + usageHelp: false + versionHelp: false + description: [] + descriptionKey: '' + typeInfo: RuntimeTypeInfo(java.util.SortedSet, aux=[class java.lang.Short], collection=true, map=false) + arity: 1 + splitRegex: '' + interactive: false + required: false + hidden: false + hideParamSyntax: false + defaultValue: 'null' + showDefaultValue: ON_DEMAND + hasInitialValue: false + initialValue: 'null' + paramLabel: '' + converters: [] + completionCandidates: null + getter: picocli.CommandLine.Model.MethodBinding(void picocli.CommandLineAnnotatedMethodImplTest$Objects.setSortedSet(java.util.SortedSet)) + setter: picocli.CommandLine.Model.MethodBinding(void picocli.CommandLineAnnotatedMethodImplTest$Objects.setSortedSet(java.util.SortedSet)) + - names: [-string] + usageHelp: false + versionHelp: false + description: [] + descriptionKey: '' + typeInfo: RuntimeTypeInfo(java.lang.String, aux=[class java.lang.String], collection=false, map=false) + arity: 1 + splitRegex: '' + interactive: false + required: false + hidden: false + hideParamSyntax: false + defaultValue: 'null' + showDefaultValue: ON_DEMAND + hasInitialValue: false + initialValue: 'null' + paramLabel: '' + converters: [] + completionCandidates: null + getter: picocli.CommandLine.Model.MethodBinding(void picocli.CommandLineAnnotatedMethodImplTest$Objects.setString(java.lang.String)) + setter: picocli.CommandLine.Model.MethodBinding(void picocli.CommandLineAnnotatedMethodImplTest$Objects.setString(java.lang.String)) + - names: [-y] + usageHelp: false + versionHelp: false + description: [] + descriptionKey: '' + typeInfo: RuntimeTypeInfo(java.lang.Byte, aux=[class java.lang.Byte], collection=false, map=false) + arity: 1 + splitRegex: '' + interactive: false + required: false + hidden: false + hideParamSyntax: false + defaultValue: 'null' + showDefaultValue: ON_DEMAND + hasInitialValue: false + initialValue: 'null' + paramLabel: '' + converters: [] + completionCandidates: null + getter: picocli.CommandLine.Model.MethodBinding(void picocli.CommandLineAnnotatedMethodImplTest$Objects.setByte(java.lang.Byte)) + setter: picocli.CommandLine.Model.MethodBinding(void picocli.CommandLineAnnotatedMethodImplTest$Objects.setByte(java.lang.Byte)) + PositionalParams: [] + UnmatchedArgsBindings: [] + Mixins: [] + UsageMessageSpec: + width: 80 + abbreviateSynopsis: false + hidden: false + showDefaultValues: false + sortOptions: true + requiredOptionMarker: ' ' + headerHeading: '' + header: [] + synopsisHeading: 'Usage: ' + customSynopsis: [] + descriptionHeading: '' + description: [] + parameterListHeading: '' + optionListHeading: '' + commandListHeading: 'Commands:%n' + footerHeading: '' + footer: [] + ParserSpec: + separator: '=' + endOfOptionsDelimiter: '--' + expandAtFiles: true + atFileCommentChar: '#' + overwrittenOptionsAllowed: false + unmatchedArgumentsAllowed: false + unmatchedOptionsArePositionalParams: false + stopAtUnmatched: false + stopAtPositional: false + posixClusteredShortOptionsAllowed: true + aritySatisfiedByAttachedOptionParam: false + caseInsensitiveEnumValuesAllowed: false + collectErrors: false + limitSplit: false + toggleBooleanFlags: true + Subcommands: [] diff --git a/picocli-annotation-processing-tests/src/test/resources/picocli/examples/annotatedmethods/CObjectsWithDefaults.yaml b/picocli-annotation-processing-tests/src/test/resources/picocli/examples/annotatedmethods/CObjectsWithDefaults.yaml new file mode 100644 index 000000000..36164ff49 --- /dev/null +++ b/picocli-annotation-processing-tests/src/test/resources/picocli/examples/annotatedmethods/CObjectsWithDefaults.yaml @@ -0,0 +1,300 @@ +--- +CommandSpec: + name: '
' + aliases: [] + userObject: picocli.CommandLineAnnotatedMethodImplTest$ObjectsWithDefaults@2626b418 + helpCommand: false + defaultValueProvider: null + versionProvider: null + version: [] + Options: + - names: [-b] + usageHelp: false + versionHelp: false + description: [] + descriptionKey: '' + typeInfo: RuntimeTypeInfo(java.lang.Boolean, aux=[class java.lang.Boolean], collection=false, map=false) + arity: 0 + splitRegex: '' + interactive: false + required: false + hidden: false + hideParamSyntax: false + defaultValue: 'true' + showDefaultValue: ON_DEMAND + hasInitialValue: false + initialValue: 'null' + paramLabel: '' + converters: [] + completionCandidates: null + getter: picocli.CommandLine.Model.MethodBinding(void picocli.CommandLineAnnotatedMethodImplTest$ObjectsWithDefaults.setBoolean(java.lang.Boolean)) + setter: picocli.CommandLine.Model.MethodBinding(void picocli.CommandLineAnnotatedMethodImplTest$ObjectsWithDefaults.setBoolean(java.lang.Boolean)) + - names: [-bigint] + usageHelp: false + versionHelp: false + description: [] + descriptionKey: '' + typeInfo: RuntimeTypeInfo(java.math.BigDecimal, aux=[class java.math.BigDecimal], collection=false, map=false) + arity: 1 + splitRegex: '' + interactive: false + required: false + hidden: false + hideParamSyntax: false + defaultValue: '16.6' + showDefaultValue: ON_DEMAND + hasInitialValue: false + initialValue: 'null' + paramLabel: '' + converters: [] + completionCandidates: null + getter: picocli.CommandLine.Model.MethodBinding(void picocli.CommandLineAnnotatedMethodImplTest$ObjectsWithDefaults.setBigDecimal(java.math.BigDecimal)) + setter: picocli.CommandLine.Model.MethodBinding(void picocli.CommandLineAnnotatedMethodImplTest$ObjectsWithDefaults.setBigDecimal(java.math.BigDecimal)) + - names: [-d] + usageHelp: false + versionHelp: false + description: [] + descriptionKey: '' + typeInfo: RuntimeTypeInfo(java.lang.Double, aux=[class java.lang.Double], collection=false, map=false) + arity: 1 + splitRegex: '' + interactive: false + required: false + hidden: false + hideParamSyntax: false + defaultValue: '15.5' + showDefaultValue: ON_DEMAND + hasInitialValue: false + initialValue: 'null' + paramLabel: '' + converters: [] + completionCandidates: null + getter: picocli.CommandLine.Model.MethodBinding(void picocli.CommandLineAnnotatedMethodImplTest$ObjectsWithDefaults.setDouble(java.lang.Double)) + setter: picocli.CommandLine.Model.MethodBinding(void picocli.CommandLineAnnotatedMethodImplTest$ObjectsWithDefaults.setDouble(java.lang.Double)) + - names: [-f] + usageHelp: false + versionHelp: false + description: [] + descriptionKey: '' + typeInfo: RuntimeTypeInfo(java.lang.Float, aux=[class java.lang.Float], collection=false, map=false) + arity: 1 + splitRegex: '' + interactive: false + required: false + hidden: false + hideParamSyntax: false + defaultValue: '14.4' + showDefaultValue: ON_DEMAND + hasInitialValue: false + initialValue: 'null' + paramLabel: '' + converters: [] + completionCandidates: null + getter: picocli.CommandLine.Model.MethodBinding(void picocli.CommandLineAnnotatedMethodImplTest$ObjectsWithDefaults.setFloat(java.lang.Float)) + setter: picocli.CommandLine.Model.MethodBinding(void picocli.CommandLineAnnotatedMethodImplTest$ObjectsWithDefaults.setFloat(java.lang.Float)) + - names: [-i] + usageHelp: false + versionHelp: false + description: [] + descriptionKey: '' + typeInfo: RuntimeTypeInfo(java.lang.Integer, aux=[class java.lang.Integer], collection=false, map=false) + arity: 1 + splitRegex: '' + interactive: false + required: false + hidden: false + hideParamSyntax: false + defaultValue: '12' + showDefaultValue: ON_DEMAND + hasInitialValue: false + initialValue: 'null' + paramLabel: '' + converters: [] + completionCandidates: null + getter: picocli.CommandLine.Model.MethodBinding(void picocli.CommandLineAnnotatedMethodImplTest$ObjectsWithDefaults.setInt(java.lang.Integer)) + setter: picocli.CommandLine.Model.MethodBinding(void picocli.CommandLineAnnotatedMethodImplTest$ObjectsWithDefaults.setInt(java.lang.Integer)) + - names: [-l] + usageHelp: false + versionHelp: false + description: [] + descriptionKey: '' + typeInfo: RuntimeTypeInfo(java.lang.Long, aux=[class java.lang.Long], collection=false, map=false) + arity: 1 + splitRegex: '' + interactive: false + required: false + hidden: false + hideParamSyntax: false + defaultValue: '13' + showDefaultValue: ON_DEMAND + hasInitialValue: false + initialValue: 'null' + paramLabel: '' + converters: [] + completionCandidates: null + getter: picocli.CommandLine.Model.MethodBinding(void picocli.CommandLineAnnotatedMethodImplTest$ObjectsWithDefaults.setLong(java.lang.Long)) + setter: picocli.CommandLine.Model.MethodBinding(void picocli.CommandLineAnnotatedMethodImplTest$ObjectsWithDefaults.setLong(java.lang.Long)) + - names: [-list] + usageHelp: false + versionHelp: false + description: [] + descriptionKey: '' + typeInfo: RuntimeTypeInfo(java.util.List, aux=[class java.lang.String], collection=true, map=false) + arity: 1 + splitRegex: ',' + interactive: false + required: false + hidden: false + hideParamSyntax: false + defaultValue: 'a,b,c' + showDefaultValue: ON_DEMAND + hasInitialValue: false + initialValue: 'null' + paramLabel: '' + converters: [] + completionCandidates: null + getter: picocli.CommandLine.Model.MethodBinding(void picocli.CommandLineAnnotatedMethodImplTest$ObjectsWithDefaults.setList(java.util.List)) + setter: picocli.CommandLine.Model.MethodBinding(void picocli.CommandLineAnnotatedMethodImplTest$ObjectsWithDefaults.setList(java.util.List)) + - names: [-map] + usageHelp: false + versionHelp: false + description: [] + descriptionKey: '' + typeInfo: RuntimeTypeInfo(java.util.Map, aux=[class java.lang.Integer, class java.lang.Double], collection=false, map=true) + arity: 1 + splitRegex: ',' + interactive: false + required: false + hidden: false + hideParamSyntax: false + defaultValue: '1=1,2=2,3=3' + showDefaultValue: ON_DEMAND + hasInitialValue: false + initialValue: 'null' + paramLabel: '' + converters: [] + completionCandidates: null + getter: picocli.CommandLine.Model.MethodBinding(void picocli.CommandLineAnnotatedMethodImplTest$ObjectsWithDefaults.setMap(java.util.Map)) + setter: picocli.CommandLine.Model.MethodBinding(void picocli.CommandLineAnnotatedMethodImplTest$ObjectsWithDefaults.setMap(java.util.Map)) + - names: [-s] + usageHelp: false + versionHelp: false + description: [] + descriptionKey: '' + typeInfo: RuntimeTypeInfo(java.lang.Short, aux=[class java.lang.Short], collection=false, map=false) + arity: 1 + splitRegex: '' + interactive: false + required: false + hidden: false + hideParamSyntax: false + defaultValue: '11' + showDefaultValue: ON_DEMAND + hasInitialValue: false + initialValue: 'null' + paramLabel: '' + converters: [] + completionCandidates: null + getter: picocli.CommandLine.Model.MethodBinding(void picocli.CommandLineAnnotatedMethodImplTest$ObjectsWithDefaults.setShort(java.lang.Short)) + setter: picocli.CommandLine.Model.MethodBinding(void picocli.CommandLineAnnotatedMethodImplTest$ObjectsWithDefaults.setShort(java.lang.Short)) + - names: [-set] + usageHelp: false + versionHelp: false + description: [] + descriptionKey: '' + typeInfo: RuntimeTypeInfo(java.util.SortedSet, aux=[class java.lang.Short], collection=true, map=false) + arity: 1 + splitRegex: ',' + interactive: false + required: false + hidden: false + hideParamSyntax: false + defaultValue: '1,2,3' + showDefaultValue: ON_DEMAND + hasInitialValue: false + initialValue: 'null' + paramLabel: '' + converters: [] + completionCandidates: null + getter: picocli.CommandLine.Model.MethodBinding(void picocli.CommandLineAnnotatedMethodImplTest$ObjectsWithDefaults.setSortedSet(java.util.SortedSet)) + setter: picocli.CommandLine.Model.MethodBinding(void picocli.CommandLineAnnotatedMethodImplTest$ObjectsWithDefaults.setSortedSet(java.util.SortedSet)) + - names: [-string] + usageHelp: false + versionHelp: false + description: [] + descriptionKey: '' + typeInfo: RuntimeTypeInfo(java.lang.String, aux=[class java.lang.String], collection=false, map=false) + arity: 1 + splitRegex: '' + interactive: false + required: false + hidden: false + hideParamSyntax: false + defaultValue: 'abc' + showDefaultValue: ON_DEMAND + hasInitialValue: false + initialValue: 'null' + paramLabel: '' + converters: [] + completionCandidates: null + getter: picocli.CommandLine.Model.MethodBinding(void picocli.CommandLineAnnotatedMethodImplTest$ObjectsWithDefaults.setString(java.lang.String)) + setter: picocli.CommandLine.Model.MethodBinding(void picocli.CommandLineAnnotatedMethodImplTest$ObjectsWithDefaults.setString(java.lang.String)) + - names: [-y] + usageHelp: false + versionHelp: false + description: [] + descriptionKey: '' + typeInfo: RuntimeTypeInfo(java.lang.Byte, aux=[class java.lang.Byte], collection=false, map=false) + arity: 1 + splitRegex: '' + interactive: false + required: false + hidden: false + hideParamSyntax: false + defaultValue: '123' + showDefaultValue: ON_DEMAND + hasInitialValue: false + initialValue: 'null' + paramLabel: '' + converters: [] + completionCandidates: null + getter: picocli.CommandLine.Model.MethodBinding(void picocli.CommandLineAnnotatedMethodImplTest$ObjectsWithDefaults.setByte(java.lang.Byte)) + setter: picocli.CommandLine.Model.MethodBinding(void picocli.CommandLineAnnotatedMethodImplTest$ObjectsWithDefaults.setByte(java.lang.Byte)) + PositionalParams: [] + UnmatchedArgsBindings: [] + Mixins: [] + UsageMessageSpec: + width: 80 + abbreviateSynopsis: false + hidden: false + showDefaultValues: false + sortOptions: true + requiredOptionMarker: ' ' + headerHeading: '' + header: [] + synopsisHeading: 'Usage: ' + customSynopsis: [] + descriptionHeading: '' + description: [] + parameterListHeading: '' + optionListHeading: '' + commandListHeading: 'Commands:%n' + footerHeading: '' + footer: [] + ParserSpec: + separator: '=' + endOfOptionsDelimiter: '--' + expandAtFiles: true + atFileCommentChar: '#' + overwrittenOptionsAllowed: false + unmatchedArgumentsAllowed: false + unmatchedOptionsArePositionalParams: false + stopAtUnmatched: false + stopAtPositional: false + posixClusteredShortOptionsAllowed: true + aritySatisfiedByAttachedOptionParam: false + caseInsensitiveEnumValuesAllowed: false + collectErrors: false + limitSplit: false + toggleBooleanFlags: true + Subcommands: [] diff --git a/picocli-annotation-processing-tests/src/test/resources/picocli/examples/annotatedmethods/CPrimitives.yaml b/picocli-annotation-processing-tests/src/test/resources/picocli/examples/annotatedmethods/CPrimitives.yaml new file mode 100644 index 000000000..553fd126a --- /dev/null +++ b/picocli-annotation-processing-tests/src/test/resources/picocli/examples/annotatedmethods/CPrimitives.yaml @@ -0,0 +1,195 @@ +--- +CommandSpec: + name: '
' + aliases: [] + userObject: picocli.CommandLineAnnotatedMethodImplTest$Primitives@2626b418 + helpCommand: false + defaultValueProvider: null + versionProvider: null + version: [] + Options: + - names: [-b] + usageHelp: false + versionHelp: false + description: [] + descriptionKey: '' + typeInfo: RuntimeTypeInfo(boolean, aux=[boolean], collection=false, map=false) + arity: 0 + splitRegex: '' + interactive: false + required: false + hidden: false + hideParamSyntax: false + defaultValue: 'null' + showDefaultValue: ON_DEMAND + hasInitialValue: false + initialValue: 'null' + paramLabel: '' + converters: [] + completionCandidates: null + getter: picocli.CommandLine.Model.MethodBinding(void picocli.CommandLineAnnotatedMethodImplTest$Primitives.setBoolean(boolean)) + setter: picocli.CommandLine.Model.MethodBinding(void picocli.CommandLineAnnotatedMethodImplTest$Primitives.setBoolean(boolean)) + - names: [-d] + usageHelp: false + versionHelp: false + description: [] + descriptionKey: '' + typeInfo: RuntimeTypeInfo(double, aux=[double], collection=false, map=false) + arity: 1 + splitRegex: '' + interactive: false + required: false + hidden: false + hideParamSyntax: false + defaultValue: 'null' + showDefaultValue: ON_DEMAND + hasInitialValue: false + initialValue: 'null' + paramLabel: '' + converters: [] + completionCandidates: null + getter: picocli.CommandLine.Model.MethodBinding(void picocli.CommandLineAnnotatedMethodImplTest$Primitives.setDouble(double)) + setter: picocli.CommandLine.Model.MethodBinding(void picocli.CommandLineAnnotatedMethodImplTest$Primitives.setDouble(double)) + - names: [-f] + usageHelp: false + versionHelp: false + description: [] + descriptionKey: '' + typeInfo: RuntimeTypeInfo(float, aux=[float], collection=false, map=false) + arity: 1 + splitRegex: '' + interactive: false + required: false + hidden: false + hideParamSyntax: false + defaultValue: 'null' + showDefaultValue: ON_DEMAND + hasInitialValue: false + initialValue: 'null' + paramLabel: '' + converters: [] + completionCandidates: null + getter: picocli.CommandLine.Model.MethodBinding(void picocli.CommandLineAnnotatedMethodImplTest$Primitives.setFloat(float)) + setter: picocli.CommandLine.Model.MethodBinding(void picocli.CommandLineAnnotatedMethodImplTest$Primitives.setFloat(float)) + - names: [-i] + usageHelp: false + versionHelp: false + description: [] + descriptionKey: '' + typeInfo: RuntimeTypeInfo(int, aux=[int], collection=false, map=false) + arity: 1 + splitRegex: '' + interactive: false + required: false + hidden: false + hideParamSyntax: false + defaultValue: 'null' + showDefaultValue: ON_DEMAND + hasInitialValue: false + initialValue: 'null' + paramLabel: '' + converters: [] + completionCandidates: null + getter: picocli.CommandLine.Model.MethodBinding(void picocli.CommandLineAnnotatedMethodImplTest$Primitives.setInt(int)) + setter: picocli.CommandLine.Model.MethodBinding(void picocli.CommandLineAnnotatedMethodImplTest$Primitives.setInt(int)) + - names: [-l] + usageHelp: false + versionHelp: false + description: [] + descriptionKey: '' + typeInfo: RuntimeTypeInfo(long, aux=[long], collection=false, map=false) + arity: 1 + splitRegex: '' + interactive: false + required: false + hidden: false + hideParamSyntax: false + defaultValue: 'null' + showDefaultValue: ON_DEMAND + hasInitialValue: false + initialValue: 'null' + paramLabel: '' + converters: [] + completionCandidates: null + getter: picocli.CommandLine.Model.MethodBinding(void picocli.CommandLineAnnotatedMethodImplTest$Primitives.setLong(long)) + setter: picocli.CommandLine.Model.MethodBinding(void picocli.CommandLineAnnotatedMethodImplTest$Primitives.setLong(long)) + - names: [-s] + usageHelp: false + versionHelp: false + description: [] + descriptionKey: '' + typeInfo: RuntimeTypeInfo(short, aux=[short], collection=false, map=false) + arity: 1 + splitRegex: '' + interactive: false + required: false + hidden: false + hideParamSyntax: false + defaultValue: 'null' + showDefaultValue: ON_DEMAND + hasInitialValue: false + initialValue: 'null' + paramLabel: '' + converters: [] + completionCandidates: null + getter: picocli.CommandLine.Model.MethodBinding(void picocli.CommandLineAnnotatedMethodImplTest$Primitives.setShort(short)) + setter: picocli.CommandLine.Model.MethodBinding(void picocli.CommandLineAnnotatedMethodImplTest$Primitives.setShort(short)) + - names: [-y] + usageHelp: false + versionHelp: false + description: [] + descriptionKey: '' + typeInfo: RuntimeTypeInfo(byte, aux=[byte], collection=false, map=false) + arity: 1 + splitRegex: '' + interactive: false + required: false + hidden: false + hideParamSyntax: false + defaultValue: 'null' + showDefaultValue: ON_DEMAND + hasInitialValue: false + initialValue: 'null' + paramLabel: '' + converters: [] + completionCandidates: null + getter: picocli.CommandLine.Model.MethodBinding(void picocli.CommandLineAnnotatedMethodImplTest$Primitives.setByte(byte)) + setter: picocli.CommandLine.Model.MethodBinding(void picocli.CommandLineAnnotatedMethodImplTest$Primitives.setByte(byte)) + PositionalParams: [] + UnmatchedArgsBindings: [] + Mixins: [] + UsageMessageSpec: + width: 80 + abbreviateSynopsis: false + hidden: false + showDefaultValues: false + sortOptions: true + requiredOptionMarker: ' ' + headerHeading: '' + header: [] + synopsisHeading: 'Usage: ' + customSynopsis: [] + descriptionHeading: '' + description: [] + parameterListHeading: '' + optionListHeading: '' + commandListHeading: 'Commands:%n' + footerHeading: '' + footer: [] + ParserSpec: + separator: '=' + endOfOptionsDelimiter: '--' + expandAtFiles: true + atFileCommentChar: '#' + overwrittenOptionsAllowed: false + unmatchedArgumentsAllowed: false + unmatchedOptionsArePositionalParams: false + stopAtUnmatched: false + stopAtPositional: false + posixClusteredShortOptionsAllowed: true + aritySatisfiedByAttachedOptionParam: false + caseInsensitiveEnumValuesAllowed: false + collectErrors: false + limitSplit: false + toggleBooleanFlags: true + Subcommands: [] diff --git a/picocli-annotation-processing-tests/src/test/resources/picocli/examples/annotatedmethods/CPrimitivesWithDefault.yaml b/picocli-annotation-processing-tests/src/test/resources/picocli/examples/annotatedmethods/CPrimitivesWithDefault.yaml new file mode 100644 index 000000000..b27e7c43c --- /dev/null +++ b/picocli-annotation-processing-tests/src/test/resources/picocli/examples/annotatedmethods/CPrimitivesWithDefault.yaml @@ -0,0 +1,195 @@ +--- +CommandSpec: + name: '
' + aliases: [] + userObject: picocli.CommandLineAnnotatedMethodImplTest$PrimitivesWithDefault@2626b418 + helpCommand: false + defaultValueProvider: null + versionProvider: null + version: [] + Options: + - names: [-b] + usageHelp: false + versionHelp: false + description: [] + descriptionKey: '' + typeInfo: RuntimeTypeInfo(boolean, aux=[boolean], collection=false, map=false) + arity: 0 + splitRegex: '' + interactive: false + required: false + hidden: false + hideParamSyntax: false + defaultValue: 'true' + showDefaultValue: ON_DEMAND + hasInitialValue: false + initialValue: 'null' + paramLabel: '' + converters: [] + completionCandidates: null + getter: picocli.CommandLine.Model.MethodBinding(void picocli.CommandLineAnnotatedMethodImplTest$PrimitivesWithDefault.setBoolean(boolean)) + setter: picocli.CommandLine.Model.MethodBinding(void picocli.CommandLineAnnotatedMethodImplTest$PrimitivesWithDefault.setBoolean(boolean)) + - names: [-d] + usageHelp: false + versionHelp: false + description: [] + descriptionKey: '' + typeInfo: RuntimeTypeInfo(double, aux=[double], collection=false, map=false) + arity: 1 + splitRegex: '' + interactive: false + required: false + hidden: false + hideParamSyntax: false + defaultValue: '16.6' + showDefaultValue: ON_DEMAND + hasInitialValue: false + initialValue: 'null' + paramLabel: '' + converters: [] + completionCandidates: null + getter: picocli.CommandLine.Model.MethodBinding(void picocli.CommandLineAnnotatedMethodImplTest$PrimitivesWithDefault.setDouble(double)) + setter: picocli.CommandLine.Model.MethodBinding(void picocli.CommandLineAnnotatedMethodImplTest$PrimitivesWithDefault.setDouble(double)) + - names: [-f] + usageHelp: false + versionHelp: false + description: [] + descriptionKey: '' + typeInfo: RuntimeTypeInfo(float, aux=[float], collection=false, map=false) + arity: 1 + splitRegex: '' + interactive: false + required: false + hidden: false + hideParamSyntax: false + defaultValue: '15.5' + showDefaultValue: ON_DEMAND + hasInitialValue: false + initialValue: 'null' + paramLabel: '' + converters: [] + completionCandidates: null + getter: picocli.CommandLine.Model.MethodBinding(void picocli.CommandLineAnnotatedMethodImplTest$PrimitivesWithDefault.setFloat(float)) + setter: picocli.CommandLine.Model.MethodBinding(void picocli.CommandLineAnnotatedMethodImplTest$PrimitivesWithDefault.setFloat(float)) + - names: [-i] + usageHelp: false + versionHelp: false + description: [] + descriptionKey: '' + typeInfo: RuntimeTypeInfo(int, aux=[int], collection=false, map=false) + arity: 1 + splitRegex: '' + interactive: false + required: false + hidden: false + hideParamSyntax: false + defaultValue: '13' + showDefaultValue: ON_DEMAND + hasInitialValue: false + initialValue: 'null' + paramLabel: '' + converters: [] + completionCandidates: null + getter: picocli.CommandLine.Model.MethodBinding(void picocli.CommandLineAnnotatedMethodImplTest$PrimitivesWithDefault.setInt(int)) + setter: picocli.CommandLine.Model.MethodBinding(void picocli.CommandLineAnnotatedMethodImplTest$PrimitivesWithDefault.setInt(int)) + - names: [-l] + usageHelp: false + versionHelp: false + description: [] + descriptionKey: '' + typeInfo: RuntimeTypeInfo(long, aux=[long], collection=false, map=false) + arity: 1 + splitRegex: '' + interactive: false + required: false + hidden: false + hideParamSyntax: false + defaultValue: '14' + showDefaultValue: ON_DEMAND + hasInitialValue: false + initialValue: 'null' + paramLabel: '' + converters: [] + completionCandidates: null + getter: picocli.CommandLine.Model.MethodBinding(void picocli.CommandLineAnnotatedMethodImplTest$PrimitivesWithDefault.setLong(long)) + setter: picocli.CommandLine.Model.MethodBinding(void picocli.CommandLineAnnotatedMethodImplTest$PrimitivesWithDefault.setLong(long)) + - names: [-s] + usageHelp: false + versionHelp: false + description: [] + descriptionKey: '' + typeInfo: RuntimeTypeInfo(short, aux=[short], collection=false, map=false) + arity: 1 + splitRegex: '' + interactive: false + required: false + hidden: false + hideParamSyntax: false + defaultValue: '12' + showDefaultValue: ON_DEMAND + hasInitialValue: false + initialValue: 'null' + paramLabel: '' + converters: [] + completionCandidates: null + getter: picocli.CommandLine.Model.MethodBinding(void picocli.CommandLineAnnotatedMethodImplTest$PrimitivesWithDefault.setShort(short)) + setter: picocli.CommandLine.Model.MethodBinding(void picocli.CommandLineAnnotatedMethodImplTest$PrimitivesWithDefault.setShort(short)) + - names: [-y] + usageHelp: false + versionHelp: false + description: [] + descriptionKey: '' + typeInfo: RuntimeTypeInfo(byte, aux=[byte], collection=false, map=false) + arity: 1 + splitRegex: '' + interactive: false + required: false + hidden: false + hideParamSyntax: false + defaultValue: '11' + showDefaultValue: ON_DEMAND + hasInitialValue: false + initialValue: 'null' + paramLabel: '' + converters: [] + completionCandidates: null + getter: picocli.CommandLine.Model.MethodBinding(void picocli.CommandLineAnnotatedMethodImplTest$PrimitivesWithDefault.setByte(byte)) + setter: picocli.CommandLine.Model.MethodBinding(void picocli.CommandLineAnnotatedMethodImplTest$PrimitivesWithDefault.setByte(byte)) + PositionalParams: [] + UnmatchedArgsBindings: [] + Mixins: [] + UsageMessageSpec: + width: 80 + abbreviateSynopsis: false + hidden: false + showDefaultValues: false + sortOptions: true + requiredOptionMarker: ' ' + headerHeading: '' + header: [] + synopsisHeading: 'Usage: ' + customSynopsis: [] + descriptionHeading: '' + description: [] + parameterListHeading: '' + optionListHeading: '' + commandListHeading: 'Commands:%n' + footerHeading: '' + footer: [] + ParserSpec: + separator: '=' + endOfOptionsDelimiter: '--' + expandAtFiles: true + atFileCommentChar: '#' + overwrittenOptionsAllowed: false + unmatchedArgumentsAllowed: false + unmatchedOptionsArePositionalParams: false + stopAtUnmatched: false + stopAtPositional: false + posixClusteredShortOptionsAllowed: true + aritySatisfiedByAttachedOptionParam: false + caseInsensitiveEnumValuesAllowed: false + collectErrors: false + limitSplit: false + toggleBooleanFlags: true + Subcommands: [] diff --git a/picocli-annotation-processing-tests/src/test/resources/picocli/examples/annotatedmethods/IFObjects.yaml b/picocli-annotation-processing-tests/src/test/resources/picocli/examples/annotatedmethods/IFObjects.yaml new file mode 100644 index 000000000..da5826300 --- /dev/null +++ b/picocli-annotation-processing-tests/src/test/resources/picocli/examples/annotatedmethods/IFObjects.yaml @@ -0,0 +1,300 @@ +--- +CommandSpec: + name: '
' + aliases: [] + userObject: null + helpCommand: false + defaultValueProvider: null + versionProvider: null + version: [] + Options: + - names: [-b] + usageHelp: false + versionHelp: false + description: [] + descriptionKey: '' + typeInfo: RuntimeTypeInfo(java.lang.Boolean, aux=[class java.lang.Boolean], collection=false, map=false) + arity: 0 + splitRegex: '' + interactive: false + required: false + hidden: false + hideParamSyntax: false + defaultValue: 'null' + showDefaultValue: ON_DEMAND + hasInitialValue: true + initialValue: 'null' + paramLabel: '' + converters: [] + completionCandidates: null + getter: picocli.CommandLine$Model$PicocliInvocationHandler$ProxyBinding@5a07e868 + setter: picocli.CommandLine$Model$PicocliInvocationHandler$ProxyBinding@5a07e868 + - names: [-bigint] + usageHelp: false + versionHelp: false + description: [] + descriptionKey: '' + typeInfo: RuntimeTypeInfo(java.math.BigInteger, aux=[class java.math.BigInteger], collection=false, map=false) + arity: 1 + splitRegex: '' + interactive: false + required: false + hidden: false + hideParamSyntax: false + defaultValue: 'null' + showDefaultValue: ON_DEMAND + hasInitialValue: true + initialValue: 'null' + paramLabel: '' + converters: [] + completionCandidates: null + getter: picocli.CommandLine$Model$PicocliInvocationHandler$ProxyBinding@76ed5528 + setter: picocli.CommandLine$Model$PicocliInvocationHandler$ProxyBinding@76ed5528 + - names: [-d] + usageHelp: false + versionHelp: false + description: [] + descriptionKey: '' + typeInfo: RuntimeTypeInfo(java.lang.Double, aux=[class java.lang.Double], collection=false, map=false) + arity: 1 + splitRegex: '' + interactive: false + required: false + hidden: false + hideParamSyntax: false + defaultValue: 'null' + showDefaultValue: ON_DEMAND + hasInitialValue: true + initialValue: 'null' + paramLabel: '' + converters: [] + completionCandidates: null + getter: picocli.CommandLine$Model$PicocliInvocationHandler$ProxyBinding@2c7b84de + setter: picocli.CommandLine$Model$PicocliInvocationHandler$ProxyBinding@2c7b84de + - names: [-f] + usageHelp: false + versionHelp: false + description: [] + descriptionKey: '' + typeInfo: RuntimeTypeInfo(java.lang.Float, aux=[class java.lang.Float], collection=false, map=false) + arity: 1 + splitRegex: '' + interactive: false + required: false + hidden: false + hideParamSyntax: false + defaultValue: 'null' + showDefaultValue: ON_DEMAND + hasInitialValue: true + initialValue: 'null' + paramLabel: '' + converters: [] + completionCandidates: null + getter: picocli.CommandLine$Model$PicocliInvocationHandler$ProxyBinding@3fee733d + setter: picocli.CommandLine$Model$PicocliInvocationHandler$ProxyBinding@3fee733d + - names: [-i] + usageHelp: false + versionHelp: false + description: [] + descriptionKey: '' + typeInfo: RuntimeTypeInfo(java.lang.Integer, aux=[class java.lang.Integer], collection=false, map=false) + arity: 1 + splitRegex: '' + interactive: false + required: false + hidden: false + hideParamSyntax: false + defaultValue: 'null' + showDefaultValue: ON_DEMAND + hasInitialValue: true + initialValue: 'null' + paramLabel: '' + converters: [] + completionCandidates: null + getter: picocli.CommandLine$Model$PicocliInvocationHandler$ProxyBinding@5acf9800 + setter: picocli.CommandLine$Model$PicocliInvocationHandler$ProxyBinding@5acf9800 + - names: [-l] + usageHelp: false + versionHelp: false + description: [] + descriptionKey: '' + typeInfo: RuntimeTypeInfo(java.lang.Long, aux=[class java.lang.Long], collection=false, map=false) + arity: 1 + splitRegex: '' + interactive: false + required: false + hidden: false + hideParamSyntax: false + defaultValue: 'null' + showDefaultValue: ON_DEMAND + hasInitialValue: true + initialValue: 'null' + paramLabel: '' + converters: [] + completionCandidates: null + getter: picocli.CommandLine$Model$PicocliInvocationHandler$ProxyBinding@4617c264 + setter: picocli.CommandLine$Model$PicocliInvocationHandler$ProxyBinding@4617c264 + - names: [-list] + usageHelp: false + versionHelp: false + description: [] + descriptionKey: '' + typeInfo: RuntimeTypeInfo(java.util.List, aux=[class java.lang.String], collection=true, map=false) + arity: 1 + splitRegex: '' + interactive: false + required: false + hidden: false + hideParamSyntax: false + defaultValue: 'null' + showDefaultValue: ON_DEMAND + hasInitialValue: true + initialValue: 'null' + paramLabel: '' + converters: [] + completionCandidates: null + getter: picocli.CommandLine$Model$PicocliInvocationHandler$ProxyBinding@36baf30c + setter: picocli.CommandLine$Model$PicocliInvocationHandler$ProxyBinding@36baf30c + - names: [-map] + usageHelp: false + versionHelp: false + description: [] + descriptionKey: '' + typeInfo: RuntimeTypeInfo(java.util.Map, aux=[class java.lang.Integer, class java.lang.Double], collection=false, map=true) + arity: 1 + splitRegex: '' + interactive: false + required: false + hidden: false + hideParamSyntax: false + defaultValue: 'null' + showDefaultValue: ON_DEMAND + hasInitialValue: true + initialValue: 'null' + paramLabel: '' + converters: [] + completionCandidates: null + getter: picocli.CommandLine$Model$PicocliInvocationHandler$ProxyBinding@7a81197d + setter: picocli.CommandLine$Model$PicocliInvocationHandler$ProxyBinding@7a81197d + - names: [-s] + usageHelp: false + versionHelp: false + description: [] + descriptionKey: '' + typeInfo: RuntimeTypeInfo(java.lang.Short, aux=[class java.lang.Short], collection=false, map=false) + arity: 1 + splitRegex: '' + interactive: false + required: false + hidden: false + hideParamSyntax: false + defaultValue: 'null' + showDefaultValue: ON_DEMAND + hasInitialValue: true + initialValue: 'null' + paramLabel: '' + converters: [] + completionCandidates: null + getter: picocli.CommandLine$Model$PicocliInvocationHandler$ProxyBinding@5ca881b5 + setter: picocli.CommandLine$Model$PicocliInvocationHandler$ProxyBinding@5ca881b5 + - names: [-set] + usageHelp: false + versionHelp: false + description: [] + descriptionKey: '' + typeInfo: RuntimeTypeInfo(java.util.SortedSet, aux=[class java.lang.Short], collection=true, map=false) + arity: 1 + splitRegex: '' + interactive: false + required: false + hidden: false + hideParamSyntax: false + defaultValue: 'null' + showDefaultValue: ON_DEMAND + hasInitialValue: true + initialValue: 'null' + paramLabel: '' + converters: [] + completionCandidates: null + getter: picocli.CommandLine$Model$PicocliInvocationHandler$ProxyBinding@24d46ca6 + setter: picocli.CommandLine$Model$PicocliInvocationHandler$ProxyBinding@24d46ca6 + - names: [-string] + usageHelp: false + versionHelp: false + description: [] + descriptionKey: '' + typeInfo: RuntimeTypeInfo(java.lang.String, aux=[class java.lang.String], collection=false, map=false) + arity: 1 + splitRegex: '' + interactive: false + required: false + hidden: false + hideParamSyntax: false + defaultValue: 'null' + showDefaultValue: ON_DEMAND + hasInitialValue: true + initialValue: 'null' + paramLabel: '' + converters: [] + completionCandidates: null + getter: picocli.CommandLine$Model$PicocliInvocationHandler$ProxyBinding@4517d9a3 + setter: picocli.CommandLine$Model$PicocliInvocationHandler$ProxyBinding@4517d9a3 + - names: [-y] + usageHelp: false + versionHelp: false + description: [] + descriptionKey: '' + typeInfo: RuntimeTypeInfo(java.lang.Byte, aux=[class java.lang.Byte], collection=false, map=false) + arity: 1 + splitRegex: '' + interactive: false + required: false + hidden: false + hideParamSyntax: false + defaultValue: 'null' + showDefaultValue: ON_DEMAND + hasInitialValue: true + initialValue: 'null' + paramLabel: '' + converters: [] + completionCandidates: null + getter: picocli.CommandLine$Model$PicocliInvocationHandler$ProxyBinding@372f7a8d + setter: picocli.CommandLine$Model$PicocliInvocationHandler$ProxyBinding@372f7a8d + PositionalParams: [] + UnmatchedArgsBindings: [] + Mixins: [] + UsageMessageSpec: + width: 80 + abbreviateSynopsis: false + hidden: false + showDefaultValues: false + sortOptions: true + requiredOptionMarker: ' ' + headerHeading: '' + header: [] + synopsisHeading: 'Usage: ' + customSynopsis: [] + descriptionHeading: '' + description: [] + parameterListHeading: '' + optionListHeading: '' + commandListHeading: 'Commands:%n' + footerHeading: '' + footer: [] + ParserSpec: + separator: '=' + endOfOptionsDelimiter: '--' + expandAtFiles: true + atFileCommentChar: '#' + overwrittenOptionsAllowed: false + unmatchedArgumentsAllowed: false + unmatchedOptionsArePositionalParams: false + stopAtUnmatched: false + stopAtPositional: false + posixClusteredShortOptionsAllowed: true + aritySatisfiedByAttachedOptionParam: false + caseInsensitiveEnumValuesAllowed: false + collectErrors: false + limitSplit: false + toggleBooleanFlags: true + Subcommands: [] diff --git a/picocli-annotation-processing-tests/src/test/resources/picocli/examples/annotatedmethods/IFObjectsWithDefault.yaml b/picocli-annotation-processing-tests/src/test/resources/picocli/examples/annotatedmethods/IFObjectsWithDefault.yaml new file mode 100644 index 000000000..26d1d3ec7 --- /dev/null +++ b/picocli-annotation-processing-tests/src/test/resources/picocli/examples/annotatedmethods/IFObjectsWithDefault.yaml @@ -0,0 +1,300 @@ +--- +CommandSpec: + name: '
' + aliases: [] + userObject: null + helpCommand: false + defaultValueProvider: null + versionProvider: null + version: [] + Options: + - names: [-b] + usageHelp: false + versionHelp: false + description: [] + descriptionKey: '' + typeInfo: RuntimeTypeInfo(java.lang.Boolean, aux=[class java.lang.Boolean], collection=false, map=false) + arity: 0 + splitRegex: '' + interactive: false + required: false + hidden: false + hideParamSyntax: false + defaultValue: 'true' + showDefaultValue: ON_DEMAND + hasInitialValue: true + initialValue: 'null' + paramLabel: '' + converters: [] + completionCandidates: null + getter: picocli.CommandLine$Model$PicocliInvocationHandler$ProxyBinding@5a07e868 + setter: picocli.CommandLine$Model$PicocliInvocationHandler$ProxyBinding@5a07e868 + - names: [-bigint] + usageHelp: false + versionHelp: false + description: [] + descriptionKey: '' + typeInfo: RuntimeTypeInfo(java.math.BigDecimal, aux=[class java.math.BigDecimal], collection=false, map=false) + arity: 1 + splitRegex: '' + interactive: false + required: false + hidden: false + hideParamSyntax: false + defaultValue: '16.6' + showDefaultValue: ON_DEMAND + hasInitialValue: true + initialValue: 'null' + paramLabel: '' + converters: [] + completionCandidates: null + getter: picocli.CommandLine$Model$PicocliInvocationHandler$ProxyBinding@76ed5528 + setter: picocli.CommandLine$Model$PicocliInvocationHandler$ProxyBinding@76ed5528 + - names: [-d] + usageHelp: false + versionHelp: false + description: [] + descriptionKey: '' + typeInfo: RuntimeTypeInfo(java.lang.Double, aux=[class java.lang.Double], collection=false, map=false) + arity: 1 + splitRegex: '' + interactive: false + required: false + hidden: false + hideParamSyntax: false + defaultValue: '15.5' + showDefaultValue: ON_DEMAND + hasInitialValue: true + initialValue: 'null' + paramLabel: '' + converters: [] + completionCandidates: null + getter: picocli.CommandLine$Model$PicocliInvocationHandler$ProxyBinding@2c7b84de + setter: picocli.CommandLine$Model$PicocliInvocationHandler$ProxyBinding@2c7b84de + - names: [-f] + usageHelp: false + versionHelp: false + description: [] + descriptionKey: '' + typeInfo: RuntimeTypeInfo(java.lang.Float, aux=[class java.lang.Float], collection=false, map=false) + arity: 1 + splitRegex: '' + interactive: false + required: false + hidden: false + hideParamSyntax: false + defaultValue: '14.4' + showDefaultValue: ON_DEMAND + hasInitialValue: true + initialValue: 'null' + paramLabel: '' + converters: [] + completionCandidates: null + getter: picocli.CommandLine$Model$PicocliInvocationHandler$ProxyBinding@3fee733d + setter: picocli.CommandLine$Model$PicocliInvocationHandler$ProxyBinding@3fee733d + - names: [-i] + usageHelp: false + versionHelp: false + description: [] + descriptionKey: '' + typeInfo: RuntimeTypeInfo(java.lang.Integer, aux=[class java.lang.Integer], collection=false, map=false) + arity: 1 + splitRegex: '' + interactive: false + required: false + hidden: false + hideParamSyntax: false + defaultValue: '12' + showDefaultValue: ON_DEMAND + hasInitialValue: true + initialValue: 'null' + paramLabel: '' + converters: [] + completionCandidates: null + getter: picocli.CommandLine$Model$PicocliInvocationHandler$ProxyBinding@5acf9800 + setter: picocli.CommandLine$Model$PicocliInvocationHandler$ProxyBinding@5acf9800 + - names: [-l] + usageHelp: false + versionHelp: false + description: [] + descriptionKey: '' + typeInfo: RuntimeTypeInfo(java.lang.Long, aux=[class java.lang.Long], collection=false, map=false) + arity: 1 + splitRegex: '' + interactive: false + required: false + hidden: false + hideParamSyntax: false + defaultValue: '13' + showDefaultValue: ON_DEMAND + hasInitialValue: true + initialValue: 'null' + paramLabel: '' + converters: [] + completionCandidates: null + getter: picocli.CommandLine$Model$PicocliInvocationHandler$ProxyBinding@4617c264 + setter: picocli.CommandLine$Model$PicocliInvocationHandler$ProxyBinding@4617c264 + - names: [-list] + usageHelp: false + versionHelp: false + description: [] + descriptionKey: '' + typeInfo: RuntimeTypeInfo(java.util.List, aux=[class java.lang.String], collection=true, map=false) + arity: 1 + splitRegex: ',' + interactive: false + required: false + hidden: false + hideParamSyntax: false + defaultValue: 'a,b,c' + showDefaultValue: ON_DEMAND + hasInitialValue: true + initialValue: 'null' + paramLabel: '' + converters: [] + completionCandidates: null + getter: picocli.CommandLine$Model$PicocliInvocationHandler$ProxyBinding@36baf30c + setter: picocli.CommandLine$Model$PicocliInvocationHandler$ProxyBinding@36baf30c + - names: [-map] + usageHelp: false + versionHelp: false + description: [] + descriptionKey: '' + typeInfo: RuntimeTypeInfo(java.util.Map, aux=[class java.lang.Integer, class java.lang.Double], collection=false, map=true) + arity: 1 + splitRegex: ',' + interactive: false + required: false + hidden: false + hideParamSyntax: false + defaultValue: '1=1,2=2,3=3' + showDefaultValue: ON_DEMAND + hasInitialValue: true + initialValue: 'null' + paramLabel: '' + converters: [] + completionCandidates: null + getter: picocli.CommandLine$Model$PicocliInvocationHandler$ProxyBinding@7a81197d + setter: picocli.CommandLine$Model$PicocliInvocationHandler$ProxyBinding@7a81197d + - names: [-s] + usageHelp: false + versionHelp: false + description: [] + descriptionKey: '' + typeInfo: RuntimeTypeInfo(java.lang.Short, aux=[class java.lang.Short], collection=false, map=false) + arity: 1 + splitRegex: '' + interactive: false + required: false + hidden: false + hideParamSyntax: false + defaultValue: '11' + showDefaultValue: ON_DEMAND + hasInitialValue: true + initialValue: 'null' + paramLabel: '' + converters: [] + completionCandidates: null + getter: picocli.CommandLine$Model$PicocliInvocationHandler$ProxyBinding@5ca881b5 + setter: picocli.CommandLine$Model$PicocliInvocationHandler$ProxyBinding@5ca881b5 + - names: [-set] + usageHelp: false + versionHelp: false + description: [] + descriptionKey: '' + typeInfo: RuntimeTypeInfo(java.util.SortedSet, aux=[class java.lang.Short], collection=true, map=false) + arity: 1 + splitRegex: ',' + interactive: false + required: false + hidden: false + hideParamSyntax: false + defaultValue: '1,2,3' + showDefaultValue: ON_DEMAND + hasInitialValue: true + initialValue: 'null' + paramLabel: '' + converters: [] + completionCandidates: null + getter: picocli.CommandLine$Model$PicocliInvocationHandler$ProxyBinding@24d46ca6 + setter: picocli.CommandLine$Model$PicocliInvocationHandler$ProxyBinding@24d46ca6 + - names: [-string] + usageHelp: false + versionHelp: false + description: [] + descriptionKey: '' + typeInfo: RuntimeTypeInfo(java.lang.String, aux=[class java.lang.String], collection=false, map=false) + arity: 1 + splitRegex: '' + interactive: false + required: false + hidden: false + hideParamSyntax: false + defaultValue: 'abc' + showDefaultValue: ON_DEMAND + hasInitialValue: true + initialValue: 'null' + paramLabel: '' + converters: [] + completionCandidates: null + getter: picocli.CommandLine$Model$PicocliInvocationHandler$ProxyBinding@4517d9a3 + setter: picocli.CommandLine$Model$PicocliInvocationHandler$ProxyBinding@4517d9a3 + - names: [-y] + usageHelp: false + versionHelp: false + description: [] + descriptionKey: '' + typeInfo: RuntimeTypeInfo(java.lang.Byte, aux=[class java.lang.Byte], collection=false, map=false) + arity: 1 + splitRegex: '' + interactive: false + required: false + hidden: false + hideParamSyntax: false + defaultValue: '123' + showDefaultValue: ON_DEMAND + hasInitialValue: true + initialValue: 'null' + paramLabel: '' + converters: [] + completionCandidates: null + getter: picocli.CommandLine$Model$PicocliInvocationHandler$ProxyBinding@372f7a8d + setter: picocli.CommandLine$Model$PicocliInvocationHandler$ProxyBinding@372f7a8d + PositionalParams: [] + UnmatchedArgsBindings: [] + Mixins: [] + UsageMessageSpec: + width: 80 + abbreviateSynopsis: false + hidden: false + showDefaultValues: false + sortOptions: true + requiredOptionMarker: ' ' + headerHeading: '' + header: [] + synopsisHeading: 'Usage: ' + customSynopsis: [] + descriptionHeading: '' + description: [] + parameterListHeading: '' + optionListHeading: '' + commandListHeading: 'Commands:%n' + footerHeading: '' + footer: [] + ParserSpec: + separator: '=' + endOfOptionsDelimiter: '--' + expandAtFiles: true + atFileCommentChar: '#' + overwrittenOptionsAllowed: false + unmatchedArgumentsAllowed: false + unmatchedOptionsArePositionalParams: false + stopAtUnmatched: false + stopAtPositional: false + posixClusteredShortOptionsAllowed: true + aritySatisfiedByAttachedOptionParam: false + caseInsensitiveEnumValuesAllowed: false + collectErrors: false + limitSplit: false + toggleBooleanFlags: true + Subcommands: [] diff --git a/picocli-annotation-processing-tests/src/test/resources/picocli/examples/annotatedmethods/IFPrimitives.yaml b/picocli-annotation-processing-tests/src/test/resources/picocli/examples/annotatedmethods/IFPrimitives.yaml new file mode 100644 index 000000000..652762394 --- /dev/null +++ b/picocli-annotation-processing-tests/src/test/resources/picocli/examples/annotatedmethods/IFPrimitives.yaml @@ -0,0 +1,195 @@ +--- +CommandSpec: + name: '
' + aliases: [] + userObject: null + helpCommand: false + defaultValueProvider: null + versionProvider: null + version: [] + Options: + - names: [-b] + usageHelp: false + versionHelp: false + description: [] + descriptionKey: '' + typeInfo: RuntimeTypeInfo(boolean, aux=[boolean], collection=false, map=false) + arity: 0 + splitRegex: '' + interactive: false + required: false + hidden: false + hideParamSyntax: false + defaultValue: 'null' + showDefaultValue: ON_DEMAND + hasInitialValue: true + initialValue: 'false' + paramLabel: '' + converters: [] + completionCandidates: null + getter: picocli.CommandLine$Model$PicocliInvocationHandler$ProxyBinding@5a07e868 + setter: picocli.CommandLine$Model$PicocliInvocationHandler$ProxyBinding@5a07e868 + - names: [-d] + usageHelp: false + versionHelp: false + description: [] + descriptionKey: '' + typeInfo: RuntimeTypeInfo(double, aux=[double], collection=false, map=false) + arity: 1 + splitRegex: '' + interactive: false + required: false + hidden: false + hideParamSyntax: false + defaultValue: 'null' + showDefaultValue: ON_DEMAND + hasInitialValue: true + initialValue: '0.0' + paramLabel: '' + converters: [] + completionCandidates: null + getter: picocli.CommandLine$Model$PicocliInvocationHandler$ProxyBinding@76ed5528 + setter: picocli.CommandLine$Model$PicocliInvocationHandler$ProxyBinding@76ed5528 + - names: [-f] + usageHelp: false + versionHelp: false + description: [] + descriptionKey: '' + typeInfo: RuntimeTypeInfo(float, aux=[float], collection=false, map=false) + arity: 1 + splitRegex: '' + interactive: false + required: false + hidden: false + hideParamSyntax: false + defaultValue: 'null' + showDefaultValue: ON_DEMAND + hasInitialValue: true + initialValue: '0.0' + paramLabel: '' + converters: [] + completionCandidates: null + getter: picocli.CommandLine$Model$PicocliInvocationHandler$ProxyBinding@2c7b84de + setter: picocli.CommandLine$Model$PicocliInvocationHandler$ProxyBinding@2c7b84de + - names: [-i] + usageHelp: false + versionHelp: false + description: [] + descriptionKey: '' + typeInfo: RuntimeTypeInfo(int, aux=[int], collection=false, map=false) + arity: 1 + splitRegex: '' + interactive: false + required: false + hidden: false + hideParamSyntax: false + defaultValue: 'null' + showDefaultValue: ON_DEMAND + hasInitialValue: true + initialValue: '0' + paramLabel: '' + converters: [] + completionCandidates: null + getter: picocli.CommandLine$Model$PicocliInvocationHandler$ProxyBinding@3fee733d + setter: picocli.CommandLine$Model$PicocliInvocationHandler$ProxyBinding@3fee733d + - names: [-l] + usageHelp: false + versionHelp: false + description: [] + descriptionKey: '' + typeInfo: RuntimeTypeInfo(long, aux=[long], collection=false, map=false) + arity: 1 + splitRegex: '' + interactive: false + required: false + hidden: false + hideParamSyntax: false + defaultValue: 'null' + showDefaultValue: ON_DEMAND + hasInitialValue: true + initialValue: '0' + paramLabel: '' + converters: [] + completionCandidates: null + getter: picocli.CommandLine$Model$PicocliInvocationHandler$ProxyBinding@5acf9800 + setter: picocli.CommandLine$Model$PicocliInvocationHandler$ProxyBinding@5acf9800 + - names: [-s] + usageHelp: false + versionHelp: false + description: [] + descriptionKey: '' + typeInfo: RuntimeTypeInfo(short, aux=[short], collection=false, map=false) + arity: 1 + splitRegex: '' + interactive: false + required: false + hidden: false + hideParamSyntax: false + defaultValue: 'null' + showDefaultValue: ON_DEMAND + hasInitialValue: true + initialValue: '0' + paramLabel: '' + converters: [] + completionCandidates: null + getter: picocli.CommandLine$Model$PicocliInvocationHandler$ProxyBinding@4617c264 + setter: picocli.CommandLine$Model$PicocliInvocationHandler$ProxyBinding@4617c264 + - names: [-y] + usageHelp: false + versionHelp: false + description: [] + descriptionKey: '' + typeInfo: RuntimeTypeInfo(byte, aux=[byte], collection=false, map=false) + arity: 1 + splitRegex: '' + interactive: false + required: false + hidden: false + hideParamSyntax: false + defaultValue: 'null' + showDefaultValue: ON_DEMAND + hasInitialValue: true + initialValue: '0' + paramLabel: '' + converters: [] + completionCandidates: null + getter: picocli.CommandLine$Model$PicocliInvocationHandler$ProxyBinding@36baf30c + setter: picocli.CommandLine$Model$PicocliInvocationHandler$ProxyBinding@36baf30c + PositionalParams: [] + UnmatchedArgsBindings: [] + Mixins: [] + UsageMessageSpec: + width: 80 + abbreviateSynopsis: false + hidden: false + showDefaultValues: false + sortOptions: true + requiredOptionMarker: ' ' + headerHeading: '' + header: [] + synopsisHeading: 'Usage: ' + customSynopsis: [] + descriptionHeading: '' + description: [] + parameterListHeading: '' + optionListHeading: '' + commandListHeading: 'Commands:%n' + footerHeading: '' + footer: [] + ParserSpec: + separator: '=' + endOfOptionsDelimiter: '--' + expandAtFiles: true + atFileCommentChar: '#' + overwrittenOptionsAllowed: false + unmatchedArgumentsAllowed: false + unmatchedOptionsArePositionalParams: false + stopAtUnmatched: false + stopAtPositional: false + posixClusteredShortOptionsAllowed: true + aritySatisfiedByAttachedOptionParam: false + caseInsensitiveEnumValuesAllowed: false + collectErrors: false + limitSplit: false + toggleBooleanFlags: true + Subcommands: [] diff --git a/picocli-annotation-processing-tests/src/test/resources/picocli/examples/annotatedmethods/IFPrimitivesWithDefault.yaml b/picocli-annotation-processing-tests/src/test/resources/picocli/examples/annotatedmethods/IFPrimitivesWithDefault.yaml new file mode 100644 index 000000000..290021cb8 --- /dev/null +++ b/picocli-annotation-processing-tests/src/test/resources/picocli/examples/annotatedmethods/IFPrimitivesWithDefault.yaml @@ -0,0 +1,195 @@ +--- +CommandSpec: + name: '
' + aliases: [] + userObject: null + helpCommand: false + defaultValueProvider: null + versionProvider: null + version: [] + Options: + - names: [-b] + usageHelp: false + versionHelp: false + description: [] + descriptionKey: '' + typeInfo: RuntimeTypeInfo(boolean, aux=[boolean], collection=false, map=false) + arity: 0 + splitRegex: '' + interactive: false + required: false + hidden: false + hideParamSyntax: false + defaultValue: 'true' + showDefaultValue: ON_DEMAND + hasInitialValue: true + initialValue: 'false' + paramLabel: '' + converters: [] + completionCandidates: null + getter: picocli.CommandLine$Model$PicocliInvocationHandler$ProxyBinding@5a07e868 + setter: picocli.CommandLine$Model$PicocliInvocationHandler$ProxyBinding@5a07e868 + - names: [-d] + usageHelp: false + versionHelp: false + description: [] + descriptionKey: '' + typeInfo: RuntimeTypeInfo(double, aux=[double], collection=false, map=false) + arity: 1 + splitRegex: '' + interactive: false + required: false + hidden: false + hideParamSyntax: false + defaultValue: '16.6' + showDefaultValue: ON_DEMAND + hasInitialValue: true + initialValue: '0.0' + paramLabel: '' + converters: [] + completionCandidates: null + getter: picocli.CommandLine$Model$PicocliInvocationHandler$ProxyBinding@76ed5528 + setter: picocli.CommandLine$Model$PicocliInvocationHandler$ProxyBinding@76ed5528 + - names: [-f] + usageHelp: false + versionHelp: false + description: [] + descriptionKey: '' + typeInfo: RuntimeTypeInfo(float, aux=[float], collection=false, map=false) + arity: 1 + splitRegex: '' + interactive: false + required: false + hidden: false + hideParamSyntax: false + defaultValue: '15.5' + showDefaultValue: ON_DEMAND + hasInitialValue: true + initialValue: '0.0' + paramLabel: '' + converters: [] + completionCandidates: null + getter: picocli.CommandLine$Model$PicocliInvocationHandler$ProxyBinding@2c7b84de + setter: picocli.CommandLine$Model$PicocliInvocationHandler$ProxyBinding@2c7b84de + - names: [-i] + usageHelp: false + versionHelp: false + description: [] + descriptionKey: '' + typeInfo: RuntimeTypeInfo(int, aux=[int], collection=false, map=false) + arity: 1 + splitRegex: '' + interactive: false + required: false + hidden: false + hideParamSyntax: false + defaultValue: '13' + showDefaultValue: ON_DEMAND + hasInitialValue: true + initialValue: '0' + paramLabel: '' + converters: [] + completionCandidates: null + getter: picocli.CommandLine$Model$PicocliInvocationHandler$ProxyBinding@3fee733d + setter: picocli.CommandLine$Model$PicocliInvocationHandler$ProxyBinding@3fee733d + - names: [-l] + usageHelp: false + versionHelp: false + description: [] + descriptionKey: '' + typeInfo: RuntimeTypeInfo(long, aux=[long], collection=false, map=false) + arity: 1 + splitRegex: '' + interactive: false + required: false + hidden: false + hideParamSyntax: false + defaultValue: '14' + showDefaultValue: ON_DEMAND + hasInitialValue: true + initialValue: '0' + paramLabel: '' + converters: [] + completionCandidates: null + getter: picocli.CommandLine$Model$PicocliInvocationHandler$ProxyBinding@5acf9800 + setter: picocli.CommandLine$Model$PicocliInvocationHandler$ProxyBinding@5acf9800 + - names: [-s] + usageHelp: false + versionHelp: false + description: [] + descriptionKey: '' + typeInfo: RuntimeTypeInfo(short, aux=[short], collection=false, map=false) + arity: 1 + splitRegex: '' + interactive: false + required: false + hidden: false + hideParamSyntax: false + defaultValue: '12' + showDefaultValue: ON_DEMAND + hasInitialValue: true + initialValue: '0' + paramLabel: '' + converters: [] + completionCandidates: null + getter: picocli.CommandLine$Model$PicocliInvocationHandler$ProxyBinding@4617c264 + setter: picocli.CommandLine$Model$PicocliInvocationHandler$ProxyBinding@4617c264 + - names: [-y] + usageHelp: false + versionHelp: false + description: [] + descriptionKey: '' + typeInfo: RuntimeTypeInfo(byte, aux=[byte], collection=false, map=false) + arity: 1 + splitRegex: '' + interactive: false + required: false + hidden: false + hideParamSyntax: false + defaultValue: '11' + showDefaultValue: ON_DEMAND + hasInitialValue: true + initialValue: '0' + paramLabel: '' + converters: [] + completionCandidates: null + getter: picocli.CommandLine$Model$PicocliInvocationHandler$ProxyBinding@36baf30c + setter: picocli.CommandLine$Model$PicocliInvocationHandler$ProxyBinding@36baf30c + PositionalParams: [] + UnmatchedArgsBindings: [] + Mixins: [] + UsageMessageSpec: + width: 80 + abbreviateSynopsis: false + hidden: false + showDefaultValues: false + sortOptions: true + requiredOptionMarker: ' ' + headerHeading: '' + header: [] + synopsisHeading: 'Usage: ' + customSynopsis: [] + descriptionHeading: '' + description: [] + parameterListHeading: '' + optionListHeading: '' + commandListHeading: 'Commands:%n' + footerHeading: '' + footer: [] + ParserSpec: + separator: '=' + endOfOptionsDelimiter: '--' + expandAtFiles: true + atFileCommentChar: '#' + overwrittenOptionsAllowed: false + unmatchedArgumentsAllowed: false + unmatchedOptionsArePositionalParams: false + stopAtUnmatched: false + stopAtPositional: false + posixClusteredShortOptionsAllowed: true + aritySatisfiedByAttachedOptionParam: false + caseInsensitiveEnumValuesAllowed: false + collectErrors: false + limitSplit: false + toggleBooleanFlags: true + Subcommands: [] diff --git a/picocli-annotation-processing-tests/src/test/resources/picocli/examples/annotatedmethods/InvalidAnnotatedInterfaceMethodOptions.java b/picocli-annotation-processing-tests/src/test/resources/picocli/examples/annotatedmethods/InvalidAnnotatedInterfaceMethodOptions.java new file mode 100644 index 000000000..6c411a309 --- /dev/null +++ b/picocli-annotation-processing-tests/src/test/resources/picocli/examples/annotatedmethods/InvalidAnnotatedInterfaceMethodOptions.java @@ -0,0 +1,32 @@ +package picocli.examples.annotatedmethods; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.SortedSet; +import java.util.TreeSet; + +import static picocli.CommandLine.*; + +public class InvalidAnnotatedInterfaceMethodOptions { + + // Invalid picocli annotation on interface fields + interface InvalidAnnotatedStringOrPrimitiveFields { + @Option(names = "-i") + int anInt = 0; + + @Option(names = "-s") + String aString = null; + } + + // Invalid picocli annotation on interface field + interface InvalidAnnotatedMutableFields { + @Option(names = "-s") + final List aList = new ArrayList(); + } +} diff --git a/picocli-annotation-processing-tests/src/test/resources/picocli/examples/messages/CommandWithBundle.java b/picocli-annotation-processing-tests/src/test/resources/picocli/examples/messages/CommandWithBundle.java new file mode 100644 index 000000000..afce65651 --- /dev/null +++ b/picocli-annotation-processing-tests/src/test/resources/picocli/examples/messages/CommandWithBundle.java @@ -0,0 +1,8 @@ +package picocli.example.messages.CommandWithBundle; + +import picocli.CommandLine.Command; + +@Command(resourceBundle = "command-method-demo") +public class CommandWithBundle { + +} \ No newline at end of file diff --git a/picocli-annotation-processing-tests/src/test/resources/picocli/examples/messages/CommandWithBundle.yaml b/picocli-annotation-processing-tests/src/test/resources/picocli/examples/messages/CommandWithBundle.yaml new file mode 100644 index 000000000..31978f06e --- /dev/null +++ b/picocli-annotation-processing-tests/src/test/resources/picocli/examples/messages/CommandWithBundle.yaml @@ -0,0 +1,65 @@ +--- +CommandSpec: + name: '
' + aliases: [] + userObject: picocli.example.messages.CommandWithBundle.CommandWithBundle + helpCommand: false + defaultValueProvider: null + versionProvider: null + version: [] + Options: [] + PositionalParams: [] + UnmatchedArgsBindings: [] + Mixins: [] + UsageMessageSpec: + width: 80 + abbreviateSynopsis: false + hidden: false + showDefaultValues: false + sortOptions: true + requiredOptionMarker: ' ' + headerHeading: 'the headerHeading' + header: [header0, header1, header2, header3] + synopsisHeading: 'Usage: ' + customSynopsis: [] + descriptionHeading: 'the descriptionHeading' + description: [Version control system, description0, description1, description2, description3] + parameterListHeading: '' + optionListHeading: '' + commandListHeading: 'Commands:%n' + footerHeading: 'the footerHeading' + footer: [footer0, footer1, footer2, footer3] + ParserSpec: + separator: '=' + endOfOptionsDelimiter: '--' + expandAtFiles: true + atFileCommentChar: '#' + overwrittenOptionsAllowed: false + unmatchedArgumentsAllowed: false + unmatchedOptionsArePositionalParams: false + stopAtUnmatched: false + stopAtPositional: false + posixClusteredShortOptionsAllowed: true + aritySatisfiedByAttachedOptionParam: false + caseInsensitiveEnumValuesAllowed: false + collectErrors: false + limitSplit: false + toggleBooleanFlags: true + ResourceBundle: + usage.header.0: 'header0' + usage.header.1: 'header1' + usage.headerHeading: 'the headerHeading' + usage.descriptionHeading: 'the descriptionHeading' + usage.header.2: 'header2' + usage.header.3: 'header3' + usage.footerHeading: 'the footerHeading' + usage.description.3: 'description3' + usage.footer.1: 'footer1' + usage.footer.0: 'footer0' + usage.description: 'Version control system' + usage.footer.3: 'footer3' + usage.description.0: 'description0' + usage.footer.2: 'footer2' + usage.description.1: 'description1' + usage.description.2: 'description2' + Subcommands: [] diff --git a/picocli-annotation-processing-tests/src/test/resources/picocli/examples/mixin/CommandWithMixin.java b/picocli-annotation-processing-tests/src/test/resources/picocli/examples/mixin/CommandWithMixin.java new file mode 100644 index 000000000..7cbb26c2a --- /dev/null +++ b/picocli-annotation-processing-tests/src/test/resources/picocli/examples/mixin/CommandWithMixin.java @@ -0,0 +1,29 @@ +package picocli.examples.mixin; + +import picocli.CommandLine; +import picocli.CommandLine.Command; +import picocli.CommandLine.Mixin; +import picocli.CommandLine.Option; +import picocli.CommandLine.Parameters; + +@Command(name = "mixee", description = "This command has a footer and an option mixed in") +public class CommandWithMixin { + @Mixin + CommonOption commonOption = new CommonOption(); + + @Option(names = "-y", description = "command option") + int y; + + @Command + public void doit(@Mixin CommonOption commonOptionParam, + @Option(names = "-z") int z, + @Parameters String arg0, + String arg1) {} + + public static void main(String[] args) { + CommandWithMixin cmd = new CommandWithMixin(); + new CommandLine(cmd).parseArgs("-x", "3", "-y", "4"); + + System.out.printf("x=%s, y=%s%n", cmd.commonOption.x, cmd.y); + } +} diff --git a/picocli-annotation-processing-tests/src/test/resources/picocli/examples/mixin/CommandWithMixin.yaml b/picocli-annotation-processing-tests/src/test/resources/picocli/examples/mixin/CommandWithMixin.yaml new file mode 100644 index 000000000..bf68b8d8f --- /dev/null +++ b/picocli-annotation-processing-tests/src/test/resources/picocli/examples/mixin/CommandWithMixin.yaml @@ -0,0 +1,353 @@ +--- +CommandSpec: + name: 'mixee' + aliases: [] + userObject: picocli.examples.mixin.CommandWithMixin@31221be2 + helpCommand: false + defaultValueProvider: null + versionProvider: null + version: [] + Options: + - names: [-x] + usageHelp: false + versionHelp: false + description: [reusable option you want in many commands] + descriptionKey: '' + typeInfo: RuntimeTypeInfo(int, aux=[int], collection=false, map=false) + arity: 1 + splitRegex: '' + interactive: false + required: false + hidden: false + hideParamSyntax: false + defaultValue: 'null' + showDefaultValue: ON_DEMAND + hasInitialValue: true + initialValue: '0' + paramLabel: '' + converters: [] + completionCandidates: null + getter: picocli.CommandLine.Model.FieldBinding(int picocli.examples.mixin.CommonOption.x) + setter: picocli.CommandLine.Model.FieldBinding(int picocli.examples.mixin.CommonOption.x) + - names: [-y] + usageHelp: false + versionHelp: false + description: [command option] + descriptionKey: '' + typeInfo: RuntimeTypeInfo(int, aux=[int], collection=false, map=false) + arity: 1 + splitRegex: '' + interactive: false + required: false + hidden: false + hideParamSyntax: false + defaultValue: 'null' + showDefaultValue: ON_DEMAND + hasInitialValue: true + initialValue: '0' + paramLabel: '' + converters: [] + completionCandidates: null + getter: picocli.CommandLine.Model.FieldBinding(int picocli.examples.mixin.CommandWithMixin.y) + setter: picocli.CommandLine.Model.FieldBinding(int picocli.examples.mixin.CommandWithMixin.y) + PositionalParams: [] + UnmatchedArgsBindings: [] + Mixins: + # commonOption + - name: '
' + aliases: [] + userObject: picocli.examples.mixin.CommonOption@377dca04 + helpCommand: false + defaultValueProvider: null + versionProvider: null + version: [] + Options: + - names: [-x] + usageHelp: false + versionHelp: false + description: [reusable option you want in many commands] + descriptionKey: '' + typeInfo: RuntimeTypeInfo(int, aux=[int], collection=false, map=false) + arity: 1 + splitRegex: '' + interactive: false + required: false + hidden: false + hideParamSyntax: false + defaultValue: 'null' + showDefaultValue: ON_DEMAND + hasInitialValue: true + initialValue: '0' + paramLabel: '' + converters: [] + completionCandidates: null + getter: picocli.CommandLine.Model.FieldBinding(int picocli.examples.mixin.CommonOption.x) + setter: picocli.CommandLine.Model.FieldBinding(int picocli.examples.mixin.CommonOption.x) + PositionalParams: [] + UnmatchedArgsBindings: [] + Mixins: [] + UsageMessageSpec: + width: 80 + abbreviateSynopsis: false + hidden: false + showDefaultValues: false + sortOptions: true + requiredOptionMarker: ' ' + headerHeading: '' + header: [] + synopsisHeading: 'Usage: ' + customSynopsis: [] + descriptionHeading: '' + description: [] + parameterListHeading: '' + optionListHeading: '' + commandListHeading: 'Commands:%n' + footerHeading: '' + footer: [Common footer] + ParserSpec: + separator: '=' + endOfOptionsDelimiter: '--' + expandAtFiles: true + atFileCommentChar: '#' + overwrittenOptionsAllowed: false + unmatchedArgumentsAllowed: false + unmatchedOptionsArePositionalParams: false + stopAtUnmatched: false + stopAtPositional: false + posixClusteredShortOptionsAllowed: true + aritySatisfiedByAttachedOptionParam: false + caseInsensitiveEnumValuesAllowed: false + collectErrors: false + limitSplit: false + toggleBooleanFlags: true + Subcommands: [] + UsageMessageSpec: + width: 80 + abbreviateSynopsis: false + hidden: false + showDefaultValues: false + sortOptions: true + requiredOptionMarker: ' ' + headerHeading: '' + header: [] + synopsisHeading: 'Usage: ' + customSynopsis: [] + descriptionHeading: '' + description: [This command has a footer and an option mixed in] + parameterListHeading: '' + optionListHeading: '' + commandListHeading: 'Commands:%n' + footerHeading: '' + footer: [Common footer] + ParserSpec: + separator: '=' + endOfOptionsDelimiter: '--' + expandAtFiles: true + atFileCommentChar: '#' + overwrittenOptionsAllowed: false + unmatchedArgumentsAllowed: false + unmatchedOptionsArePositionalParams: false + stopAtUnmatched: false + stopAtPositional: false + posixClusteredShortOptionsAllowed: true + aritySatisfiedByAttachedOptionParam: false + caseInsensitiveEnumValuesAllowed: false + collectErrors: false + limitSplit: false + toggleBooleanFlags: true + Subcommands: + # doit + - name: 'doit' + aliases: [] + userObject: public void picocli.examples.mixin.CommandWithMixin.doit(picocli.examples.mixin.CommonOption,int,java.lang.String,java.lang.String) + helpCommand: false + defaultValueProvider: null + versionProvider: null + version: [] + Options: + - names: [-x] + usageHelp: false + versionHelp: false + description: [reusable option you want in many commands] + descriptionKey: '' + typeInfo: RuntimeTypeInfo(int, aux=[int], collection=false, map=false) + arity: 1 + splitRegex: '' + interactive: false + required: false + hidden: false + hideParamSyntax: false + defaultValue: 'null' + showDefaultValue: ON_DEMAND + hasInitialValue: true + initialValue: '0' + paramLabel: '' + converters: [] + completionCandidates: null + getter: picocli.CommandLine.Model.FieldBinding(int picocli.examples.mixin.CommonOption.x) + setter: picocli.CommandLine.Model.FieldBinding(int picocli.examples.mixin.CommonOption.x) + - names: [-z] + usageHelp: false + versionHelp: false + description: [] + descriptionKey: '' + typeInfo: RuntimeTypeInfo(int, aux=[int], collection=false, map=false) + arity: 1 + splitRegex: '' + interactive: false + required: false + hidden: false + hideParamSyntax: false + defaultValue: 'null' + showDefaultValue: ON_DEMAND + hasInitialValue: true + initialValue: '0' + paramLabel: '' + converters: [] + completionCandidates: null + getter: picocli.CommandLine.Model.ObjectBinding(value=0) + setter: picocli.CommandLine.Model.ObjectBinding(value=0) + PositionalParams: + - index: 0 + description: [] + descriptionKey: '' + typeInfo: RuntimeTypeInfo(java.lang.String, aux=[class java.lang.String], collection=false, map=false) + arity: 1 + splitRegex: '' + interactive: false + required: true + hidden: false + hideParamSyntax: false + defaultValue: 'null' + showDefaultValue: ON_DEMAND + hasInitialValue: false + initialValue: 'null' + paramLabel: '' + converters: [] + completionCandidates: null + getter: picocli.CommandLine.Model.ObjectBinding(value=null) + setter: picocli.CommandLine.Model.ObjectBinding(value=null) + - index: 1 + description: [] + descriptionKey: 'null' + typeInfo: RuntimeTypeInfo(java.lang.String, aux=[class java.lang.String], collection=false, map=false) + arity: 1 + splitRegex: '' + interactive: false + required: true + hidden: false + hideParamSyntax: false + defaultValue: 'null' + showDefaultValue: ON_DEMAND + hasInitialValue: false + initialValue: 'null' + paramLabel: '' + converters: [] + completionCandidates: null + getter: picocli.CommandLine.Model.ObjectBinding(value=null) + setter: picocli.CommandLine.Model.ObjectBinding(value=null) + UnmatchedArgsBindings: [] + Mixins: + # arg0 + - name: '
' + aliases: [] + userObject: picocli.examples.mixin.CommonOption@728938a9 + helpCommand: false + defaultValueProvider: null + versionProvider: null + version: [] + Options: + - names: [-x] + usageHelp: false + versionHelp: false + description: [reusable option you want in many commands] + descriptionKey: '' + typeInfo: RuntimeTypeInfo(int, aux=[int], collection=false, map=false) + arity: 1 + splitRegex: '' + interactive: false + required: false + hidden: false + hideParamSyntax: false + defaultValue: 'null' + showDefaultValue: ON_DEMAND + hasInitialValue: true + initialValue: '0' + paramLabel: '' + converters: [] + completionCandidates: null + getter: picocli.CommandLine.Model.FieldBinding(int picocli.examples.mixin.CommonOption.x) + setter: picocli.CommandLine.Model.FieldBinding(int picocli.examples.mixin.CommonOption.x) + PositionalParams: [] + UnmatchedArgsBindings: [] + Mixins: [] + UsageMessageSpec: + width: 80 + abbreviateSynopsis: false + hidden: false + showDefaultValues: false + sortOptions: true + requiredOptionMarker: ' ' + headerHeading: '' + header: [] + synopsisHeading: 'Usage: ' + customSynopsis: [] + descriptionHeading: '' + description: [] + parameterListHeading: '' + optionListHeading: '' + commandListHeading: 'Commands:%n' + footerHeading: '' + footer: [Common footer] + ParserSpec: + separator: '=' + endOfOptionsDelimiter: '--' + expandAtFiles: true + atFileCommentChar: '#' + overwrittenOptionsAllowed: false + unmatchedArgumentsAllowed: false + unmatchedOptionsArePositionalParams: false + stopAtUnmatched: false + stopAtPositional: false + posixClusteredShortOptionsAllowed: true + aritySatisfiedByAttachedOptionParam: false + caseInsensitiveEnumValuesAllowed: false + collectErrors: false + limitSplit: false + toggleBooleanFlags: true + Subcommands: [] + UsageMessageSpec: + width: 80 + abbreviateSynopsis: false + hidden: false + showDefaultValues: false + sortOptions: true + requiredOptionMarker: ' ' + headerHeading: '' + header: [] + synopsisHeading: 'Usage: ' + customSynopsis: [] + descriptionHeading: '' + description: [] + parameterListHeading: '' + optionListHeading: '' + commandListHeading: 'Commands:%n' + footerHeading: '' + footer: [Common footer] + ParserSpec: + separator: '=' + endOfOptionsDelimiter: '--' + expandAtFiles: true + atFileCommentChar: '#' + overwrittenOptionsAllowed: false + unmatchedArgumentsAllowed: false + unmatchedOptionsArePositionalParams: false + stopAtUnmatched: false + stopAtPositional: false + posixClusteredShortOptionsAllowed: true + aritySatisfiedByAttachedOptionParam: false + caseInsensitiveEnumValuesAllowed: false + collectErrors: false + limitSplit: false + toggleBooleanFlags: true + Subcommands: [] diff --git a/picocli-annotation-processing-tests/src/test/resources/picocli/examples/mixin/CommonOption.java b/picocli-annotation-processing-tests/src/test/resources/picocli/examples/mixin/CommonOption.java new file mode 100644 index 000000000..f4ab4b339 --- /dev/null +++ b/picocli-annotation-processing-tests/src/test/resources/picocli/examples/mixin/CommonOption.java @@ -0,0 +1,10 @@ +package picocli.examples.mixin; + +import picocli.CommandLine.Command; +import picocli.CommandLine.Option; + +@Command(footer = "Common footer") +public class CommonOption { + @Option(names = "-x", description = "reusable option you want in many commands") + int x; +} diff --git a/picocli-annotation-processing-tests/src/test/resources/picocli/examples/subcommands/FileUtils.yaml b/picocli-annotation-processing-tests/src/test/resources/picocli/examples/subcommands/FileUtils.yaml new file mode 100644 index 000000000..89e391be3 --- /dev/null +++ b/picocli-annotation-processing-tests/src/test/resources/picocli/examples/subcommands/FileUtils.yaml @@ -0,0 +1,137 @@ +--- +CommandSpec: + name: 'fileutils' + aliases: [] + userObject: picocli.examples.subcommands.ParentCommandDemo$FileUtils@2e0fa5d3 + helpCommand: false + defaultValueProvider: null + versionProvider: null + version: [] + Options: + - names: [-d, --directory] + usageHelp: false + versionHelp: false + description: [this option applies to all subcommands] + descriptionKey: '' + typeInfo: RuntimeTypeInfo(java.io.File, aux=[class java.io.File], collection=false, map=false) + arity: 1 + splitRegex: '' + interactive: false + required: false + hidden: false + hideParamSyntax: false + defaultValue: 'null' + showDefaultValue: ON_DEMAND + hasInitialValue: true + initialValue: 'null' + paramLabel: '' + converters: [] + completionCandidates: null + getter: picocli.CommandLine.Model.FieldBinding(java.io.File picocli.examples.subcommands.ParentCommandDemo.FileUtils.baseDirectory) + setter: picocli.CommandLine.Model.FieldBinding(java.io.File picocli.examples.subcommands.ParentCommandDemo.FileUtils.baseDirectory) + PositionalParams: [] + UnmatchedArgsBindings: [] + Mixins: [] + UsageMessageSpec: + width: 80 + abbreviateSynopsis: false + hidden: false + showDefaultValues: false + sortOptions: true + requiredOptionMarker: ' ' + headerHeading: '' + header: [] + synopsisHeading: 'Usage: ' + customSynopsis: [] + descriptionHeading: '' + description: [] + parameterListHeading: '' + optionListHeading: '' + commandListHeading: 'Commands:%n' + footerHeading: '' + footer: [] + ParserSpec: + separator: '=' + endOfOptionsDelimiter: '--' + expandAtFiles: true + atFileCommentChar: '#' + overwrittenOptionsAllowed: false + unmatchedArgumentsAllowed: false + unmatchedOptionsArePositionalParams: false + stopAtUnmatched: false + stopAtPositional: false + posixClusteredShortOptionsAllowed: true + aritySatisfiedByAttachedOptionParam: false + caseInsensitiveEnumValuesAllowed: false + collectErrors: false + limitSplit: false + toggleBooleanFlags: true + Subcommands: + # list + - name: 'list' + aliases: [] + userObject: picocli.examples.subcommands.ParentCommandDemo$List@5010be6 + helpCommand: false + defaultValueProvider: null + versionProvider: null + version: [] + Options: + - names: [-r, --recursive] + usageHelp: false + versionHelp: false + description: [Recursively list subdirectories] + descriptionKey: '' + typeInfo: RuntimeTypeInfo(boolean, aux=[boolean], collection=false, map=false) + arity: 0 + splitRegex: '' + interactive: false + required: false + hidden: false + hideParamSyntax: false + defaultValue: 'null' + showDefaultValue: ON_DEMAND + hasInitialValue: true + initialValue: 'false' + paramLabel: '' + converters: [] + completionCandidates: null + getter: picocli.CommandLine.Model.FieldBinding(boolean picocli.examples.subcommands.ParentCommandDemo.List.recursive) + setter: picocli.CommandLine.Model.FieldBinding(boolean picocli.examples.subcommands.ParentCommandDemo.List.recursive) + PositionalParams: [] + UnmatchedArgsBindings: [] + Mixins: [] + UsageMessageSpec: + width: 80 + abbreviateSynopsis: false + hidden: false + showDefaultValues: false + sortOptions: true + requiredOptionMarker: ' ' + headerHeading: '' + header: [] + synopsisHeading: 'Usage: ' + customSynopsis: [] + descriptionHeading: '' + description: [] + parameterListHeading: '' + optionListHeading: '' + commandListHeading: 'Commands:%n' + footerHeading: '' + footer: [] + ParserSpec: + separator: '=' + endOfOptionsDelimiter: '--' + expandAtFiles: true + atFileCommentChar: '#' + overwrittenOptionsAllowed: false + unmatchedArgumentsAllowed: false + unmatchedOptionsArePositionalParams: false + stopAtUnmatched: false + stopAtPositional: false + posixClusteredShortOptionsAllowed: true + aritySatisfiedByAttachedOptionParam: false + caseInsensitiveEnumValuesAllowed: false + collectErrors: false + limitSplit: false + toggleBooleanFlags: true + Subcommands: [] diff --git a/picocli-annotation-processing-tests/src/test/resources/picocli/examples/subcommands/ParentCommandDemo.java b/picocli-annotation-processing-tests/src/test/resources/picocli/examples/subcommands/ParentCommandDemo.java new file mode 100644 index 000000000..6cfb65956 --- /dev/null +++ b/picocli-annotation-processing-tests/src/test/resources/picocli/examples/subcommands/ParentCommandDemo.java @@ -0,0 +1,66 @@ +/* + Copyright 2017 Remko Popma + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ +package picocli.examples.subcommands; + +import picocli.CommandLine; +import picocli.CommandLine.Command; +import picocli.CommandLine.Option; +import picocli.CommandLine.ParentCommand; + +import java.io.File; + +public class ParentCommandDemo { + + @Command(name = "fileutils", subcommands = List.class) + static class FileUtils implements Runnable { + + @Option(names = {"-d", "--directory"}, description = "this option applies to all subcommands") + private File baseDirectory; + + public void run() { System.out.println("FileUtils: my dir is " + baseDirectory); } + } + + @Command(name = "list") + static class List implements Runnable { + + @ParentCommand + private FileUtils parent; + + @Option(names = {"-r", "--recursive"}, description = "Recursively list subdirectories") + private boolean recursive; + + public void run() { + list(new File(parent.baseDirectory, "")); + } + + private void list(File dir) { + System.out.println(dir.getAbsolutePath()); + if (dir.isDirectory()) { + for (File f : dir.listFiles()) { + if (f.isDirectory() && recursive) { + list(f); + } else { + System.out.println(f.getAbsolutePath()); + } + } + } + } + } + + public static void main(String[] args) { + CommandLine.run(new FileUtils(), "--directory=examples/src", "list", "-r"); + } +} diff --git a/picocli-annotation-processing-tests/src/test/resources/picocli/examples/validation/Invalid.java b/picocli-annotation-processing-tests/src/test/resources/picocli/examples/validation/Invalid.java new file mode 100644 index 000000000..9c738fbc1 --- /dev/null +++ b/picocli-annotation-processing-tests/src/test/resources/picocli/examples/validation/Invalid.java @@ -0,0 +1,89 @@ +package picocli.examples.validation; + +import static picocli.CommandLine.Command; +import static picocli.CommandLine.Mixin; +import static picocli.CommandLine.Option; +import static picocli.CommandLine.Parameters; +import static picocli.CommandLine.ParentCommand; +import static picocli.CommandLine.Spec; +import static picocli.CommandLine.Unmatched; + +@Command(subcommands = {Invalid.Sub1.class, Invalid.Sub2.class}) +public class Invalid { + + @Option(names = "-a") + @Parameters + int invalidOptionAndParameters; + + @Mixin + int invalidPrimitiveMixin; + + @Option(names = "-b") + @Mixin + Integer invalidOptionAndMixin; + + @Option(names = "-c") + @Unmatched + int invalidOptionAndUnmatched; + + @Option(names = "-d") + @Spec + int invalidOptionAndSpec; + + @Option(names = "-e") + @ParentCommand + int invalidOptionAndParentCommand; + + // --- + @Parameters + @Mixin + Integer invalidParametersAndMixin; + + @Parameters + @Unmatched + int invalidParametersAndUnmatched; + + @Parameters + @Spec + int invalidParametersAndSpec; + + @Parameters + @ParentCommand + int invalidParametersAndParentCommand; + + // --- + @Unmatched + @Mixin + Integer invalidUnmatchedAndMixin; + + @Unmatched + @Spec + int invalidUnmatchedAndSpec; + + @Unmatched + @ParentCommand + int invalidUnmatchedAndParentCommand; + + // --- + @Spec + @Mixin + Integer invalidSpecAndMixin; + + @Spec + @ParentCommand + int invalidSpecAndParentCommand; + + // --- + @ParentCommand + @Mixin + Integer invalidParentCommandAndMixin; + + static class Sub1 { + @Parameters String[] params; + } + + @Command + static class Sub2 { + @Parameters String[] params; + } +} diff --git a/picocli-codegen/src/main/java/picocli/codegen/AnnotatedCommandSourceGenerator.java b/picocli-codegen/src/main/java/picocli/codegen/AnnotatedCommandSourceGenerator.java new file mode 100644 index 000000000..f35d0fe95 --- /dev/null +++ b/picocli-codegen/src/main/java/picocli/codegen/AnnotatedCommandSourceGenerator.java @@ -0,0 +1,903 @@ +package picocli.codegen; + +import picocli.CommandLine; +import picocli.CommandLine.Command; +import picocli.CommandLine.ITypeConverter; +import picocli.CommandLine.Mixin; +import picocli.CommandLine.Model.ArgSpec; +import picocli.CommandLine.Model.CommandSpec; +import picocli.CommandLine.Model.ITypeInfo; +import picocli.CommandLine.Model.MethodParam; +import picocli.CommandLine.Model.OptionSpec; +import picocli.CommandLine.Model.PositionalParamSpec; +import picocli.CommandLine.Option; +import picocli.CommandLine.Parameters; +import picocli.CommandLine.ParentCommand; +import picocli.CommandLine.Spec; +import picocli.CommandLine.Unmatched; +import picocli.codegen.annotation.processing.ITypeMetaData; +import picocli.codegen.util.Assert; +import picocli.codegen.util.TypeImporter; + +import javax.lang.model.element.Element; +import javax.lang.model.element.ElementKind; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.PackageElement; +import javax.lang.model.element.TypeElement; +import javax.lang.model.element.VariableElement; +import javax.lang.model.type.TypeMirror; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.lang.reflect.Array; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Queue; +import java.util.Set; +import java.util.Stack; + +public class AnnotatedCommandSourceGenerator { + + @Command(name = "
", + aliases = {}, + mixinStandardHelpOptions = false, + headerHeading = "", + header = {}, + descriptionHeading = "", + description = {}, + synopsisHeading = "Usage: ", + abbreviateSynopsis = false, + customSynopsis = {}, + optionListHeading = "", + parameterListHeading = "", + commandListHeading = "Commands:%n", + footerHeading = "", + footer = {}, + requiredOptionMarker = ' ', + addMethodSubcommands = true, + subcommands = {}, + version = {}, +// versionProvider = null, + showDefaultValues = false, +// defaultValueProvider = null, + resourceBundle = "", + sortOptions = true, + hidden = false, + helpCommand = false, + separator = "=", + usageHelpWidth = 80) + class App { + @ParentCommand + Object parent; + + @Spec + CommandSpec spec; + + @Unmatched + List unmatched; + + @Mixin + Object mixin; + + @Option(names = {}, + required = false, + help = false, + usageHelp = false, + versionHelp = false, + description = {}, + arity = "", + paramLabel = "", + hideParamSyntax = false, + type = {}, + converter = {}, + split = "", + hidden = false, + defaultValue = "__no_default_value__", + showDefaultValue = CommandLine.Help.Visibility.ON_DEMAND, +// completionCandidates = null, + interactive = false, + descriptionKey = "") + Object option; + + @Parameters(index = "", + description = {}, + arity = "", + paramLabel = "", + hideParamSyntax = false, + type = {}, + converter = {}, + split = "", + hidden = false, + defaultValue = "__no_default_value__", + showDefaultValue = CommandLine.Help.Visibility.ON_DEMAND, +// completionCandidates = null, + interactive = false, + descriptionKey = "") + Object parameter; + } + + private final static String INDENT_INCREMENT = " "; + private static final String[] EMPTY_ARRAY = new String[0]; + + private final CommandSpec commandSpec; + private TypeImporter importer; + private String outputPackage; + + public AnnotatedCommandSourceGenerator(CommandSpec commandSpec) { + this(commandSpec, extractPackageName(commandSpec.userObject())); + } + public AnnotatedCommandSourceGenerator(CommandSpec commandSpec, String outputPackage) { + this.commandSpec = Assert.notNull(commandSpec, "commandSpec"); + this.outputPackage = Assert.notNull(outputPackage, "outputPackage"); + this.importer = new TypeImporter(outputPackage); + } + + private static String extractPackageName(Object userObject) { + if (userObject instanceof ExecutableElement) { + return extractPackageName((TypeElement) ((ExecutableElement) userObject).getEnclosingElement()); + } else if (userObject instanceof TypeElement) { + return extractPackageName((TypeElement) userObject); + } else if (userObject instanceof Method) { + return extractPackageName(((Method) userObject).getDeclaringClass()); + } else if (userObject instanceof Class) { + return extractPackageName((Class) userObject); + } else { + return extractPackageName(userObject.getClass()); + } + } + + private static String extractPackageName(Class cls) { + return cls.getPackage().getName(); + } + + private static String extractPackageName(TypeElement typeElement) { + Element enclosing = typeElement.getEnclosingElement(); + while (enclosing != null) { + if (enclosing instanceof PackageElement) { + PackageElement pkg = (PackageElement) enclosing; + if (pkg.isUnnamed()) { + return ""; + } + String fqcn = pkg.getQualifiedName().toString(); + return fqcn; + } + enclosing = enclosing.getEnclosingElement(); + } + return ""; + } + + public String getOutputPackage() { + return outputPackage; + } + + public void setOutputPackage(String outputPackage) { + this.outputPackage = Assert.notNull(outputPackage, "outputPackage"); + this.importer = new TypeImporter(outputPackage); + } + + public String generate() { + StringWriter result = new StringWriter(); + writeTo(new PrintWriter(result), ""); + return result.toString(); + } + + public void writeTo(PrintWriter pw, String indent) { + StringWriter result = new StringWriter(); + PrintWriter tmp = new PrintWriter(result); + printCommand(tmp, commandSpec, indent, new HashSet()); + + pw.println("package " + outputPackage + ";"); + pw.println(importer.createImportDeclaration()); + pw.println(); + pw.print(result); + pw.flush(); + } + + private void printCommand(PrintWriter pw, CommandSpec spec, String indent, Set visited) { + Stack after = new Stack(); + Stack surroundingElements = new Stack(); + indent = printSurroundingElements(pw, spec.userObject(), indent, surroundingElements, after, visited); + + printCommandAnnotation(pw, spec, indent); + pw.println(); + printCommandElementDefOpen(pw, spec.userObject(), indent); + boolean isCommandMethod = isCommandMethod(spec); + String indent2 = indent + INDENT_INCREMENT; + + for (String mixinName : spec.mixins().keySet()) { + CommandSpec mixin = spec.mixins().get(mixinName); + if ("picocli.CommandLine.AutoHelpMixin".equals(mixin.userObject().getClass().getCanonicalName())) { + continue; + } + pw.println(); + pw.printf("%s@%s ", indent2, importer.getImportedName(Mixin.class.getCanonicalName())); + pw.print(importer.getImportedName(extractClassName(mixin.userObject()))); + pw.println(" " + mixinName + ";"); + } + + String sep = ""; + for (OptionSpec option : spec.options()) { + if (!isMixedIn(option, spec)) { + pw.printf(sep); + pw.println(); + printOptionAnnotation(pw, option, indent2); + pw.printf(isCommandMethod ? " " : ("%n" + indent2)); + sep = printArgElementDef(pw, option.userObject(), isCommandMethod, indent2); + } + } + for (PositionalParamSpec param : spec.positionalParameters()) { + if (!isMixedIn(param, spec)) { + pw.printf(sep); + pw.println(); + printParametersAnnotation(pw, param, indent2); + pw.printf(isCommandMethod ? " " : ("%n" + indent2)); + sep = printArgElementDef(pw, param.userObject(), isCommandMethod, indent2); + } + } + if (!isCommandMethod) { + pw.printf(sep); + } + + for (String mixinName : spec.mixins().keySet()) { + CommandSpec mixin = spec.mixins().get(mixinName); + if (isNestedCommand(mixin, spec) && !isBuiltInMixin(mixin)) { + pw.println(); + printCommand(pw, mixin, indent + INDENT_INCREMENT, visited); + } + } + for (String subcommandName : spec.subcommands().keySet()) { + CommandSpec subcommand = spec.subcommands().get(subcommandName).getCommandSpec(); + if (isNestedCommand(subcommand, spec) && !isBuiltInSubcommand(subcommand)) { + pw.println(); + printCommand(pw, subcommand, indent + INDENT_INCREMENT, visited); + } + } + printCommandElementDefClose(pw, spec.userObject(), indent); + + while (!after.isEmpty()) { + Object surroundingElement = surroundingElements.pop(); + for (String mixinName : spec.mixins().keySet()) { + CommandSpec mixin = spec.mixins().get(mixinName); + if (isNested(mixin.userObject(), surroundingElement) && !isBuiltInMixin(mixin)) { + pw.println(); + printCommand(pw, mixin, indent, visited); + } + } + for (String subcommandName : spec.subcommands().keySet()) { + CommandSpec subcommand = spec.subcommands().get(subcommandName).getCommandSpec(); + if (isNested(subcommand.userObject(), surroundingElement) && !isBuiltInSubcommand(subcommand)) { + pw.println(); + printCommand(pw, subcommand, indent, visited); + } + } + pw.print(after.pop()); + } + } + + public static boolean isBuiltInMixin(CommandSpec mixin) { + String str = mixin.userObject().toString(); + return "picocli.CommandLine.AutoHelpMixin".equals(str) + || "picocli.CommandLine$AutoHelpMixin".equals(str); + } + + public static boolean isBuiltInSubcommand(CommandSpec subcommand) { + String str = subcommand.userObject().toString(); + return "picocli.CommandLine.HelpCommand".equals(str) + || "picocli.CommandLine$HelpCommand".equals(str); + } + + @SuppressWarnings("unchecked") + private String printSurroundingElements(PrintWriter pw, + Object userObject, + String indent, + Stack surrounding, + Stack after, + Set visited) { + + collectEnclosingElements(userObject, surrounding, visited); + Stack enclosing = (Stack) surrounding.clone(); + Queue indents = new LinkedList(); + for (int i = 0; i < enclosing.size(); i++) { + indents.add(indent); + indent += " "; + } + + String currentIndent = indent; + Stack before = new Stack(); + while (!enclosing.isEmpty()) { + Object obj = enclosing.pop(); + currentIndent = indents.poll(); + if (obj == userObject) { + break; + } + + StringWriter sw = new StringWriter(); + if (obj instanceof Method || obj instanceof ExecutableElement) { + printArgElementDef(new PrintWriter(sw), obj, true, currentIndent); + String definition = sw.toString(); + definition = definition.substring(0, definition.indexOf("{") + 1); + before.push(String.format("%s%n", definition)); + after.push(String.format("%s}%n", currentIndent)); + + } else { + printCommandElementDefOpen(new PrintWriter(sw), obj, currentIndent); + before.push(String.format("%s%n", sw.toString())); + sw.getBuffer().setLength(0); + printCommandElementDefClose(new PrintWriter(sw), obj, currentIndent); + after.push(sw.toString()); + } + } + while (!before.isEmpty()) { + pw.print(before.pop()); + } + return currentIndent; + } + + private void collectEnclosingElements(Object userObject, Stack enclosing, Set visited) { + if (visited.contains(userObject)) { + return; + } + visited.add(userObject); + enclosing.add(userObject); + if (userObject instanceof Method) { + collectEnclosingElements(((Method) userObject).getDeclaringClass(), enclosing, visited); + } else if (userObject instanceof ExecutableElement) { + collectEnclosingElements(((ExecutableElement) userObject).getEnclosingElement(), enclosing, visited); + } else if (userObject instanceof Class) { + Class type = (Class) userObject; + if (type.getEnclosingMethod() != null) { + collectEnclosingElements(type.getEnclosingMethod(), enclosing, visited); + } else if (type.getDeclaringClass() != null) { + collectEnclosingElements(type.getDeclaringClass(), enclosing, visited); + } + } else if (userObject instanceof TypeElement) { + Element enclosingElement = ((TypeElement) userObject).getEnclosingElement(); + if (enclosingElement instanceof TypeElement || enclosingElement instanceof ExecutableElement) { + collectEnclosingElements(enclosingElement, enclosing, visited); + } + } + } + + public static boolean isNestedCommand(CommandSpec inner, CommandSpec outer) { + Object innerUserObject = inner.userObject(); + Object outerUserObject = outer.userObject(); + return isNested(innerUserObject, outerUserObject); + } + + private static boolean isNested(Object innerUserObject, Object outerUserObject) { + if (innerUserObject instanceof Method) { + Class cls = ((Method) innerUserObject).getDeclaringClass(); + if (cls.equals(outerUserObject) || cls.equals(outerUserObject.getClass())) { + return true; + } + } else if (innerUserObject instanceof Element) { // ExecutableElement or TypeElement + Element enclosingElement = ((Element) innerUserObject).getEnclosingElement(); + while (enclosingElement != null) { + if (enclosingElement.equals(outerUserObject)) { + return true; + } + enclosingElement = enclosingElement.getEnclosingElement(); + } + return false; + } else if (innerUserObject instanceof Class) { + Class cls = (Class) innerUserObject; + if (cls.isMemberClass() && + (cls.getEnclosingClass().equals(outerUserObject) || cls.getEnclosingClass().equals(outerUserObject.getClass()))) { + return true; + } + } else { + Class cls = innerUserObject.getClass(); + if (cls.isMemberClass() && + (cls.getEnclosingClass().equals(outerUserObject) || cls.getEnclosingClass().equals(outerUserObject.getClass()))) { + return true; + } + } + return false; + } + + private static boolean isMixedIn(OptionSpec option, CommandSpec spec) { + for (CommandSpec mixin : spec.mixins().values()) { + if (mixin.findOption(option.longestName()) != null) { + return true; + } + } + return false; + } + + private static boolean isMixedIn(PositionalParamSpec positional, CommandSpec spec) { + for (CommandSpec mixin : spec.mixins().values()) { + for (PositionalParamSpec mixedIn : mixin.positionalParameters()) { + if (mixedIn.equals(positional)) { + return true; + } + } + } + return false; + } + + private static String argElementName(ArgSpec argSpec) { + Object userObject = argSpec.userObject(); + if (userObject instanceof Field) { + return ((Field) userObject).getName(); + } else if (userObject instanceof MethodParam) { + return ((MethodParam) userObject).getName(); + } else if (userObject instanceof Method) { + return propertyName(((Method) userObject).getName()); + } else if (userObject instanceof VariableElement) { + return ((VariableElement) userObject).getSimpleName().toString(); + } else if (userObject instanceof ExecutableElement) { + return propertyName(((ExecutableElement) userObject).getSimpleName().toString()); + } else { + return userObject + ""; + } + } + static String propertyName(String methodName) { + if (methodName.length() > 3 && (methodName.startsWith("get") || methodName.startsWith("set"))) { return decapitalize(methodName.substring(3)); } + return decapitalize(methodName); + } + private static String decapitalize(String name) { + if (name == null || name.length() == 0) { return name; } + char[] chars = name.toCharArray(); + chars[0] = Character.toLowerCase(chars[0]); + return new String(chars); + } + + private String printArgElementDef(PrintWriter pw, + Object userObject, + boolean enclosedInCommandMethod, + String indent) { + if (userObject instanceof Field) { + Field f = (Field) userObject; + String result = typeName(f.getGenericType()) + " " + f.getName(); + if (f.getModifiers() != 0 && !enclosedInCommandMethod) { + pw.print(Modifier.toString(f.getModifiers()) + " " + result); + } else { + pw.print(result); + } + // default return value + } else if (userObject instanceof MethodParam) { + MethodParam param = (MethodParam) userObject; + String result = typeName(param.getParameterizedType()) + " " + param.getName(); + pw.print(result); + // default return value //return ",%n"; + } else if (userObject instanceof Method) { + Method m = (Method) userObject; + StringBuilder sb = new StringBuilder(128); + sb.append(typeName(m.getGenericReturnType())).append(" ").append(m.getName()); + sb.append('('); + Type[] params = m.getGenericParameterTypes(); + for (int j = 0; j < params.length; j++) { + String param = typeName(params[j]); + if (m.isVarArgs() && (j == params.length - 1)) {// replace T[] with T... + param = param.replaceFirst("\\[\\]$", "..."); + } + sb.append(param); + sb.append(" ").append(parameterName(m, j)); + if (j < (params.length - 1)) { + sb.append(','); + } + } + sb.append(')'); + Type[] exceptions = m.getGenericExceptionTypes(); + if (exceptions.length > 0) { + sb.append(" throws "); + for (int k = 0; k < exceptions.length; k++) { + sb.append((exceptions[k] instanceof Class) + ? importer.getImportedName(((Class) exceptions[k]).getCanonicalName()) + : importer.getImportedName(exceptions[k].toString())) ; + if (k < (exceptions.length - 1)) { + sb.append(','); + } + } + } + String result = sb.toString(); + if (m.getModifiers() != 0 && !enclosedInCommandMethod) { + pw.print(Modifier.toString(m.getModifiers()) + " " + result); + } else { + pw.print(result); + } + if (m.getDeclaringClass().isInterface()) { + pw.print(";"); + } else { + pw.println(" {"); + pw.println(indent + " // TODO replace the stored value with the new value"); + pw.println(indent + "}"); + } + return ""; + } else if (userObject instanceof VariableElement) { + VariableElement f = (VariableElement) userObject; + String result = typeName(f.asType()) + " " + f.getSimpleName(); + if (!f.getModifiers().isEmpty() && !enclosedInCommandMethod) { + pw.print(modifierString(f.getModifiers()) + "" + result); + } else { + pw.print(result); + } + // default return value + + } else if (userObject instanceof ExecutableElement) { + ExecutableElement m = (ExecutableElement) userObject; + StringBuilder sb = new StringBuilder(128); + sb.append(typeName(m.getReturnType())).append(" ").append(m.getSimpleName()); + sb.append('('); + List parameters = m.getParameters(); + //List typeParameters = m.getTypeParameters(); + for (int j = 0; j < parameters.size(); j++) { + String param = typeName(parameters.get(j).asType()); + if (m.isVarArgs() && (j == parameters.size() - 1)) {// replace T[] with T... + param = param.replaceFirst("\\[\\]$", "..."); + } + sb.append(param); + sb.append(" ").append(parameters.get(j).getSimpleName()); + if (j < (parameters.size() - 1)) { + sb.append(','); + } + } + sb.append(')'); + List exceptions = m.getThrownTypes(); + if (!exceptions.isEmpty()) { + sb.append(" throws "); + for (int k = 0; k < exceptions.size(); k++) { + sb.append(importer.getImportedName(exceptions.get(k).toString())) ; + if (k < (exceptions.size() - 1)) { + sb.append(','); + } + } + } + String result = sb.toString(); + if (!m.getModifiers().isEmpty() && !enclosedInCommandMethod) { + pw.print(modifierString(m.getModifiers()) + "" + result); + } else { + pw.print(result); + } + if (m.getEnclosingElement().getKind() == ElementKind.INTERFACE) { + pw.print(";"); + } else { + pw.println(" {"); + pw.println(indent + " // TODO replace the stored value with the new value"); + pw.println(indent + "}"); + } + return ""; + } else { + pw.print("CANNOT RENDER " + userObject); + } + return enclosedInCommandMethod ? "," : ";%n"; + } + + private String typeName(Type type) { + if (type instanceof Class) { + return importer.getImportedName(((Class) type).getCanonicalName()); + } + return importer.getImportedName(type.toString()); + } + + private String typeName(TypeMirror type) { +// StringBuilder sb = new StringBuilder(); +// type.accept(new AbstractTypeVisitor6() { +// +// }, sb); + return importer.getImportedName(type.toString()); + } + + private static String parameterName(Method m, int j) { + try { + Object parameterArray = Method.class.getDeclaredMethod("getParameters").invoke(m); + Object parameter = Array.get(parameterArray, j); + return (String) Class.forName("java.lang.reflect.Parameter").getDeclaredMethod("getName").invoke(parameter); + } catch (Exception ignored) {} + return "arg" + j; + } + + private static boolean isCommandMethod(CommandSpec spec) { + Object userObject = spec.userObject(); + return userObject instanceof Method || userObject instanceof ExecutableElement; + } + + private void printCommandElementDefOpen(PrintWriter pw, Object userObject, String indent) { + if (userObject instanceof Method) { + Method m = (Method) userObject; + pw.print(indent); + if (m.getModifiers() != 0) { + pw.print(Modifier.toString(m.getModifiers())); + } + pw.print(typeName(m.getGenericReturnType())); + pw.print(" "); + pw.print(m.getName()); + pw.print("("); + } else if (userObject instanceof ExecutableElement) { + ExecutableElement m = (ExecutableElement) userObject; + pw.print(indent); + if (!m.getModifiers().isEmpty()) { + pw.print(modifierString(m.getModifiers())); + } + pw.print(typeName(m.getReturnType())); + pw.print(" "); + pw.print(m.getSimpleName()); + pw.print("("); + } else if (userObject instanceof TypeElement) { + TypeElement type = (TypeElement) userObject; + String modifiers = modifierString(type.getModifiers()); + String name = type.getSimpleName().toString(); + pw.printf("%s%sclass %s {", indent, modifiers, name); + } else { + Class cls = userObject.getClass(); + String modifiers = cls.getModifiers() == 0 ? "" : (Modifier.toString(cls.getModifiers()) + " "); + String name = importer.getImportedName(userObject.getClass().getCanonicalName()); + pw.printf("%s%sclass %s {", indent, modifiers, name); + } + } + + private static String modifierString(Set modifiers) { + return modifierString(modifiers, new StringBuilder()).toString(); + } + + private static StringBuilder modifierString(Set modifiers, StringBuilder sb) { + for (javax.lang.model.element.Modifier mod : modifiers) { + sb.append(mod.toString()); + sb.append(" "); + } + return sb; + } + + private static void printCommandElementDefClose(PrintWriter pw, Object userObject, String indent) { + if (userObject instanceof Method || userObject instanceof ExecutableElement) { + String full = (userObject).toString(); + pw.print(full.substring(full.indexOf(')'))); + pw.println(" {"); + pw.println(indent + " // TODO implement commandSpec"); + pw.println(indent + "}"); + } else { + pw.printf("%s}%n", indent); + } + } + + private void printParametersAnnotation(PrintWriter pw, PositionalParamSpec spec, String indent) { + pw.printf("%s@%s", indent, importer.getImportedName(Parameters.class.getCanonicalName())); + indent = String.format(",%n%s ", indent); + String sep = "("; + + sep = append(pw, sep, indent, "index = \"%s\"", spec.index().toString(), spec.index().isUnspecified()); + sep = appendStringArray(pw, sep, indent, "description = %s", spec.description(), EMPTY_ARRAY); + sep = append(pw, sep, indent, "arity = \"%s\"", spec.arity().toString(), spec.arity().isUnspecified() ? spec.arity().toString() : ""); + sep = append(pw, sep, indent, "paramLabel = \"%s\"", spec.paramLabel(), "<" + argElementName(spec) + ">"); + sep = append(pw, sep, indent, "hideParamSyntax = %s", spec.hideParamSyntax(), false); + sep = appendTypeInfo(pw, sep, indent, spec.typeInfo()); + sep = appendTypeConverter(pw, sep, indent, spec.converters()); + sep = append(pw, sep, indent, "split = \"%s\"", spec.splitRegex(), ""); + sep = append(pw, sep, indent, "hidden = %s", spec.hidden(), false); + sep = append(pw, sep, indent, "defaultValue = \"%s\"", spec.defaultValue() == null ? "__no_default_value__" : spec.defaultValue(), "__no_default_value__"); + sep = append(pw, sep, indent, "showDefaultValue = %s", spec.showDefaultValue(), CommandLine.Help.Visibility.ON_DEMAND); + sep = appendCompletionCandidates(pw, sep, indent, spec); + sep = append(pw, sep, indent, "interactive = %s", spec.interactive(), false); + sep = append(pw, sep, indent, "descriptionKey = \"%s\"", spec.descriptionKey(), ""); + + if (!"(".equals(sep)) { + pw.print(")"); + } + } + + @SuppressWarnings("deprecation") + private void printOptionAnnotation(PrintWriter pw, OptionSpec spec, String indent) { + pw.printf("%s@%s", indent, importer.getImportedName(Option.class.getCanonicalName())); + indent = String.format(",%n%s ", indent); + String sep = "("; + + sep = appendStringArray(pw, sep, indent, "names = %s", spec.names(), EMPTY_ARRAY); + sep = append(pw, sep, indent, "required = %s", spec.required(), false); + sep = append(pw, sep, indent, "help = %s", spec.help(), false); + sep = append(pw, sep, indent, "usageHelp = %s", spec.usageHelp(), false); + sep = append(pw, sep, indent, "versionHelp = %s", spec.versionHelp(), false); + sep = appendStringArray(pw, sep, indent, "description = %s", spec.description(), EMPTY_ARRAY); + sep = append(pw, sep, indent, "arity = \"%s\"", spec.arity().toString(), spec.arity().isUnspecified() ? spec.arity().toString() : ""); + sep = append(pw, sep, indent, "paramLabel = \"%s\"", spec.paramLabel(), "<" + argElementName(spec) + ">"); + sep = append(pw, sep, indent, "hideParamSyntax = %s", spec.hideParamSyntax(), false); + sep = appendTypeInfo(pw, sep, indent, spec.typeInfo()); + sep = appendTypeConverter(pw, sep, indent, spec.converters()); + sep = append(pw, sep, indent, "split = \"%s\"", spec.splitRegex(), ""); + sep = append(pw, sep, indent, "hidden = %s", spec.hidden(), false); + sep = append(pw, sep, indent, "defaultValue = %s", spec.defaultValue() == null ? "__no_default_value__" : spec.defaultValue(), "__no_default_value__"); + sep = append(pw, sep, indent, "showDefaultValue = \"%s\"", spec.showDefaultValue(), CommandLine.Help.Visibility.ON_DEMAND); + sep = appendCompletionCandidates(pw, sep, indent, spec); + sep = append(pw, sep, indent, "interactive = %s", spec.interactive(), false); + sep = append(pw, sep, indent, "descriptionKey = \"%s\"", spec.descriptionKey(), ""); + + if (!"(".equals(sep)) { + pw.print(")"); + } + } + + private void printCommandAnnotation(PrintWriter pw, CommandSpec spec, String indent) { + pw.printf("%s@%s", indent, importer.getImportedName(Command.class.getCanonicalName())); + indent = String.format(",%n%s ", indent); + String sep = "("; + + sep = append(pw, sep, indent, "name = \"%s\"", spec.name(), "
"); + sep = appendStringArray(pw, sep, indent, "aliases = %s", spec.aliases(), EMPTY_ARRAY); + sep = append(pw, sep, indent, "mixinStandardHelpOptions = %s", spec.mixinStandardHelpOptions(), false); + sep = append(pw, sep, indent, "headerHeading = \"%s\"", spec.usageMessage().headerHeading(), ""); + sep = appendStringArray(pw, sep, indent, "header = %s", spec.usageMessage().header(), EMPTY_ARRAY); + sep = append(pw, sep, indent, "descriptionHeading = \"%s\"", spec.usageMessage().descriptionHeading(), ""); + sep = appendStringArray(pw, sep, indent, "description = %s", spec.usageMessage().description(), EMPTY_ARRAY); + sep = append(pw, sep, indent, "synopsisHeading = \"%s\"", spec.usageMessage().synopsisHeading(), "Usage: "); + sep = append(pw, sep, indent, "abbreviateSynopsis = %s", spec.usageMessage().abbreviateSynopsis(), false); + sep = appendStringArray(pw, sep, indent, "customSynopsis = %s", spec.usageMessage().customSynopsis(), EMPTY_ARRAY); + sep = append(pw, sep, indent, "optionListHeading = \"%s\"", spec.usageMessage().optionListHeading(), ""); + sep = append(pw, sep, indent, "parameterListHeading = \"%s\"", spec.usageMessage().parameterListHeading(), ""); + sep = append(pw, sep, indent, "commandListHeading = \"%s\"", spec.usageMessage().commandListHeading(), "Commands:%n"); + sep = append(pw, sep, indent, "footerHeading = \"%s\"", spec.usageMessage().footerHeading(), ""); + sep = appendStringArray(pw, sep, indent, "footer = %s", spec.usageMessage().footer(), EMPTY_ARRAY); + sep = append(pw, sep, indent, "requiredOptionMarker = \'%s\'", spec.usageMessage().requiredOptionMarker(), ' '); + sep = append(pw, sep, indent, "addMethodSubcommands = %s", spec.isAddMethodSubcommands(), !isCommandMethod(spec)); + sep = appendSubcommandClasses(pw, sep, indent, spec.subcommands()); + sep = appendStringArray(pw, sep, indent, "version = %s", spec.version(), EMPTY_ARRAY); + sep = appendClassName(pw, sep, indent, "versionProvider = %s", spec.versionProvider()); + sep = append(pw, sep, indent, "showDefaultValues = %s", spec.usageMessage().showDefaultValues(), false); + sep = appendClassName(pw, sep, indent, "defaultValueProvider = %s", spec.defaultValueProvider()); + sep = append(pw, sep, indent, "resourceBundle = \"%s\"", spec.resourceBundleBaseName(), "null"); + sep = append(pw, sep, indent, "sortOptions = %s", spec.usageMessage().sortOptions(), true); + sep = append(pw, sep, indent, "hidden = %s", spec.usageMessage().hidden(), false); + sep = append(pw, sep, indent, "helpCommand = %s", spec.helpCommand(), false); + sep = append(pw, sep, indent, "separator = \"%s\"", spec.parser().separator(), "="); + sep = append(pw, sep, indent, "usageHelpWidth = %s", spec.usageMessage().width(), 80); + + if (!"(".equals(sep)) { + pw.print(")"); + } + } + + private static String append(PrintWriter pw, + String prefix, + String newPrefix, + String template, + Object value, + Object defaultValue) { + if (defaultValue.equals(value) || ("null".equals(defaultValue) && value == null)) { + return prefix; + } + pw.print(prefix); + pw.printf(template, value); + return newPrefix; + } + + private static String appendStringArray(PrintWriter pw, + String prefix, + String newPrefix, + String template, + String[] values, + String[] defaultValues) { + if (values == null || Arrays.equals(values, defaultValues)) { + return prefix; + } + List quoted = new ArrayList(); + for (String value : values) { + quoted.add('"' + value + '"'); + } + pw.print(prefix); + pw.printf(template, listToString(quoted)); + return newPrefix; + } + + private String appendSubcommandClasses(PrintWriter pw, + String prefix, + String newPrefix, + Map subcommands) { + List subcommandClasses = new ArrayList(); + for (CommandLine cmd : subcommands.values()) { + Object obj = cmd.getCommand(); + if (!(obj instanceof Method) && !(obj instanceof ExecutableElement)) { + if (obj instanceof Element) { + subcommandClasses.add(importer.getImportedName(obj.toString()) + ".class"); + } else { + subcommandClasses.add(importer.getImportedName(obj.getClass().getCanonicalName()) + ".class"); + } + } + } + if (subcommandClasses.isEmpty()) { + return prefix; + } + pw.print(prefix); + pw.printf("subcommands = %s", listToString(subcommandClasses)); + return newPrefix; + } + + private String appendClassName(PrintWriter pw, + String prefix, + String newPrefix, + String template, + Object object) { + if (object == null || isDefault(object)) { + return prefix; + } + pw.print(prefix); + pw.printf(template, extractClassName(object) + ".class"); + return newPrefix; + } + + private String appendTypeInfo(PrintWriter pw, + String prefix, + String newPrefix, + ITypeInfo typeInfo) { + if (typeInfo.isCollection() || typeInfo.isMap()) { + List aux = typeInfo.getAuxiliaryTypeInfos(); + pw.print(prefix); + pw.printf("type = %s", listToString(extractClassNames(aux))); + return newPrefix; + } + return prefix; + } + + private List extractClassNames(List list) { + List result = new ArrayList(); + for (ITypeInfo typeInfo : list) { + result.add(importer.getImportedName(typeInfo.getClassName()) + ".class"); + } + return result; + } + + private String appendTypeConverter(PrintWriter pw, + String prefix, + String newPrefix, + ITypeConverter[] typeConverters) { + if (typeConverters == null) { + return prefix; + } + + List classNames = new ArrayList(); + for (ITypeConverter converter : typeConverters) { + if (!isDefault(converter)) { + classNames.add(extractClassName(converter) + ".class"); + } + } + if (classNames.isEmpty()) { + return prefix; + } + pw.print(prefix); + pw.printf("converter = %s", listToString(classNames)); + return newPrefix; + } + + private String appendCompletionCandidates(PrintWriter pw, + String prefix, + String newPrefix, + ArgSpec argSpec) { + Iterable completionCandidates = argSpec.completionCandidates(); + if (completionCandidates == null || isDefault(completionCandidates) || argSpec.typeInfo().isEnum()) { + return prefix; + } + pw.print(prefix); + pw.printf("completionCandidates = %s.class", extractClassName(completionCandidates)); + return newPrefix; + } + + private static String listToString(List values) { + if (values.isEmpty()) { + return "{}"; + } + if (values.size() == 1) { + return values.get(0); + } + return values.toString().replace('[', '{').replace(']', '}'); + } + + private static boolean isDefault(Object typeMetaData) { + return typeMetaData instanceof ITypeMetaData && ((ITypeMetaData) typeMetaData).isDefault(); + } + + private String extractClassName(Object object) { + if (object instanceof ITypeMetaData) { + ITypeMetaData metaData = (ITypeMetaData) object; + return importer.getImportedName(metaData.getTypeMirror().toString()); + } else if (object instanceof Element) { + TypeElement typeElement = (TypeElement) object; + return importer.getImportedName(typeElement.getQualifiedName().toString()); + } else { + return importer.getImportedName(object.getClass().getCanonicalName()); + } + } +} diff --git a/picocli-codegen/src/main/java/picocli/codegen/IGenerator.java b/picocli-codegen/src/main/java/picocli/codegen/IGenerator.java new file mode 100644 index 000000000..54e066816 --- /dev/null +++ b/picocli-codegen/src/main/java/picocli/codegen/IGenerator.java @@ -0,0 +1,9 @@ +package picocli.codegen; + +import java.io.IOException; +import java.io.OutputStream; +import java.util.Map; + +public interface IGenerator { + void generate(OutputStream out, Map options) throws IOException; +} diff --git a/picocli-codegen/src/main/java/picocli/codegen/annotation/processing/AbstractCommandSpecProcessor.java b/picocli-codegen/src/main/java/picocli/codegen/annotation/processing/AbstractCommandSpecProcessor.java new file mode 100644 index 000000000..5ac684914 --- /dev/null +++ b/picocli-codegen/src/main/java/picocli/codegen/annotation/processing/AbstractCommandSpecProcessor.java @@ -0,0 +1,1070 @@ +package picocli.codegen.annotation.processing; + +import picocli.CommandLine; +import picocli.CommandLine.Command; +import picocli.CommandLine.IFactory; +import picocli.CommandLine.Mixin; +import picocli.CommandLine.Model.CommandSpec; +import picocli.CommandLine.Model.IAnnotatedElement; +import picocli.CommandLine.Model.ITypeInfo; +import picocli.CommandLine.Model.OptionSpec; +import picocli.CommandLine.Model.PositionalParamSpec; +import picocli.CommandLine.Option; +import picocli.CommandLine.Parameters; +import picocli.CommandLine.ParentCommand; +import picocli.CommandLine.Spec; +import picocli.CommandLine.Unmatched; +import picocli.codegen.annotation.processing.internal.CompletionCandidatesMetaData; +import picocli.codegen.annotation.processing.internal.DefaultValueProviderMetaData; +import picocli.codegen.annotation.processing.internal.GetterSetterMetaData; +import picocli.codegen.annotation.processing.internal.TypeConverterMetaData; +import picocli.codegen.annotation.processing.internal.VersionProviderMetaData; +import picocli.codegen.util.Assert; + +import javax.annotation.processing.AbstractProcessor; +import javax.annotation.processing.ProcessingEnvironment; +import javax.annotation.processing.RoundEnvironment; +import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.AnnotationValue; +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.ArrayType; +import javax.lang.model.type.DeclaredType; +import javax.lang.model.type.ExecutableType; +import javax.lang.model.type.MirroredTypeException; +import javax.lang.model.type.TypeKind; +import javax.lang.model.type.TypeMirror; +import javax.tools.Diagnostic; +import java.lang.annotation.Annotation; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.EnumSet; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.Stack; +import java.util.TreeSet; +import java.util.logging.ConsoleHandler; +import java.util.logging.Handler; +import java.util.logging.Level; +import java.util.logging.Logger; + +import static java.lang.String.format; +import static javax.lang.model.element.ElementKind.ENUM; + +/** + * Abstract annotation processor for {@code @picocli.*} annotations that produces a set of + * {@link CommandSpec} objects built from the annotated source code. + *

+ * Subclasses should override the {@link #handleCommands(Map, Set, RoundEnvironment)} + * method to do something useful with these {@code CommandSpec} objects, + * like generating source code, documentation or configuration files. + *

+ * Note that due to the limitations of annotation processing and the compiler API, + * annotation attributes of type {@code Class} are {@linkplain Element#getAnnotation(Class) not available} + * as {@code Class} values at compile time, but only as {@code TypeMirror} values. + * Picocli 4.0 introduces a new {@link ITypeInfo} interface that provides {@code ArgSpec} + * type metadata that can be used both at compile time and at runtime. + *

+ * Similarly, {@code ArgSpec} objects constructed by the annotation processor will have a + * {@link picocli.CommandLine.Model.IGetter} and {@link picocli.CommandLine.Model.ISetter} + * implementation that is different from the one used at runtime and cannot be invoked directly: + * the annotation processor will assign an {@link GetterSetterMetaData} + * implementation that gives subclass annotation processors access to the annotated element. + *

+ * {@code CommandSpec} objects constructed by the annotation processor will have an + * {@link VersionProviderMetaData} version provider and a {@link DefaultValueProviderMetaData} + * default value provider, which gives subclass annotation processors access to the + * {@code TypeMirror} of the version provider and default value provider specified in the + * annotation. + *

+ * @since 4.0 + */ +public abstract class AbstractCommandSpecProcessor extends AbstractProcessor { + private static final String COMMAND_DEFAULT_NAME = "
"; + private static Logger logger = Logger.getLogger(AbstractCommandSpecProcessor.class.getName()); + + /** The ProcessingEnvironment set by the {@link #init(ProcessingEnvironment)} method. */ + protected ProcessingEnvironment processingEnv; + + private static final String COMMAND_TYPE = Command.class.getName().replace('$', '.'); + + static ConsoleHandler handler = new ConsoleHandler(); + + { + for (Handler h : Logger.getLogger("picocli.annotation.processing").getHandlers()) { + Logger.getLogger("picocli.annotation.processing").removeHandler(h); + } + handler.setFormatter(new JulLogFormatter()); + handler.setLevel(Level.ALL); + Logger.getLogger("picocli.annotation.processing").addHandler(handler); + Logger.getLogger("picocli.annotation.processing").setLevel(Level.ALL); + } + + /** + * Returns the annotation types supported by the super class, and adds + * {@code "picocli.*"} if necessary. + * Subclasses can omit the {@code @SupportedAnnotationTypes("picocli.*")} annotation, + * but add other annotations if desired. + * + * @return the set of supported annotation types, with at least {@code "picocli.*"} + */ + @Override + public Set getSupportedAnnotationTypes() { + Set result = super.getSupportedAnnotationTypes(); + if (!result.contains("picocli.*")) { + result = new TreeSet(result); + result.add("picocli.*"); + } + return result; + } + + @Override + public synchronized void init(ProcessingEnvironment processingEnv) { + this.processingEnv = processingEnv; + } + + // inherit doc + @Override + public boolean process(Set annotations, RoundEnvironment roundEnv) { + logger.info("Entered process, processingOver=" + roundEnv.processingOver()); + + IFactory factory = null; //new NullFactory(); + Map commands = new LinkedHashMap(); + Map> commandTypes = new LinkedHashMap>(); + Map options = new LinkedHashMap(); + Map parameters = new LinkedHashMap(); + List mixinInfoList = new ArrayList(); + Map parentCommands = new LinkedHashMap(); + Map specs = new LinkedHashMap(); + Map unmatched = new LinkedHashMap(); + + logger.fine("Building commands..."); + buildCommands(roundEnv, factory, commands, commandTypes, options, parameters); + + logger.fine("Building mixins..."); + buildMixins(roundEnv, factory, commands, mixinInfoList, commandTypes, options, parameters); + + logger.fine("Building options..."); + buildOptions(roundEnv, factory, options); + + logger.fine("Building parameters..."); + buildParameters(roundEnv, factory, parameters); + + logger.fine("Building parentCommands..."); + buildParentCommands(roundEnv, factory, parentCommands); + + logger.fine("Building specs..."); + buildSpecs(roundEnv, factory, specs); + + logger.fine("Building unmatched..."); + buildUnmatched(roundEnv, factory, unmatched); + + logger.fine("---------------------------"); + logger.fine("Known commands..."); + for (Map.Entry cmd : commands.entrySet()) { + logger.fine(String.format("%s has CommandSpec[name=%s]", cmd.getKey(), cmd.getValue().name())); + } + logger.fine("Known mixins..."); + for (MixinInfo mixinInfo : mixinInfoList) { + logger.fine(String.format("%s is mixin for %s", mixinInfo.mixin.userObject(), mixinInfo.mixee.userObject())); + } + + for (Map.Entry option : options.entrySet()) { + CommandSpec commandSpec = getOrCreateCommandSpecForArg(option, commands); + logger.fine("Building OptionSpec for " + option + " in spec " + commandSpec); + commandSpec.addOption(option.getValue().build()); + } + for (Map.Entry parameter : parameters.entrySet()) { + CommandSpec commandSpec = getOrCreateCommandSpecForArg(parameter, commands); + logger.fine("Building PositionalParamSpec for " + parameter); + commandSpec.addPositional(parameter.getValue().build()); + } + for (MixinInfo mixinInfo : mixinInfoList) { + mixinInfo.addMixin(); + } + + logger.fine("Found annotations: " + annotations); + //processingEnv.getMessager().printMessage(Diagnostic.Kind.MANDATORY_WARNING, "Found annotations: " + annotations); + for (TypeElement annotation : annotations) { + Set annotatedElements = roundEnv.getElementsAnnotatedWith(annotation); + //processingEnv.getMessager().printMessage(Diagnostic.Kind.WARNING, annotatedElements + " is annotated with " + annotation); + logger.finest(annotatedElements + " is annotated with " + annotation); + // … + } + + validateNoAnnotationsOnInterfaceField(roundEnv); + validateInvalidCombination(roundEnv, Mixin.class, Option.class); + validateInvalidCombination(roundEnv, Mixin.class, Parameters.class); + validateInvalidCombination(roundEnv, Mixin.class, Unmatched.class); + validateInvalidCombination(roundEnv, Mixin.class, Spec.class); + validateInvalidCombination(roundEnv, Unmatched.class, Option.class); + validateInvalidCombination(roundEnv, Unmatched.class, Parameters.class); + validateInvalidCombination(roundEnv, Spec.class, Option.class); + validateInvalidCombination(roundEnv, Spec.class, Parameters.class); + validateInvalidCombination(roundEnv, Spec.class, Unmatched.class); + validateInvalidCombination(roundEnv, Option.class, Parameters.class); + + // TODO + //validateSpecFieldTypeIsCommandSpec(roundEnv); + //validateOptionOrParametersIsNotFinalPrimitiveOrFinalString(roundEnv); + //validateUnmatchedFieldTypeIsStringArrayOrListOfString(roundEnv); + + return handleCommands(commands, annotations, roundEnv); + } + + private void validateNoAnnotationsOnInterfaceField(RoundEnvironment roundEnv) { + validateNoAnnotationsOnInterfaceField(roundEnv.getElementsAnnotatedWith(Option.class)); + validateNoAnnotationsOnInterfaceField(roundEnv.getElementsAnnotatedWith(Parameters.class)); + validateNoAnnotationsOnInterfaceField(roundEnv.getElementsAnnotatedWith(Mixin.class)); + validateNoAnnotationsOnInterfaceField(roundEnv.getElementsAnnotatedWith(ParentCommand.class)); + validateNoAnnotationsOnInterfaceField(roundEnv.getElementsAnnotatedWith(Spec.class)); + validateNoAnnotationsOnInterfaceField(roundEnv.getElementsAnnotatedWith(Unmatched.class)); + } + + private void validateNoAnnotationsOnInterfaceField(Set all) { + for (Element element : all) { + if (element.getKind() == ElementKind.FIELD && + element.getEnclosingElement().getKind() == ElementKind.INTERFACE) { + error(element, "Invalid picocli annotation on interface field %s.%s", + element.getEnclosingElement().toString(), element.getSimpleName()); + } + } + } + + private void validateInvalidCombination( + RoundEnvironment roundEnv, Class c1, Class c2) { + for (Element element : roundEnv.getElementsAnnotatedWith(c1)) { + if (element.getAnnotation(c2) != null) { + error(element, "%s cannot have both @%s and @%s annotations", + element, c1.getCanonicalName(), c2.getCanonicalName()); + } + } + } + + /** + * Subclasses must implement this method and do something with the + * {@code CommandSpec} command model objects that were found during compilation. + * + * @param commands a map of annotated elements to their associated {@code CommandSpec}. + * Note that the key set may contain classes that do not have a {@code @Command} + * annotation but were added to the map because the class has fields + * annotated with {@code Option} or {@code @Parameters}. + * @param annotations the annotation types requested to be processed + * @param roundEnv environment for information about the current and prior round + * @return whether or not the set of annotation types are claimed by this processor. + * If {@code true} is returned, the annotation types are claimed and subsequent + * processors will not be asked to process them; if {@code false} is returned, + * the annotation types are unclaimed and subsequent processors may be asked + * to process them. A processor may always return the same boolean value or + * may vary the result based on chosen criteria. + */ + protected abstract boolean handleCommands(Map commands, + Set annotations, + RoundEnvironment roundEnv); + + private CommandSpec getOrCreateCommandSpecForArg(Map.Entry argElement, + Map commands) { + Element key = argElement.getKey().getEnclosingElement(); + CommandSpec commandSpec = commands.get(key); + if (commandSpec == null) { + logger.fine("Element " + argElement.getKey() + " is enclosed by " + key + " which does not have a @Command annotation"); + commandSpec = CommandSpec.forAnnotatedObjectLenient(key); + commands.put(key, commandSpec); + } + return commandSpec; + } + + private void buildUnmatched(RoundEnvironment roundEnv, IFactory factory, Map unmatched) { + Set explicitUnmatched = roundEnv.getElementsAnnotatedWith(Unmatched.class); + for (Element element : explicitUnmatched) { + debugElement(element, "@Unmatched"); + if (element.getKind() == ElementKind.FIELD) { + + } else if (element.getKind() == ElementKind.METHOD) { + + } else if (element.getKind() == ElementKind.PARAMETER) { + + } + } + } + + private void buildSpecs(RoundEnvironment roundEnv, IFactory factory, Map specs) { + Set explicitSpecs = roundEnv.getElementsAnnotatedWith(Spec.class); + for (Element element : explicitSpecs) { + debugElement(element, "@Spec"); + if (element.getKind() == ElementKind.FIELD) { + + } else if (element.getKind() == ElementKind.METHOD) { + + } else if (element.getKind() == ElementKind.PARAMETER) { + + } + } + } + + private void buildMixins(RoundEnvironment roundEnv, + IFactory factory, + Map mixinsDeclared, + List mixinInfoList, + Map> commandTypes, + Map options, + Map parameters) { + Set explicitMixins = roundEnv.getElementsAnnotatedWith(Mixin.class); + for (Element element : explicitMixins) { + if (element.asType().getKind() != TypeKind.DECLARED) { + error(element, "@Mixin must have a declared type, not %s", element.asType()); + continue; + } + TypeElement type = (TypeElement) ((DeclaredType) element.asType()).asElement(); + CommandSpec mixin = buildCommand(type, factory, mixinsDeclared, commandTypes, options, parameters); + + logger.fine("Built mixin: " + mixin + " from " + element); + if (EnumSet.of(ElementKind.FIELD, ElementKind.PARAMETER).contains(element.getKind())) { + VariableElement variableElement = (VariableElement) element; + String name = element.getAnnotation(Mixin.class).name(); + if (name.length() == 0) { + name = variableElement.getSimpleName().toString(); + } + Element targetType = element.getEnclosingElement(); + CommandSpec mixee = buildCommand(targetType, factory, mixinsDeclared, commandTypes, options, parameters); + mixinInfoList.add(new MixinInfo(mixee, name, mixin)); + logger.fine("Mixin name=" + name + ", target command=" + mixee.userObject()); + } + } + } + + private void buildParentCommands(RoundEnvironment roundEnv, IFactory factory, Map parentCommands) { + Set explicitParentCommands = roundEnv.getElementsAnnotatedWith(ParentCommand.class); + for (Element element : explicitParentCommands) { + debugElement(element, "@ParentCommand"); + if (element.getKind() == ElementKind.FIELD) { + + } else if (element.getKind() == ElementKind.METHOD) { + + } else if (element.getKind() == ElementKind.PARAMETER) { + + } + } + } + + private void buildOptions(RoundEnvironment roundEnv, + IFactory factory, + Map options) { + Set explicitOptions = roundEnv.getElementsAnnotatedWith(Option.class); + for (Element element : explicitOptions) { + if (options.containsKey(element)) { continue; } + TypedMember typedMember = extractTypedMember(element, "@Option"); + if (typedMember != null) { + OptionSpec.Builder builder = OptionSpec.builder(typedMember, factory); + builder.completionCandidates(extractCompletionCandidates(element, element.getAnnotationMirrors())); + builder.converters(extractConverters(element, element.getAnnotationMirrors())); + options.put(element, builder); + } + } + } + + private void buildParameters(RoundEnvironment roundEnv, + IFactory factory, + Map parameters) { + Set explicitParameters = roundEnv.getElementsAnnotatedWith(Parameters.class); + for (Element element : explicitParameters) { + if (parameters.containsKey(element)) { continue; } + TypedMember typedMember = extractTypedMember(element, "@Parameters"); + if (typedMember != null) { + PositionalParamSpec.Builder builder = PositionalParamSpec.builder(typedMember, factory); + builder.completionCandidates(extractCompletionCandidates(element, element.getAnnotationMirrors())); + builder.converters(extractConverters(element, element.getAnnotationMirrors())); + parameters.put(element, builder); + } + } + } + + private TypedMember extractTypedMember(Element element, String annotation) { + debugElement(element, annotation); + if (element.getKind() == ElementKind.FIELD) { // || element.getKind() == ElementKind.PARAMETER) { + return new TypedMember((VariableElement) element, -1); + } else if (element.getKind() == ElementKind.METHOD) { + return new TypedMember((ExecutableElement) element); + } + error(element, "Cannot only process %s annotations on fields, " + + "methods and method parameters, not on %s", annotation, element.getKind()); + return null; + } + + private void buildCommands(RoundEnvironment roundEnv, + IFactory factory, + Map commands, + Map> commandTypes, + Map options, + Map parameters) { + Set explicitCommands = roundEnv.getElementsAnnotatedWith(Command.class); + for (Element element : explicitCommands) { + buildCommand(element, factory, commands, commandTypes, options, parameters); + } + } + + private CommandSpec buildCommand(Element element, + IFactory factory, + Map commands, + Map> commandTypes, + Map options, + Map parameters) { + String commandClassName = element.asType().toString(); + debugElement(element, "@Command"); + + CommandSpec result = commands.get(element); + if (result != null) { + return result; + } + result = CommandSpec.wrapWithoutInspection(element); + result.withToString(commandClassName); + commands.put(element, result); + + boolean hasCommandAnnotation = false; + boolean mixinStandardHelpOptions = false; + if (element.getKind() == ElementKind.CLASS) { + TypeElement superClass = superClassFor((TypeElement) element); + debugElement(superClass, " super"); + + TypeElement typeElement = (TypeElement) element; + Stack hierarchy = new Stack(); + int count = 0; + while (typeElement != null && count++ < 20) { + logger.fine("Adding to type hierarchy: " + typeElement); + hierarchy.add(typeElement); + typeElement = superClassFor(typeElement); + } + while (!hierarchy.isEmpty()) { + typeElement = hierarchy.pop(); + Command cmd = typeElement.getAnnotation(Command.class); + if (cmd != null) { + updateCommandAttributes(result, cmd); + + List subcommands = findSubcommands(typeElement.getAnnotationMirrors(), + factory, commands, commandTypes, options, parameters); + for (CommandSpec sub : subcommands) { + result.addSubcommand(sub.name(), sub); + } + hasCommandAnnotation = true; + } + List forSubclass = commandTypes.get(typeElement.asType()); + if (forSubclass == null) { + forSubclass = new ArrayList(); + commandTypes.put(typeElement.asType(), forSubclass); + } + forSubclass.add(result); + //hasCommandAnnotation |= initFromAnnotatedFields(instance, typeElement, result, factory); // TODO + if (cmd != null) { + mixinStandardHelpOptions |= cmd.mixinStandardHelpOptions(); + } + } + result.mixinStandardHelpOptions(mixinStandardHelpOptions); //#377 Standard help options should be added last + } else if (element.getKind() == ElementKind.METHOD) { + ExecutableElement method = (ExecutableElement) element; + debugMethod(method); + + Command cmd = method.getAnnotation(Command.class); + updateCommandAttributes(result, cmd); + result.setAddMethodSubcommands(false); + result.withToString(commandClassName + "." + method.getSimpleName()); + + // set command name to method name, unless @Command#name is set + if (result.name().equals(COMMAND_DEFAULT_NAME)) { + result.name(method.getSimpleName().toString()); + } + + Element cls = method.getEnclosingElement(); + if (cls.getAnnotation(Command.class) != null && cls.getAnnotation(Command.class).addMethodSubcommands()) { + CommandSpec commandSpec = buildCommand(cls, + factory, commands, commandTypes, options, parameters); + commandSpec.addSubcommand(result.name(), result); + } + hasCommandAnnotation = true; + result.mixinStandardHelpOptions(method.getAnnotation(Command.class).mixinStandardHelpOptions()); + buildOptionsAndPositionalsFromMethodParameters(method, result, factory, options, parameters); + } + //result.updateArgSpecMessages(); // TODO resource bundle + + // TODO run validation logic +// if (annotationsAreMandatory) { validateCommandSpec(result, hasCommandAnnotation, commandClassName); } +// result.withToString(commandClassName).validate(); + logger.fine(String.format("CommandSpec[name=%s] built for %s", result.name(), element)); + return result; + } + + private void updateCommandAttributes(CommandSpec result, Command cmd) { + // null factory to prevent + // javax.lang.model.type.MirroredTypeException: Attempt to access Class object for TypeMirror picocli.CommandLine.NoVersionProvider + result.updateCommandAttributes(cmd, null); + try { + cmd.versionProvider(); + } catch (MirroredTypeException ex) { + VersionProviderMetaData provider = new VersionProviderMetaData(ex.getTypeMirror()); + if (!provider.isDefault()) { + result.versionProvider(provider); + } + } + try { + cmd.defaultValueProvider(); + } catch (MirroredTypeException ex) { + DefaultValueProviderMetaData provider = new DefaultValueProviderMetaData(ex.getTypeMirror()); + if (!provider.isDefault()) { + result.defaultValueProvider(provider); + } + } + } + + private List findSubcommands(List annotationMirrors, + IFactory factory, + Map commands, + Map> commandTypes, + Map options, + Map parameters) { + List result = new ArrayList(); + for (AnnotationMirror am : annotationMirrors) { + if (am.getAnnotationType().toString().equals(COMMAND_TYPE)) { + for (Map.Entry entry : am.getElementValues().entrySet()) { + if ("subcommands".equals(entry.getKey().getSimpleName().toString())) { + AnnotationValue list = entry.getValue(); + + @SuppressWarnings("unchecked") + List typeMirrors = (List) list.getValue(); + registerSubcommands(typeMirrors, result, factory, commands, commandTypes, options, parameters); + break; + } + } + } + } + + return result; + } + + private void registerSubcommands(List typeMirrors, + List result, + IFactory factory, + Map commands, + Map> commandTypes, + Map options, + Map parameters) { + + for (AnnotationValue typeMirror : typeMirrors) { + Element subcommandElement = processingEnv.getElementUtils().getTypeElement( + typeMirror.getValue().toString().replace('$', '.')); + logger.fine("Processing subcommand: " + subcommandElement); + + if (isValidSubcommandHasNameAttribute(subcommandElement)) { + CommandSpec commandSpec = buildCommand(subcommandElement, + factory, commands, commandTypes, options, parameters); + result.add(commandSpec); + } + } + } + + private boolean isValidSubcommandHasNameAttribute(Element subcommandElement) { + Command annotation = subcommandElement.getAnnotation(Command.class); + if (annotation == null) { + error(subcommandElement, "Subcommand is missing @Command annotation with a name attribute"); + return false; + } else if (COMMAND_DEFAULT_NAME.equals(annotation.name())) { + error(subcommandElement, "Subcommand @Command annotation should have a name attribute"); + return false; + } + return true; + } + + private void buildOptionsAndPositionalsFromMethodParameters(ExecutableElement method, + CommandSpec result, + IFactory factory, + Map options, + Map parameters) { + List params = method.getParameters(); + int position = -1; + for (VariableElement variable : params) { + boolean isOption = variable.getAnnotation(Option.class) != null; + boolean isPositional = variable.getAnnotation(Parameters.class) != null; + boolean isMixin = variable.getAnnotation(Mixin.class) != null; + + if (isOption && isPositional) { + error(variable, "Method %s parameter %s should not have both @Option and @Parameters annotation", method.getSimpleName(), variable.getSimpleName()); + } else if ((isOption || isPositional) && isMixin) { + error(variable, "Method %s parameter %s should not have a @Mixin annotation as well as an @Option or @Parameters annotation", method.getSimpleName(), variable.getSimpleName()); + } + if (isOption) { + TypedMember typedMember = new TypedMember(variable, -1); + OptionSpec.Builder builder = OptionSpec.builder(typedMember, factory); + + builder.completionCandidates(extractCompletionCandidates(variable, variable.getAnnotationMirrors())); + builder.converters(extractConverters(variable, variable.getAnnotationMirrors())); + options.put(variable, builder); + } else if (!isMixin) { + position++; + TypedMember typedMember = new TypedMember(variable, position); + PositionalParamSpec.Builder builder = PositionalParamSpec.builder(typedMember, factory); + builder.completionCandidates(extractCompletionCandidates(variable, variable.getAnnotationMirrors())); + builder.converters(extractConverters(variable, variable.getAnnotationMirrors())); + parameters.put(variable, builder); + } + } + } + + private Iterable extractCompletionCandidates(Element element, List annotationMirrors) { + for (AnnotationMirror mirror : annotationMirrors) { + DeclaredType annotationType = mirror.getAnnotationType(); + if (isOption(annotationType) || isParameter(annotationType)) { + Map elementValues = mirror.getElementValues(); + for (ExecutableElement attribute : elementValues.keySet()) { + if ("completionCandidates".equals(attribute.getSimpleName().toString())) { + AnnotationValue typeMirror = elementValues.get(attribute); + return new CompletionCandidatesMetaData((TypeMirror) typeMirror); + } + } + } + } + return null; + } + + @SuppressWarnings("unchecked") + private TypeConverterMetaData[] extractConverters(Element element, List annotationMirrors) { + for (AnnotationMirror mirror : annotationMirrors) { + DeclaredType annotationType = mirror.getAnnotationType(); + if (isOption(annotationType) || isParameter(annotationType)) { + Map elementValues = mirror.getElementValues(); + for (ExecutableElement attribute : elementValues.keySet()) { + if ("converter".equals(attribute.getSimpleName().toString())) { + AnnotationValue list = elementValues.get(attribute); + List typeMirrors = (List) list.getValue(); + List result = new ArrayList(); + for (AnnotationValue annotationValue : typeMirrors) { + result.add(new TypeConverterMetaData((TypeMirror) annotationValue)); + } + return result.toArray(new TypeConverterMetaData[0]); + } + } + } + } + return new TypeConverterMetaData[0]; + } + + private boolean isOption(DeclaredType annotationType) { + return Option.class.getName().equals(annotationType.toString()); + } + + private boolean isParameter(DeclaredType annotationType) { + return Parameters.class.getName().equals(annotationType.toString()); + } + + private void debugMethod(ExecutableElement method) { + logger.finest(format(" method: simpleName=%s, asType=%s, varargs=%s, returnType=%s, enclosingElement=%s, params=%s, typeParams=%s", + method.getSimpleName(), method.asType(), method.isVarArgs(), method.getReturnType(), method.getEnclosingElement(), method.getParameters(), method.getTypeParameters())); + for (VariableElement variable : method.getParameters()) { + logger.finest(format(" variable: name=%s, annotationMirrors=%s, @Option=%s, @Parameters=%s", + variable.getSimpleName(), variable.getAnnotationMirrors(), variable.getAnnotation( + Option.class), variable.getAnnotation(Parameters.class))); + } + } + + private void debugElement(Element element, String s) { + logger.finest(format(s + ": kind=%s, cls=%s, simpleName=%s, type=%s, typeKind=%s, enclosed=%s, enclosing=%s", + element.getKind(), element.getClass().getName(), element.getSimpleName(), element.asType(), + element.asType().getKind(), element.getEnclosedElements(), element.getEnclosingElement())); + TypeMirror typeMirror = element.asType(); + if (element.getKind() == ENUM) { + for (Element enclosed : element.getEnclosedElements()) { + debugElement(enclosed, s + " "); + } + } else { + debugType(typeMirror, element, s + " "); + } + } + + private void debugType(TypeMirror typeMirror, Element element, String indent) { + if (indent.length() > 20) { return; } + if (typeMirror.getKind() == TypeKind.DECLARED) { + DeclaredType declaredType = (DeclaredType) typeMirror; + logger.finest(format("%stype=%s, asElement=%s, (elementKind=%s, elementClass=%s), typeArgs=%s, enclosing=%s", + indent, declaredType, + declaredType.asElement(), declaredType.asElement().getKind(), declaredType.asElement().getClass(), + declaredType.getTypeArguments(), + declaredType.getEnclosingType())); + for (TypeMirror tm : declaredType.getTypeArguments()) { + if (!tm.equals(typeMirror)) { + debugType(tm, element, indent + " "); + } + } + if (declaredType.asElement().getKind() == ENUM && !element.equals(declaredType.asElement())) { + debugElement(declaredType.asElement(), indent + " --> "); + } + } else if (typeMirror.getKind() == TypeKind.EXECUTABLE) { + ExecutableType type = (ExecutableType) typeMirror; + logger.finest(format("%stype=%s, typeArgs=%s, paramTypes=%s, returnType=%s", + indent, type, type.getTypeVariables(), + type.getParameterTypes(), type.getReturnType())); + for (TypeMirror tm : type.getParameterTypes()) { + if (!tm.equals(typeMirror)) { + debugType(tm, element, indent + " "); + } + } + } else { + logger.finest(format("%s%s %s is of kind=%s", indent, typeMirror, element.getSimpleName(), typeMirror.getKind())); + } + } + + private static class MixinInfo { + private final CommandSpec mixee; + private final String name; + private final CommandSpec mixin; + + MixinInfo(CommandSpec mixee, String name, CommandSpec mixin) { + this.mixee = mixee; + this.name = name; + this.mixin = mixin; + } + + void addMixin() { + logger.fine(String.format("Adding mixin %s to %s", mixin.name(), mixee.name())); + mixee.addMixin(name, mixin); + } + } + + static class CompileTimeTypeInfo implements ITypeInfo { + private static Logger logger = Logger.getLogger(CompileTimeTypeInfo.class.getName()); + private static final EnumSet PRIMITIVES = EnumSet.of(TypeKind.BYTE, TypeKind.BOOLEAN, TypeKind.CHAR, + TypeKind.DOUBLE, TypeKind.FLOAT, TypeKind.INT, TypeKind.LONG, TypeKind.SHORT); + + final TypeMirror typeMirror; + final List auxTypeMirrors; + final List actualGenericTypeArguments; + final TypeElement typeElement; + final boolean isCollection; + final boolean isMap; + + public CompileTimeTypeInfo(TypeMirror asType) { + typeMirror = asType; + + // for non-multi-value types, the auxiliary type is a single-value list with the type + List aux = Arrays.asList(typeMirror); + TypeElement tempTypeElement = null; + boolean collection = false; + boolean map = false; + + if (typeMirror.getKind() == TypeKind.DECLARED) { + logger.finest("CompileTimeTypeInfo DECLARED typeMirror " + typeMirror); + Element element = ((DeclaredType) typeMirror).asElement(); + if (element.getKind().isClass() || element.getKind().isInterface()) { + tempTypeElement = (TypeElement) element; + logger.finest("element is class or interface " + tempTypeElement); + map = find("java.util.Map", tempTypeElement); + collection = !map && find("java.util.Collection", tempTypeElement); + } + aux = ((DeclaredType) typeMirror).getTypeArguments(); + actualGenericTypeArguments = new ArrayList(); + for (TypeMirror typeMirror : aux) { + actualGenericTypeArguments.add(typeMirror.toString()); + } + logger.finest("aux (type args): " + aux); + if (aux.isEmpty()) { + if (map || collection) { + aux = Arrays.asList(createStringTypeMirror(), createStringTypeMirror()); + logger.finest("fixed aux (for multi type): " + aux); + } else { + aux = Arrays.asList(typeMirror); + logger.finest("fixed aux (for single type): " + aux); + } + } + } else if (typeMirror.getKind() == TypeKind.ARRAY) { + aux = Arrays.asList(((ArrayType) typeMirror).getComponentType()); + actualGenericTypeArguments = Arrays.asList(aux.get(0).toString()); + } else { + actualGenericTypeArguments = Collections.emptyList(); + } + auxTypeMirrors = aux; + typeElement = tempTypeElement; + isCollection = collection; + isMap = map; + } + + private TypeMirror createStringTypeMirror() { + TypeElement element = typeElement; + while (element.getSuperclass().getKind() != TypeKind.NONE) { + logger.finest("finding toString in " + element); + + element = (TypeElement) ((DeclaredType) element.getSuperclass()).asElement(); + } + for (Element enclosed : typeElement.getEnclosedElements()) { + if (enclosed.getKind() == ElementKind.METHOD) { + ExecutableElement method = (ExecutableElement) enclosed; + if (method.getSimpleName().contentEquals("toString")) { + return method.getReturnType(); + } + } + } + throw new IllegalStateException("Cannot find toString method in Object"); + } + private static boolean find(String interfaceName, TypeElement typeElement) { + return find(interfaceName, typeElement, new HashSet()); + } + private static boolean find(String interfaceName, TypeElement typeElement, Set visited) { + if (visited.contains(typeElement)) { return false; } + visited.add(typeElement); + //logger.finest("trying to find " + interfaceName + " in " + typeElement); + + if (typeElement.getQualifiedName().contentEquals(interfaceName)) { + return true; + } + for (TypeMirror implemented : typeElement.getInterfaces()) { + if (find(interfaceName, (TypeElement) ((DeclaredType) implemented).asElement())) { + return true; + } + } + while (typeElement.getSuperclass().getKind() != TypeKind.NONE) { + typeElement = (TypeElement) ((DeclaredType) typeElement.getSuperclass()).asElement(); + if (find(interfaceName, typeElement)) { + return true; + } + } + return false; + } + + @Override + public List getAuxiliaryTypeInfos() { + // for non-multi-value types, the auxiliary type is a single-value list with the type + if (!isMultiValue()) { + logger.fine("getAuxiliaryTypeInfos (non-multi) returning new list with this"); + return Arrays.asList(this); + } + + List result = new ArrayList(); + for (TypeMirror typeMirror : auxTypeMirrors) { + result.add(new CompileTimeTypeInfo(typeMirror)); + } + logger.fine("getAuxiliaryTypeInfos (multi) returning list " + result); + return result; + } + + @Override + public List getActualGenericTypeArguments() { + return actualGenericTypeArguments; + } + + @Override + public boolean isBoolean() { + TypeMirror type = auxTypeMirrors.get(0); + return type.getKind() == TypeKind.BOOLEAN || "java.lang.Boolean".equals(type.toString()); + } + + @Override + public boolean isMultiValue() { + return isArray() || isCollection() || isMap(); + } + + @Override + public boolean isArray() { + return typeMirror.getKind() == TypeKind.ARRAY; + } + + @Override + public boolean isCollection() { + return isCollection; + } + + @Override + public boolean isMap() { + return isMap; + } + + @Override + public boolean isEnum() { + TypeMirror type = auxTypeMirrors.get(0); + return type.getKind() == TypeKind.DECLARED && + ((DeclaredType) type).asElement().getKind() == ElementKind.ENUM; + } + + @Override + public List getEnumConstantNames() { + if (!isEnum()) { + return Collections.emptyList(); + } + List result = new ArrayList(); + TypeMirror type = auxTypeMirrors.get(0); + List enclosed = ((DeclaredType) type).asElement().getEnclosedElements(); + for (Element element : enclosed) { + if (element.getKind() == ElementKind.ENUM_CONSTANT) { + result.add(element.toString()); + } + } + return result; + } + + @Override + public String getClassName() { + return typeElement == null ? typeMirror.toString() : typeElement.getQualifiedName().toString(); + } + + @Override + public String getClassSimpleName() { + return typeElement == null ? typeMirror.toString() : typeElement.getSimpleName().toString(); + } + + @Override + public Class getType() { + return null; + } + + @Override + public Class[] getAuxiliaryTypes() { + return new Class[0]; + } + + @Override + public String toString() { + return String.format("CompileTimeTypeInfo(%s, aux=%s, collection=%s, map=%s)", + typeMirror, Arrays.toString(auxTypeMirrors.toArray()), isCollection, isMap); + } + } + + static class TypedMember implements IAnnotatedElement { + final Element element; + final String name; + final ITypeInfo typeInfo; + final boolean hasInitialValue; + final int position; + private CommandLine.Model.IGetter getter; + private CommandLine.Model.ISetter setter; + + static boolean isAnnotated(Element e) { + return false + || e.getAnnotation(Option.class) == null + || e.getAnnotation(Parameters.class) == null + || e.getAnnotation(Unmatched.class) == null + || e.getAnnotation(Mixin.class) == null + || e.getAnnotation(Spec.class) == null + || e.getAnnotation(ParentCommand.class) == null; + } + TypedMember(VariableElement variable, int position) { + element = Assert.notNull(variable, "field"); + name = variable.getSimpleName().toString(); + hasInitialValue = variable.getConstantValue() != null; + typeInfo = new CompileTimeTypeInfo(variable.asType()); + this.position = position; + getter = new GetterSetterMetaData(element); + setter = (GetterSetterMetaData) getter; + } + + private TypedMember(ExecutableElement method) { + element = Assert.notNull(method, "method"); + name = propertyName(method.getSimpleName().toString()); + position = -1; + List parameterTypes = ((ExecutableType) method.asType()).getParameterTypes(); + boolean isGetter = parameterTypes.isEmpty() && method.getReturnType().getKind() != TypeKind.VOID; + boolean isSetter = !parameterTypes.isEmpty(); + if (isSetter == isGetter) { throw new CommandLine.InitializationException("Invalid method, must be either getter or setter: " + method); } + if (isGetter) { + hasInitialValue = true; // TODO + typeInfo = new CompileTimeTypeInfo(method.getReturnType()); + //if (Proxy.isProxyClass(scope.getClass())) { + // CommandLine.Model.PicocliInvocationHandler handler = (CommandLine.Model.PicocliInvocationHandler) Proxy.getInvocationHandler(scope); + // CommandLine.Model.PicocliInvocationHandler.ProxyBinding binding = handler.new ProxyBinding(method); + // getter = binding; setter = binding; + // initializeInitialValue(method); + //} else { + // //throw new IllegalArgumentException("Getter method but not a proxy: " + scope + ": " + method); + // CommandLine.Model.MethodBinding binding = new CommandLine.Model.MethodBinding(scope, method); + // getter = binding; setter = binding; + //} + } else { + hasInitialValue = false; + typeInfo = new CompileTimeTypeInfo(parameterTypes.get(0)); + //CommandLine.Model.MethodBinding binding = new CommandLine.Model.MethodBinding(scope, method); + //getter = binding; setter = binding; + } + getter = new GetterSetterMetaData(element); + setter = (GetterSetterMetaData) getter; + } + + public Object userObject() { return element; } + public boolean isAnnotationPresent(Class annotationClass) { return getAnnotation(annotationClass) != null; } + public T getAnnotation(Class annotationClass) { return element.getAnnotation(annotationClass); } + public String getName() { return name; } + public boolean isArgSpec() { return isOption() || isParameter() || isMethodParameter(); } + public boolean isOption() { return isAnnotationPresent(Option.class); } + public boolean isParameter() { return isAnnotationPresent(Parameters.class); } + public boolean isMixin() { return isAnnotationPresent(Mixin.class); } + public boolean isUnmatched() { return isAnnotationPresent(Unmatched.class); } + public boolean isInjectSpec() { return isAnnotationPresent(Spec.class); } + public boolean isMultiValue() { return getTypeInfo().isMultiValue(); } + public ITypeInfo getTypeInfo() { return typeInfo; } + public CommandLine.Model.IGetter getter() { return getter; } + public CommandLine.Model.ISetter setter() { return setter; } + public String toString() { return element.toString(); } + public String getToString() { + if (isMixin()) { return abbreviate("mixin from member " + toGenericString()); } + return (element.getKind() + " ") + abbreviate(toGenericString()); + } + String toGenericString() { return element.asType().toString() + element.getEnclosingElement() + "." + element.getSimpleName(); } + public boolean hasInitialValue() { return hasInitialValue; } + public boolean isMethodParameter() { return position >= 0; } + public int getMethodParamPosition() { return position; } + public String getMixinName() { + String annotationName = getAnnotation(Mixin.class).name(); + return empty(annotationName) ? getName() : annotationName; + } + + static String propertyName(String methodName) { + if (methodName.length() > 3 && (methodName.startsWith("get") || methodName.startsWith("set"))) { return decapitalize(methodName.substring(3)); } + return decapitalize(methodName); + } + + private static String decapitalize(String name) { + if (name == null || name.length() == 0) { return name; } + char[] chars = name.toCharArray(); + chars[0] = Character.toLowerCase(chars[0]); + return new String(chars); + } + + static String abbreviate(String text) { + return text.replace("private ", "") + .replace("protected ", "") + .replace("public ", "") + .replace("java.lang.", ""); + } + static boolean empty(String str) { return str == null || str.trim().length() == 0; } + } + + /** + * Obtains the super type element for a given type element. + * + * @param element The type element + * @return The super type element or null if none exists + */ + static TypeElement superClassFor(TypeElement element) { + TypeMirror superclass = element.getSuperclass(); + if (superclass.getKind() == TypeKind.NONE) { + return null; + } + logger.finest(format("Superclass of %s is %s (of kind %s)", element, superclass, superclass.getKind())); + DeclaredType kind = (DeclaredType) superclass; + return (TypeElement) kind.asElement(); + } + + void error(Element e, String msg, Object... args) { + processingEnv.getMessager().printMessage( + Diagnostic.Kind.ERROR, + format(msg, args), + e); + } + + static class NullFactory implements IFactory { + @Override + public K create(Class cls) throws Exception { + return null; + } + } + +} diff --git a/picocli-codegen/src/main/java/picocli/codegen/annotation/processing/AnnotatedCommandSourceGeneratorProcessor.java b/picocli-codegen/src/main/java/picocli/codegen/annotation/processing/AnnotatedCommandSourceGeneratorProcessor.java new file mode 100644 index 000000000..6b2186fdc --- /dev/null +++ b/picocli-codegen/src/main/java/picocli/codegen/annotation/processing/AnnotatedCommandSourceGeneratorProcessor.java @@ -0,0 +1,155 @@ +package picocli.codegen.annotation.processing; + +import picocli.CommandLine.Model.CommandSpec; +import picocli.codegen.AnnotatedCommandSourceGenerator; + +import javax.annotation.processing.RoundEnvironment; +import javax.lang.model.element.Element; +import javax.lang.model.element.ElementKind; +import javax.lang.model.element.TypeElement; +import javax.tools.FileObject; +import java.io.IOException; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.io.Writer; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.logging.Logger; + +import static javax.tools.StandardLocation.SOURCE_OUTPUT; +import static picocli.codegen.AnnotatedCommandSourceGenerator.isBuiltInMixin; +import static picocli.codegen.AnnotatedCommandSourceGenerator.isBuiltInSubcommand; +import static picocli.codegen.AnnotatedCommandSourceGenerator.isNestedCommand; + +public class AnnotatedCommandSourceGeneratorProcessor extends AbstractCommandSpecProcessor { + private static Logger logger = Logger.getLogger(AnnotatedCommandSourceGeneratorProcessor.class.getName()); + + @Override + protected boolean handleCommands(Map commands, + Set annotations, + RoundEnvironment roundEnv) { + + List list = new ArrayList(); + for (Map.Entry entry : commands.entrySet()) { + SourceUnit sourceUnit = find(entry.getKey(), list); + sourceUnit.commands.add(entry.getValue()); + } + for (SourceUnit sourceUnit : list) { + try { + generateCode(sourceUnit); + } catch (IOException e) { + error(sourceUnit.topLevel, "Unable to generated code for %s: %s", sourceUnit.topLevel, e); + } + } + return true; + } + + private SourceUnit find(Element element, List list) { + for (SourceUnit sourceUnit : list) { + if (sourceUnit.contains(element)) { + return sourceUnit; + } + } + SourceUnit result = new SourceUnit(element); + list.add(result); + return result; + } + + private void generateCode(SourceUnit sourceUnit) throws IOException { + TypeElement typeElement = (TypeElement) sourceUnit.topLevel; + int count = 0; + for (CommandSpec spec : sourceUnit.commandHierarchies()) { + AnnotatedCommandSourceGenerator generator = new AnnotatedCommandSourceGenerator(spec); + generator.setOutputPackage("generated." + generator.getOutputPackage()); + + String unique = count == 0 ? "" : count + ""; + count++; + + // create a resource to prevent recursive processing of the annotations in the generated file + FileObject sourceFile = processingEnv.getFiler().createResource( + SOURCE_OUTPUT, + generator.getOutputPackage(), + typeElement.getSimpleName() + unique + ".java"); + + Writer writer = null; + try { + writer = sourceFile.openWriter(); + //PrintWriter pw = new PrintWriter(writer); + StringWriter sw = new StringWriter(); + PrintWriter pw = new PrintWriter(sw); + generator.writeTo(pw, ""); + pw.flush(); + System.out.println(sw); + writer.write(sw.toString()); + writer.flush(); + } finally { + if (writer != null) { + writer.close(); + } + } + } + } + + static class SourceUnit { + String packageName; + String className; + Element topLevel; + List commands = new ArrayList(); + + public SourceUnit(Element element) { + topLevel = topLevel(element); + } + + public boolean contains(Element element) { + Element topLevelELement = topLevel(element); + return equals(topLevelELement, topLevel); + } + + private boolean equals(Element topLevelELement, Element topLevel) { + return topLevelELement.toString().equals(topLevel.toString()); + } + + static Element topLevel(Element element) { + while (element.getEnclosingElement().getKind() != ElementKind.PACKAGE) { + element = element.getEnclosingElement(); + } + return element; + } + + public List commandHierarchies() { + List result = new ArrayList(); + for (CommandSpec cmd : commands) { + String excludeReason = null; + for (CommandSpec any : commands) { + if (cmd != any && isNestedCommand(cmd, any)) { + // TODO Exclude if nested in shared surrounding element + excludeReason = "Excluding " + cmd + ": it is nested in " + any; + break; + } + if (isBuiltInMixin(cmd) || isBuiltInSubcommand(cmd)) { + excludeReason = "Excluding built-in " + cmd.userObject(); + break; + } + } + if (excludeReason == null) { + result.add(cmd); + } else { + logger.info(excludeReason); + } + } + return result; + } + } + + private Map removeNested(Map commands) { + Map result = new LinkedHashMap(); + for (Map.Entry entry : commands.entrySet()) { + CommandSpec cmd = entry.getValue(); + String excludeReason = null; + } + return result; + } +} diff --git a/picocli-codegen/src/main/java/picocli/codegen/annotation/processing/ITypeMetaData.java b/picocli-codegen/src/main/java/picocli/codegen/annotation/processing/ITypeMetaData.java new file mode 100644 index 000000000..a4eb4e998 --- /dev/null +++ b/picocli-codegen/src/main/java/picocli/codegen/annotation/processing/ITypeMetaData.java @@ -0,0 +1,34 @@ +package picocli.codegen.annotation.processing; + +import javax.lang.model.element.TypeElement; +import javax.lang.model.type.DeclaredType; +import javax.lang.model.type.TypeMirror; + +/** + * Abstraction over annotation attributes that take a class (or array of classes) as their value. + * For example: + * {@code @Command(defaultValueProvider = xxx.class)} + * + * @since 4.0 + */ +public interface ITypeMetaData { + + /** + * Returns {@code true} if the annotated element did not have the annotation attribute. + * @return {@code true} if the value is the default value. + */ + boolean isDefault(); + + /** + * Returns the TypeMirror of the value. + * @return the TypeMirror of the {@code @Command(defaultValueProvider = xxx.class)} annotation. + */ + TypeMirror getTypeMirror(); + + /** + * Returns the {@link TypeElement} of the {@link #getTypeMirror() type mirror}. + * @return the type mirror as a TypeElement + */ + TypeElement getTypeElement(); + +} diff --git a/picocli-codegen/src/main/java/picocli/codegen/annotation/processing/JulLogFormatter.java b/picocli-codegen/src/main/java/picocli/codegen/annotation/processing/JulLogFormatter.java new file mode 100644 index 000000000..a7aab64e2 --- /dev/null +++ b/picocli-codegen/src/main/java/picocli/codegen/annotation/processing/JulLogFormatter.java @@ -0,0 +1,23 @@ +package picocli.codegen.annotation.processing; + +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.logging.Formatter; +import java.util.logging.LogRecord; + +class JulLogFormatter extends Formatter { + SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss.SSS"); + + @Override + public synchronized String format(LogRecord record) { + StringBuilder sb = new StringBuilder(); + sb.append(sdf.format(new Date(record.getMillis()))).append(" "); + sb.append(record.getLevel()).append(" "); + sb.append("["); + sb.append(record.getSourceClassName()).append(".").append(record.getSourceMethodName()); + sb.append("] "); + sb.append(record.getMessage()); + sb.append(System.getProperty("line.separator")); + return sb.toString(); + } +} diff --git a/picocli-codegen/src/main/java/picocli/codegen/annotation/processing/internal/CompletionCandidatesMetaData.java b/picocli-codegen/src/main/java/picocli/codegen/annotation/processing/internal/CompletionCandidatesMetaData.java new file mode 100644 index 000000000..78531af13 --- /dev/null +++ b/picocli-codegen/src/main/java/picocli/codegen/annotation/processing/internal/CompletionCandidatesMetaData.java @@ -0,0 +1,60 @@ +package picocli.codegen.annotation.processing.internal; + +import picocli.codegen.annotation.processing.ITypeMetaData; +import picocli.codegen.util.Assert; + +import javax.lang.model.element.TypeElement; +import javax.lang.model.type.DeclaredType; +import javax.lang.model.type.TypeMirror; +import java.util.Iterator; + +/** + * Implementation of the {@link Iterable} interface that provides metadata on the + * {@code @Command(completionCandidates = xxx.class)} annotation. + * + * @since 4.0 + */ +public class CompletionCandidatesMetaData implements Iterable, ITypeMetaData { + + private final TypeMirror typeMirror; + + public CompletionCandidatesMetaData(TypeMirror typeMirror) { + this.typeMirror = Assert.notNull(typeMirror, "typeMirror"); + } + + /** + * Returns {@code true} if the command did not have a {@code completionCandidates} annotation attribute. + * @return {@code true} if the command did not have a {@code completionCandidates} annotation attribute. + */ + public boolean isDefault() { + return false; + } + + /** + * Returns the TypeMirror that this TypeConverterMetaData was constructed with. + * @return the TypeMirror of the {@code @Command(completionCandidates = xxx.class)} annotation. + */ + public TypeMirror getTypeMirror() { + return typeMirror; + } + + public TypeElement getTypeElement() { + return (TypeElement) ((DeclaredType) typeMirror).asElement(); + } + + /** Always returns {@code null}. */ + @Override + public Iterator iterator() { + return null; + } + + /** + * Returns a string representation of this object, for debugging purposes. + * @return a string representation of this object + */ + @Override + public String toString() { + return String.format("%s(%s)", getClass().getSimpleName(), isDefault() ? "default" : typeMirror); + } + +} diff --git a/picocli-codegen/src/main/java/picocli/codegen/annotation/processing/internal/DefaultValueProviderMetaData.java b/picocli-codegen/src/main/java/picocli/codegen/annotation/processing/internal/DefaultValueProviderMetaData.java new file mode 100644 index 000000000..77226fe5b --- /dev/null +++ b/picocli-codegen/src/main/java/picocli/codegen/annotation/processing/internal/DefaultValueProviderMetaData.java @@ -0,0 +1,61 @@ +package picocli.codegen.annotation.processing.internal; + +import picocli.CommandLine.IDefaultValueProvider; +import picocli.CommandLine.Model.ArgSpec; +import picocli.codegen.annotation.processing.ITypeMetaData; +import picocli.codegen.util.Assert; + +import javax.lang.model.element.TypeElement; +import javax.lang.model.type.DeclaredType; +import javax.lang.model.type.TypeMirror; + +/** + * Implementation of the {@link IDefaultValueProvider} interface that provides metadata on the + * {@code @Command(defaultValueProvider = xxx.class)} annotation. + * + * @since 4.0 + */ +public class DefaultValueProviderMetaData implements IDefaultValueProvider, ITypeMetaData { + + private final TypeMirror typeMirror; + + public DefaultValueProviderMetaData() { + this.typeMirror = null; + } + public DefaultValueProviderMetaData(TypeMirror typeMirror) { + this.typeMirror = Assert.notNull(typeMirror, "typeMirror"); + } + + /** + * Returns {@code true} if the command did not have a {@code defaultValueProvider} annotation attribute. + * @return {@code true} if the command did not have a {@code defaultValueProvider} annotation attribute. + */ + public boolean isDefault() { + return typeMirror == null || "picocli.CommandLine.NoDefaultProvider".equals(getTypeElement().getQualifiedName().toString()); + } + + /** + * Returns the TypeMirror that this DefaultValueProviderMetaData was constructed with. + * @return the TypeMirror of the {@code @Command(defaultValueProvider = xxx.class)} annotation. + */ + public TypeMirror getTypeMirror() { + return typeMirror; + } + + public TypeElement getTypeElement() { + return (TypeElement) ((DeclaredType) typeMirror).asElement(); + } + + /** Always returns {@code null}. */ + @Override + public String defaultValue(ArgSpec argSpec) { return null; } + + /** + * Returns a string representation of this object, for debugging purposes. + * @return a string representation of this object + */ + @Override + public String toString() { + return String.format("%s(%s)", getClass().getSimpleName(), isDefault() ? "default" : typeMirror); + } +} diff --git a/picocli-codegen/src/main/java/picocli/codegen/annotation/processing/internal/GetterSetterMetaData.java b/picocli-codegen/src/main/java/picocli/codegen/annotation/processing/internal/GetterSetterMetaData.java new file mode 100644 index 000000000..218584a4a --- /dev/null +++ b/picocli-codegen/src/main/java/picocli/codegen/annotation/processing/internal/GetterSetterMetaData.java @@ -0,0 +1,64 @@ +package picocli.codegen.annotation.processing.internal; + +import picocli.CommandLine.Model.IGetter; +import picocli.CommandLine.Model.ISetter; + +import javax.lang.model.element.Element; + +/** + * Implementation of the {@link IGetter} and {@link ISetter} interface that allows + * custom {@code CommandSpec} annotation processors to inspect {@code ArgSpec} objects + * to discover what program element was annotated with {@code @Option} or {@code @Parameters}. + * + * @since 4.0 + */ +public class GetterSetterMetaData implements IGetter, ISetter { + + private final Element element; + + /** + * Constructs a new {@code GetterSetterMetaData} with the specified element + * @param element the program element annotated with {@code @Option} or {@code @Parameters} + */ + public GetterSetterMetaData(Element element) { + this.element = element; + } + + /** + * Returns the program element annotated with {@code @Option} or {@code @Parameters}. + * @return the program element for an {@code ArgSpec}. + */ + public Element getElement() { + return element; + } + + /** + * This implementation does nothing and always returns {@code null}. + * @param ignored + * @return {@code null} always + */ + @Override + public T get() { + return null; + } + + /** + * This implementation does nothing. + * @param value the new value of the option or positional parameter. Ignored. + * @param ignored + * @return {@code null} always + */ + @Override + public T set(T value) { + return null; + } + + /** + * Returns a string representation of this binding, for debugging purposes. + * @return a string representation of this binding + */ + @Override + public String toString() { + return String.format("%s(%s %s)", getClass().getCanonicalName(), element.getKind(), element); + } +} diff --git a/picocli-codegen/src/main/java/picocli/codegen/annotation/processing/internal/TypeConverterMetaData.java b/picocli-codegen/src/main/java/picocli/codegen/annotation/processing/internal/TypeConverterMetaData.java new file mode 100644 index 000000000..7a32edadc --- /dev/null +++ b/picocli-codegen/src/main/java/picocli/codegen/annotation/processing/internal/TypeConverterMetaData.java @@ -0,0 +1,57 @@ +package picocli.codegen.annotation.processing.internal; + +import picocli.CommandLine.ITypeConverter; +import picocli.codegen.annotation.processing.ITypeMetaData; +import picocli.codegen.util.Assert; + +import javax.lang.model.element.TypeElement; +import javax.lang.model.type.DeclaredType; +import javax.lang.model.type.TypeMirror; + +/** + * Implementation of the {@link ITypeConverter} interface that provides metadata on the + * {@code @Command(typeConverter = xxx.class)} annotation. + * + * @since 4.0 + */ +public class TypeConverterMetaData implements ITypeConverter, ITypeMetaData { + + private final TypeMirror typeMirror; + + public TypeConverterMetaData(TypeMirror typeMirror) { + this.typeMirror = Assert.notNull(typeMirror, "typeMirror"); + } + + /** + * Returns {@code true} if the command did not have a {@code typeConverter} annotation attribute. + * @return {@code true} if the command did not have a {@code typeConverter} annotation attribute. + */ + public boolean isDefault() { + return false; + } + + /** + * Returns the TypeMirror that this TypeConverterMetaData was constructed with. + * @return the TypeMirror of the {@code @Command(typeConverter = xxx.class)} annotation. + */ + public TypeMirror getTypeMirror() { + return typeMirror; + } + + public TypeElement getTypeElement() { + return (TypeElement) ((DeclaredType) typeMirror).asElement(); + } + + /** Always returns {@code null}. */ + @Override + public Object convert(String value) { return null; } + + /** + * Returns a string representation of this object, for debugging purposes. + * @return a string representation of this object + */ + @Override + public String toString() { + return String.format("%s(%s)", getClass().getSimpleName(), isDefault() ? "default" : typeMirror); + } +} diff --git a/picocli-codegen/src/main/java/picocli/codegen/annotation/processing/internal/VersionProviderMetaData.java b/picocli-codegen/src/main/java/picocli/codegen/annotation/processing/internal/VersionProviderMetaData.java new file mode 100644 index 000000000..581350692 --- /dev/null +++ b/picocli-codegen/src/main/java/picocli/codegen/annotation/processing/internal/VersionProviderMetaData.java @@ -0,0 +1,60 @@ +package picocli.codegen.annotation.processing.internal; + +import picocli.CommandLine.IVersionProvider; +import picocli.codegen.annotation.processing.ITypeMetaData; +import picocli.codegen.util.Assert; + +import javax.lang.model.element.TypeElement; +import javax.lang.model.type.DeclaredType; +import javax.lang.model.type.TypeMirror; + +/** + * Implementation of the {@link IVersionProvider} interface that provides metadata on the + * {@code @Command(versionProvider = xxx.class)} annotation. + * + * @since 4.0 + */ +public class VersionProviderMetaData implements IVersionProvider, ITypeMetaData { + + private final TypeMirror typeMirror; + + public VersionProviderMetaData() { + this.typeMirror = null; + } + public VersionProviderMetaData(TypeMirror typeMirror) { + this.typeMirror = Assert.notNull(typeMirror, "typeMirror"); + } + + /** + * Returns {@code true} if the command did not have a {@code versionProvider} annotation attribute. + * @return {@code true} if the command did not have a {@code versionProvider} annotation attribute. + */ + public boolean isDefault() { + return typeMirror == null || "picocli.CommandLine.NoVersionProvider".equals(getTypeElement().getQualifiedName().toString()); + } + + /** + * Returns the TypeMirror that this VersionProviderMetaData was constructed with. + * @return the TypeMirror of the {@code @Command(versionProvider = xxx.class)} annotation. + */ + public TypeMirror getTypeMirror() { + return typeMirror; + } + + public TypeElement getTypeElement() { + return (TypeElement) ((DeclaredType) typeMirror).asElement(); + } + + /** Always returns an empty array. */ + @Override + public String[] getVersion() { return new String[0]; } + + /** + * Returns a string representation of this object, for debugging purposes. + * @return a string representation of this object + */ + @Override + public String toString() { + return String.format("%s(%s)", getClass().getSimpleName(), isDefault() ? "default" : typeMirror); + } +} diff --git a/picocli-codegen/src/main/java/picocli/codegen/aot/graalvm/ReflectionConfigGenerator.java b/picocli-codegen/src/main/java/picocli/codegen/aot/graalvm/ReflectionConfigGenerator.java index d34235f86..012dc8878 100644 --- a/picocli-codegen/src/main/java/picocli/codegen/aot/graalvm/ReflectionConfigGenerator.java +++ b/picocli-codegen/src/main/java/picocli/codegen/aot/graalvm/ReflectionConfigGenerator.java @@ -145,6 +145,13 @@ static final class Visitor { getOrCreateClassName("java.lang.reflect.Executable").addMethod("getParameters"); getOrCreateClassName("java.lang.reflect.Parameter").addMethod("getName"); + // ANSI color enabled detection + getOrCreateClassName("java.lang.System").addMethod("console"); + getOrCreateClassName("org.fusesource.jansi.AnsiConsole").addField("out"); + + // picocli 4.0 + getOrCreateClassName("java.util.ResourceBundle").addMethod("getBaseBundleName"); + // type converters registered with reflection getOrCreateClassName("java.time.Duration").addMethod("parse", CharSequence.class); getOrCreateClassName("java.time.Instant").addMethod("parse", CharSequence.class); diff --git a/picocli-codegen/src/main/java/picocli/codegen/util/Assert.java b/picocli-codegen/src/main/java/picocli/codegen/util/Assert.java new file mode 100644 index 000000000..5c5379334 --- /dev/null +++ b/picocli-codegen/src/main/java/picocli/codegen/util/Assert.java @@ -0,0 +1,25 @@ +package picocli.codegen.util; + +/** + * Utility class providing some defensive coding convenience methods. + */ +public final class Assert { + /** + * Throws a NullPointerException if the specified object is null. + * @param object the object to verify + * @param description error message + * @param type of the object to check + * @return the verified object + */ + public static T notNull(T object, String description) { + if (object == null) { + throw new NullPointerException(description); + } + return object; + } + public static boolean equals(Object obj1, Object obj2) { return obj1 == null ? obj2 == null : obj1.equals(obj2); } + public static int hashCode(Object obj) {return obj == null ? 0 : obj.hashCode(); } + public static int hashCode(boolean bool) {return bool ? 1 : 0; } + + private Assert() {} // private constructor: never instantiate +} diff --git a/picocli-codegen/src/main/java/picocli/codegen/util/Resources.java b/picocli-codegen/src/main/java/picocli/codegen/util/Resources.java new file mode 100644 index 000000000..59fc87ac9 --- /dev/null +++ b/picocli-codegen/src/main/java/picocli/codegen/util/Resources.java @@ -0,0 +1,35 @@ +package picocli.codegen.util; + +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; +import java.util.Scanner; + +public final class Resources { + + public static List slurpAll(String... resources) { + List result = new ArrayList(); + for (String resource : resources) { + result.add(slurp(resource)); + } + return result; + } + + public static String slurp(String resource) { + try { + return slurp(Resources.class.getResource(resource).openStream()); + } catch (IOException ex) { + throw new IllegalStateException(ex); + } + } + + public static String slurp(InputStream in) { + Scanner s = new Scanner(in).useDelimiter("\\A"); + return s.hasNext() ? s.next() : ""; + } + + private Resources() { + // utility class; don't instantiate + } +} diff --git a/picocli-codegen/src/main/java/picocli/codegen/util/TypeImporter.java b/picocli-codegen/src/main/java/picocli/codegen/util/TypeImporter.java new file mode 100644 index 000000000..42999e8ed --- /dev/null +++ b/picocli-codegen/src/main/java/picocli/codegen/util/TypeImporter.java @@ -0,0 +1,375 @@ +package picocli.codegen.util; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Set; +import java.util.SortedSet; +import java.util.TreeSet; + +public class TypeImporter { + private static Set javaDefaultTypes; + + private Set registeredPackages = new HashSet(); + private SortedSet imports = new TreeSet(); + + /** + * Creates an type importer for a compilation unit in the given package. + * @param outputPackage the package of the compilation unit + */ + public TypeImporter(String outputPackage) { + registeredPackages.add(makeValid(outputPackage)); + } + + /* + * Removes all code points that are not valid Java identifier parts from the given string if there is any. + * The string is returned unchanged otherwise. + */ + private static String makeValid(String s) { + StringBuilder result = new StringBuilder(s.length()); + for (int ch = 0, i = 0; i < s.length(); i += Character.charCount(ch)) { + ch = s.codePointAt(i); + if (Character.isJavaIdentifierPart(ch) || ch > ' ') { + result.appendCodePoint(ch); + } + } + return result.toString(); + } + + public String getImportedName(String fullyQualifiedClass) { + int i = fullyQualifiedClass.indexOf('<'); + if (i == -1) { + return register(fullyQualifiedClass).getSimpleName(); + } + + StringBuilder result = new StringBuilder(); + for (int start = 0, end = fullyQualifiedClass.length(); i < end; i++) { + char c = fullyQualifiedClass.charAt(i); + switch (c) { + case ',': + case '<': + case '>': + case '&': + case '[': + case ']': { + if (start != i) { + String potentialClassName = fullyQualifiedClass.substring(start, i); + if (!isTypeVariable(potentialClassName)) { + Generic clean = new Generic(potentialClassName); + result.append(clean.specialization + register(clean.typeName).getSimpleName()); + } else { + result.append(potentialClassName); + } + } + result.append(c); + start = i + 1; + break; + } + case '?': { + int j = i + 1; + while (j < end && Character.isWhitespace(fullyQualifiedClass.charAt(j))) { + j++; + } + if (j + 6 < end && "extends".equals(fullyQualifiedClass.substring(j, j + 7))) { + result.append(fullyQualifiedClass.substring(i, j + 7)); + i = j + 6; + } else if (j + 4 < end && "super".equals(fullyQualifiedClass.substring(j, j + 5))) { + result.append(fullyQualifiedClass.substring(i, j + 5)); + i = j + 4; + } else { + result.append(c); + } + start = i + 1; + break; + } + default: { + if (Character.isWhitespace(c) && start == i) { + result.append(c); + start++; + } + break; + } + } + } + return result.toString(); + } + + private boolean isTypeVariable(String potentialClassName) { + return potentialClassName.length() == 1; + } + + private TypeName register(String typeName) { + return register(new TypeName(typeName)); + } + private TypeName register(TypeName typeName) { + String pkg = typeName.getPackageName(); + + if (typeName.getSimpleName().equals("*")) { + registeredPackages.add(pkg); + imports.add(typeName); + } else if (shouldImport(typeName)) { + if ("".equals(pkg) && typeName.findMatchingSimpleName(imports) != null) { + return typeName; // already registered + } + if (!registeredPackages.contains(pkg)) { + imports.add(typeName); + } + } + + return typeName; + } + + /** + * Determines whether the given non-wildcard import should be added. + * By default, this returns false if the simple name is a built-in Java language type name. + */ + private boolean shouldImport(TypeName typeName) { + // don't import classes from the java.lang package + String pkg = typeName.getPackageName(); + String simpleName = typeName.getSimpleName(); + boolean exclude = (pkg.equals("java.lang") || pkg.equals("")) && getJavaDefaultTypes().contains(simpleName); + return !exclude; + } + + /** + * Returns the set of simple names of the primitive types and types in the java.lang package + * (classes that don't need to be imported). + */ + static Set getJavaDefaultTypes() { + if (javaDefaultTypes == null) { + javaDefaultTypes = Collections.unmodifiableSet(new HashSet(Arrays.asList( + "AbstractMethodError", + "Appendable", + "ArithmeticException", + "ArrayIndexOutOfBoundsException", + "ArrayStoreException", + "AssertionError", + "AutoCloseable", + "Boolean", + "BootstrapMethodError", + "Byte", + "Character", + "CharSequence", + "Class", + "ClassCastException", + "ClassCircularityError", + "ClassFormatError", + "ClassLoader", + "ClassNotFoundException", + "ClassValue", + "Cloneable", + "CloneNotSupportedException", + "Comparable", + "Compiler", + "Deprecated", + "Double", + "Enum", + "EnumConstantNotPresentException", + "Error", + "Exception", + "ExceptionInInitializerError", + "Float", + "FunctionalInterface", + "IllegalAccessError", + "IllegalAccessException", + "IllegalArgumentException", + "IllegalCallerException", + "IllegalMonitorStateException", + "IllegalStateException", + "IllegalThreadStateException", + "IncompatibleClassChangeError", + "IndexOutOfBoundsException", + "InheritableThreadLocal", + "InstantiationError", + "InstantiationException", + "Integer", + "InternalError", + "InterruptedException", + "Iterable", + "LayerInstantiationException", + "LinkageError", + "Long", + "Math", + "Module", + "ModuleLayer", + "NegativeArraySizeException", + "NoClassDefFoundError", + "NoSuchFieldError", + "NoSuchFieldException", + "NoSuchMethodError", + "NoSuchMethodException", + "NullPointerException", + "Number", + "NumberFormatException", + "Object", + "OutOfMemoryError", + "Override", + "Package", + "Process", + "ProcessBuilder", + "ProcessHandle", + "Readable", + "ReflectiveOperationException", + "Runnable", + "Runtime", + "RuntimeException", + "RuntimePermission", + "SafeVarargs", + "SecurityException", + "SecurityManager", + "Short", + "StackOverflowError", + "StackTraceElement", + "StackWalker", + "StrictMath", + "String", + "StringBuffer", + "StringBuilder", + "StringIndexOutOfBoundsException", + "SuppressWarnings", + "System", + "Thread", + "ThreadDeath", + "ThreadGroup", + "ThreadLocal", + "Throwable", + "TypeNotPresentException", + "UnknownError", + "UnsatisfiedLinkError", + "UnsupportedClassVersionError", + "UnsupportedOperationException", + "VerifyError", + "VirtualMachineError", + "Void", + "boolean", + "byte", + "char", + "double", + "float", + "int", + "long", + "short", + "void"))); + } + return javaDefaultTypes; + } + + /** + * Returns a string with the import declarations to add to the class, using the system line separator. + */ + public String createImportDeclaration() { + return createImportDeclaration(System.getProperty("line.separator")); + } + /** + * Returns a string with the import declarations to add to the class, + * using the specified line separator to separate import lines. + */ + public String createImportDeclaration(String lineDelimiter) { + StringBuilder result = new StringBuilder(); + for (TypeName importName : getImports()) { + result.append(lineDelimiter + "import " + importName + ";"); + } + return result.toString(); + } + + private SortedSet getImports() { + compactImports(); + return imports; + } + + /* + * Removes explicit imports covered by wildcards. + */ + private void compactImports() { + Iterator i = imports.iterator(); + while (i.hasNext()) { + TypeName importName = i.next(); + if (!importName.getSimpleName().equals("*") && registeredPackages.contains(importName.getPackageName())) { + i.remove(); + } + } + } + + static class TypeName implements Comparable { + private final String qualifiedName; + private final String packageName; + private final String simpleName; + private final String importName; + + TypeName(String qualifiedName) { + this.qualifiedName = qualifiedName; + String canonical = qualifiedName.replaceAll("\\$", "."); + int dot = canonical.lastIndexOf('.'); + packageName = dot == -1 ? "" : makeValid(canonical.substring(0, dot)); + + int from = dot + 1; + int end = canonical.indexOf('[', from); + if (end == -1) { + end = canonical.length(); + } + simpleName = makeValid(canonical.substring(from, end)); + importName = dot == -1 ? simpleName : packageName + "." + simpleName; + } + + /* + * Returns the normalized package name of this type name. + */ + private String getPackageName() { + return packageName; + } + + /* + * Returns the last segment (the part following the last '.') of this type name. + * For member classes, this method returns the enclosing top-level class, unless + * the member class was registered by its canonical name, in which case the simple name of the member class is returned. + */ + private String getSimpleName() { + return simpleName; + } + + public String getImportName() { + return importName; + } + + @Override + public int compareTo(TypeName o) { + return this.qualifiedName.compareTo(o.qualifiedName); + } + + @Override + public String toString() { + return qualifiedName; + } + + public TypeName findMatchingSimpleName(Collection imports) { + for (TypeName typeName: imports) { + if (typeName.getSimpleName().equals(getSimpleName())) { + return typeName; + } + } + return null; + } + } + + static class Generic { + String specialization; + String typeName; + + Generic(String name) { + specialization = ""; + typeName = name; + int i = name.indexOf("extends"); + if (i > 0 && Character.isWhitespace(name.charAt(i - 1)) && Character.isWhitespace(name.charAt(i + + "extends".length()))) { + typeName = name.substring(i + "extends".length()).trim(); + specialization = name.substring(0, name.indexOf(typeName)); + } + i = name.indexOf("super"); + if (i > 0 && Character.isWhitespace(name.charAt(i - 1)) && Character.isWhitespace(name.charAt(i + + "super".length()))) { + typeName = name.substring(i + "super".length()).trim(); + specialization = name.substring(0, name.indexOf(typeName)); + } + } + } +} diff --git a/picocli-codegen/src/test/java/picocli/codegen/AnnotatedCommandSourceGeneratorTest.java b/picocli-codegen/src/test/java/picocli/codegen/AnnotatedCommandSourceGeneratorTest.java new file mode 100644 index 000000000..2b0d67189 --- /dev/null +++ b/picocli-codegen/src/test/java/picocli/codegen/AnnotatedCommandSourceGeneratorTest.java @@ -0,0 +1,23 @@ +package picocli.codegen; + +import org.junit.Ignore; +import org.junit.Test; +import picocli.CommandLine.Model.CommandSpec; +import picocli.codegen.aot.graalvm.Example; +import picocli.codegen.util.Resources; + +import static org.junit.Assert.*; + +public class AnnotatedCommandSourceGeneratorTest { + + @Ignore + @Test + public void generate() { + CommandSpec spec = CommandSpec.forAnnotatedObject(Example.class); + String generated = new AnnotatedCommandSourceGenerator(spec).generate(); + //System.out.println(generated); + + String expected = Resources.slurp("/picocli/codegen/aot/graalvm/Example.txt"); + assertEquals(expected, generated); + } +} diff --git a/picocli-codegen/src/test/java/picocli/codegen/aot/graalvm/Example.java b/picocli-codegen/src/test/java/picocli/codegen/aot/graalvm/Example.java index 291093eea..4cec8ef67 100644 --- a/picocli-codegen/src/test/java/picocli/codegen/aot/graalvm/Example.java +++ b/picocli-codegen/src/test/java/picocli/codegen/aot/graalvm/Example.java @@ -41,7 +41,7 @@ public class Example implements Runnable { List unmatched; private int minimum; - private File[] otherFiles; + private List otherFiles; @Command int multiply(@Option(names = "--count") int count, @@ -59,7 +59,7 @@ public void setMinimum(int min) { } @Parameters(index = "1..*") - public void setOtherFiles(File[] otherFiles) { + public void setOtherFiles(List otherFiles) { for (File f : otherFiles) { if (!f.exists()) { throw new ParameterException(spec.commandLine(), "File " + f.getAbsolutePath() + " must exist"); @@ -70,7 +70,7 @@ public void setOtherFiles(File[] otherFiles) { public void run() { System.out.printf("timeUnit=%s, length=%s, file=%s, unmatched=%s, minimum=%s, otherFiles=%s%n", - timeUnit, mixin.length, file, unmatched, minimum, Arrays.toString(otherFiles)); + timeUnit, mixin.length, file, unmatched, minimum, otherFiles); } public static void main(String[] args) { diff --git a/picocli-codegen/src/test/java/picocli/codegen/util/TypeImporterTest.java b/picocli-codegen/src/test/java/picocli/codegen/util/TypeImporterTest.java new file mode 100644 index 000000000..bd35cebc6 --- /dev/null +++ b/picocli-codegen/src/test/java/picocli/codegen/util/TypeImporterTest.java @@ -0,0 +1,67 @@ +package picocli.codegen.util; + +import org.junit.Test; + +import static org.junit.Assert.*; + +public class TypeImporterTest { + + String[][] data = { + {"java.util.Map, java.lang.String[]>", "Map, String[]>"}, + {"my.pkg.MethodAnalysisClassVisitor>", "MethodAnalysisClassVisitor>"}, + {"my.pkg.util.IterableToCollectionSelector, C extends my.pkg.util.Condition1, R extends java.util.Collection>", + "IterableToCollectionSelector, C extends Condition1, R extends Collection>"}, + {"java.util.List", "List"}, + {"int", "int"}, + {"void", "void"}, + }; + + @Test + public void getImportedName() { + TypeImporter importer = new TypeImporter("a.b.c"); + for (String[] tuple : data) { + String actual = importer.getImportedName(tuple[0]); + assertEquals(tuple[1], actual); + } + } + + @Test + public void createImportDeclaration() { + TypeImporter importer = new TypeImporter("a.b.c"); + for (String[] tuple : data) { + importer.getImportedName(tuple[0]); + } + String expected = String.format("" + + "%n" + + "import java.lang.reflect.Constructor;%n" + + "import java.math.BigDecimal;%n" + + "import java.util.Collection;%n" + + "import java.util.Iterable;%n" + + "import java.util.List;%n" + + "import java.util.Map;%n" + + "import my.pkg.MethodAnalysisClassVisitor;%n" + + "import my.pkg.XsuperT;%n" + + "import my.pkg.util.Condition1;%n" + + "import my.pkg.util.IterableToCollectionSelector;"); + assertEquals(expected, importer.createImportDeclaration()); + } + + @Test + public void createImportDeclaration1() { + TypeImporter importer = new TypeImporter("my.pkg"); + for (String[] tuple : data) { + importer.getImportedName(tuple[0]); + } + String expected = "" + + "\n" + + "import java.lang.reflect.Constructor;\n" + + "import java.math.BigDecimal;\n" + + "import java.util.Collection;\n" + + "import java.util.Iterable;\n" + + "import java.util.List;\n" + + "import java.util.Map;\n" + + "import my.pkg.util.Condition1;\n" + + "import my.pkg.util.IterableToCollectionSelector;"; + assertEquals(expected, importer.createImportDeclaration("\n")); + } +} \ No newline at end of file diff --git a/picocli-codegen/src/test/resources/example-reflect.json b/picocli-codegen/src/test/resources/example-reflect.json index fde383acc..bced1586c 100644 --- a/picocli-codegen/src/test/resources/example-reflect.json +++ b/picocli-codegen/src/test/resources/example-reflect.json @@ -26,6 +26,36 @@ { "name" : "getName", "parameterTypes" : [] } ] }, + { + "name" : "java.lang.System", + "allDeclaredConstructors" : true, + "allPublicConstructors" : true, + "allDeclaredMethods" : true, + "allPublicMethods" : true, + "methods" : [ + { "name" : "console", "parameterTypes" : [] } + ] + }, + { + "name" : "org.fusesource.jansi.AnsiConsole", + "allDeclaredConstructors" : true, + "allPublicConstructors" : true, + "allDeclaredMethods" : true, + "allPublicMethods" : true, + "fields" : [ + { "name" : "out" } + ] + }, + { + "name" : "java.util.ResourceBundle", + "allDeclaredConstructors" : true, + "allPublicConstructors" : true, + "allDeclaredMethods" : true, + "allPublicMethods" : true, + "methods" : [ + { "name" : "getBaseBundleName", "parameterTypes" : [] } + ] + }, { "name" : "java.time.Duration", "allDeclaredConstructors" : true, @@ -233,7 +263,7 @@ ], "methods" : [ { "name" : "setMinimum", "parameterTypes" : ["int"] }, - { "name" : "setOtherFiles", "parameterTypes" : ["[Ljava.io.File;"] }, + { "name" : "setOtherFiles", "parameterTypes" : ["java.util.List"] }, { "name" : "multiply", "parameterTypes" : ["int", "int"] } ] }, @@ -287,7 +317,7 @@ "allPublicMethods" : true }, { - "name" : "[Ljava.io.File;", + "name" : "java.util.List", "allDeclaredConstructors" : true, "allPublicConstructors" : true, "allDeclaredMethods" : true, diff --git a/picocli-codegen/src/test/resources/picocli/codegen/aot/graalvm/Example.txt b/picocli-codegen/src/test/resources/picocli/codegen/aot/graalvm/Example.txt new file mode 100644 index 000000000..9c0db6cfb --- /dev/null +++ b/picocli-codegen/src/test/resources/picocli/codegen/aot/graalvm/Example.txt @@ -0,0 +1,49 @@ +package picocli.codegen.aot.graalvm; + +import java.io.File; +import java.util.List; +import java.util.concurrent.TimeUnit; +import picocli.CommandLine.Command; +import picocli.CommandLine.HelpCommand; +import picocli.CommandLine.Mixin; +import picocli.CommandLine.Option; +import picocli.CommandLine.Parameters; +import picocli.codegen.aot.graalvm.Example.ExampleMixin; + +@Command(name = "example", + mixinStandardHelpOptions = true, + subcommands = HelpCommand.class, + version = "3.7.0") +public class Example { + @Mixin ExampleMixin mixin; + + @Option(names = "-t") + TimeUnit timeUnit; + + @Option(names = "--minimum") + public void setMinimum(int arg0) { + // TODO replace the stored value with the new value + } + + @Parameters(index = "0") + File file; + + @Parameters(index = "1..*", + type = File.class) + public void setOtherFiles(List arg0) { + // TODO replace the stored value with the new value + } + + @Command + public static class ExampleMixin { + @Option(names = "-l") + int length; + } + + @Command(name = "multiply") + int multiply( + @Option(names = "--count") int arg0, + @Parameters(index = "0") int arg1) { + // TODO implement commandSpec + } +} diff --git a/picocli-examples/build.gradle b/picocli-examples/build.gradle index ca0df9054..d221e53a0 100644 --- a/picocli-examples/build.gradle +++ b/picocli-examples/build.gradle @@ -28,3 +28,9 @@ Application-name: $rootProject.name $project.name """ } } + +//tasks.withType(JavaCompile) { +// options.compilerArgs << '-Xlint:unchecked' << '-Xlint:deprecation' << +// '-processorpath' << 'C:\\Users\\remko\\IdeaProjects\\picocli3\\picocli-codegen\\build\\libs\\picocli-codegen-4.0.0-SNAPSHOT.jar;C:\\Users\\remko\\IdeaProjects\\picocli3\\build\\libs\\picocli-4.0.0-SNAPSHOT.jar' << +// '-processor' << 'picocli.codegen.annotation.processing.AnnotatedCommandSourceGeneratorProcessor' +//} diff --git a/picocli-examples/src/main/java/picocli/examples/mixin/CommandWithMixin.java b/picocli-examples/src/main/java/picocli/examples/mixin/CommandWithMixin.java index fb336e6d9..7cbb26c2a 100644 --- a/picocli-examples/src/main/java/picocli/examples/mixin/CommandWithMixin.java +++ b/picocli-examples/src/main/java/picocli/examples/mixin/CommandWithMixin.java @@ -4,6 +4,7 @@ import picocli.CommandLine.Command; import picocli.CommandLine.Mixin; import picocli.CommandLine.Option; +import picocli.CommandLine.Parameters; @Command(name = "mixee", description = "This command has a footer and an option mixed in") public class CommandWithMixin { @@ -13,6 +14,12 @@ public class CommandWithMixin { @Option(names = "-y", description = "command option") int y; + @Command + public void doit(@Mixin CommonOption commonOptionParam, + @Option(names = "-z") int z, + @Parameters String arg0, + String arg1) {} + public static void main(String[] args) { CommandWithMixin cmd = new CommandWithMixin(); new CommandLine(cmd).parseArgs("-x", "3", "-y", "4"); diff --git a/settings.gradle b/settings.gradle index c7999ff9f..a6a119d93 100644 --- a/settings.gradle +++ b/settings.gradle @@ -3,4 +3,5 @@ include 'picocli-examples' include 'picocli-shell-jline2' include 'picocli-shell-jline3' include 'picocli-codegen' +include 'picocli-annotation-processing-tests' diff --git a/src/main/java/picocli/CommandLine.java b/src/main/java/picocli/CommandLine.java index b86de1313..ec6d88c03 100644 --- a/src/main/java/picocli/CommandLine.java +++ b/src/main/java/picocli/CommandLine.java @@ -44,7 +44,6 @@ import picocli.CommandLine.Model.*; import static java.util.Locale.ENGLISH; -import static picocli.CommandLine.Model.ArgsReflection.abbreviate; import static picocli.CommandLine.Help.Column.Overflow.SPAN; import static picocli.CommandLine.Help.Column.Overflow.TRUNCATE; import static picocli.CommandLine.Help.Column.Overflow.WRAP; @@ -139,7 +138,7 @@ *

*/ public class CommandLine { - + /** This is picocli version {@value}. */ public static final String VERSION = "3.9.3-SNAPSHOT"; @@ -3367,13 +3366,13 @@ public interface IHelpFactory { */ Help create(CommandSpec commandSpec, Help.ColorScheme colorScheme); } - + private static class DefaultHelpFactory implements IHelpFactory { public Help create(CommandSpec commandSpec, Help.ColorScheme colorScheme) { return new Help(commandSpec, colorScheme); } } - + /** * Factory for instantiating classes that are registered declaratively with annotation attributes, like * {@link Command#subcommands()}, {@link Option#converter()}, {@link Parameters#converter()} and {@link Command#versionProvider()}. @@ -3460,7 +3459,7 @@ public Range(int min, int max, boolean variable, boolean unspecified, String ori * @param field the field whose Option annotation to inspect * @return a new {@code Range} based on the Option arity annotation on the specified field */ public static Range optionArity(Field field) { return optionArity(new TypedMember(field)); } - private static Range optionArity(TypedMember member) { + private static Range optionArity(IAnnotatedElement member) { return member.isAnnotationPresent(Option.class) ? adjustForType(Range.valueOf(member.getAnnotation(Option.class).arity()), member) : new Range(0, 0, false, true, "0"); @@ -3470,7 +3469,7 @@ private static Range optionArity(TypedMember member) { * @param field the field whose Parameters annotation to inspect * @return a new {@code Range} based on the Parameters arity annotation on the specified field */ public static Range parameterArity(Field field) { return parameterArity(new TypedMember(field)); } - private static Range parameterArity(TypedMember member) { + private static Range parameterArity(IAnnotatedElement member) { if (member.isAnnotationPresent(Parameters.class)) { return adjustForType(Range.valueOf(member.getAnnotation(Parameters.class).arity()), member); } else { @@ -3483,19 +3482,19 @@ private static Range parameterArity(TypedMember member) { * @param field the field whose Parameters annotation to inspect * @return a new {@code Range} based on the Parameters index annotation on the specified field */ public static Range parameterIndex(Field field) { return parameterIndex(new TypedMember(field)); } - private static Range parameterIndex(TypedMember member) { + private static Range parameterIndex(IAnnotatedElement member) { if (member.isAnnotationPresent(Parameters.class)) { Range result = Range.valueOf(member.getAnnotation(Parameters.class).index()); if (!result.isUnspecified) { return result; } } if (member.isMethodParameter()) { - int min = ((MethodParam) member.accessible).position; + int min = member.getMethodParamPosition(); int max = member.isMultiValue() ? Integer.MAX_VALUE : min; return new Range(min, max, member.isMultiValue(), false, ""); } return Range.valueOf("*"); // the default } - static Range adjustForType(Range result, TypedMember member) { + static Range adjustForType(Range result, IAnnotatedElement member) { return result.isUnspecified ? defaultArity(member) : result; } /** Returns the default arity {@code Range}: for {@link Option options} this is 0 for booleans and 1 for @@ -3505,16 +3504,14 @@ static Range adjustForType(Range result, TypedMember member) { * @return a new {@code Range} indicating the default arity of the specified field * @since 2.0 */ public static Range defaultArity(Field field) { return defaultArity(new TypedMember(field)); } - private static Range defaultArity(TypedMember member) { - Class type = member.getType(); + private static Range defaultArity(IAnnotatedElement member) { + ITypeInfo info = member.getTypeInfo(); if (member.isAnnotationPresent(Option.class)) { - Class[] typeAttribute = ArgsReflection - .inferTypes(type, member.getAnnotation(Option.class).type(), member.getGenericType()); - boolean zeroArgs = isBoolean(type) || (isMultiValue(type) && isBoolean(typeAttribute[0])); + boolean zeroArgs = info.isBoolean() || (info.isMultiValue() && info.getAuxiliaryTypeInfos().get(0).isBoolean()); return zeroArgs ? Range.valueOf("0").unspecified(true) : Range.valueOf("1").unspecified(true); } - if (isMultiValue(type)) { + if (info.isMultiValue()) { return Range.valueOf("0..1").unspecified(true); } return Range.valueOf("1").unspecified(true);// for single-valued fields (incl. boolean positional parameters) @@ -3527,7 +3524,7 @@ private static Range defaultArity(TypedMember member) { return isBoolean(type) ? Range.valueOf("0").unspecified(true) : Range.valueOf("1").unspecified(true); } private int size() { return 1 + max - min; } - static Range parameterCapacity(TypedMember member) { + static Range parameterCapacity(IAnnotatedElement member) { Range arity = parameterArity(member); if (!member.isMultiValue()) { return arity; } Range index = parameterIndex(member); @@ -3589,6 +3586,9 @@ private static int parseInt(String str, int defaultValue) { * @param unspecified the {@code unspecified} value of the returned Range object * @return a new Range object with the specified {@code unspecified} value */ public Range unspecified(boolean unspecified) { return new Range(min, max, isVariable, unspecified, originalValue); } + /** Returns {@code true} if this Range is a default value, {@code false} if the user specified this value. + * @since 4.0 */ + public boolean isUnspecified() { return isUnspecified; } /** * Returns {@code true} if this Range includes the specified value, {@code false} otherwise. @@ -3689,6 +3689,9 @@ public static class CommandSpec { /** Constant Boolean holding the default setting for whether this is a help command: {@value}.*/ static final Boolean DEFAULT_IS_HELP_COMMAND = Boolean.FALSE; + /** Constant Boolean holding the default setting for whether method commands should be added as subcommands: {@value}.*/ + static final Boolean DEFAULT_IS_ADD_METHOD_SUBCOMMANDS = Boolean.TRUE; + private final Map commands = new LinkedHashMap(); private final Map optionsByNameMap = new LinkedHashMap(); private final Map posixOptionsByKeyMap = new LinkedHashMap(); @@ -3704,6 +3707,7 @@ public static class CommandSpec { private final Object userObject; private CommandLine commandLine; private CommandSpec parent; + private Boolean isAddMethodSubcommands; private String name; private Set aliases = new LinkedHashSet(); @@ -3813,6 +3817,24 @@ protected CommandSpec commandLine(CommandLine commandLine) { /** Initializes the usageMessage specification for this command from the specified settings and returns this commandSpec.*/ public CommandSpec usageMessage(UsageMessageSpec settings) { usageMessage.initFrom(settings, this); return this; } + /** Returns the resource bundle base name for this command. + * @return the resource bundle base name from the {@linkplain UsageMessageSpec#messages()} + * @since 4.0 */ + public String resourceBundleBaseName() { return Messages.resourceBundleBaseName(usageMessage.messages()); } + /** Initializes the resource bundle for this command: sets the {@link UsageMessageSpec#messages(Messages) UsageMessageSpec.messages} to + * a {@link Messages Messages} object created from this command spec and the specified bundle, and then sets the + * {@link ArgSpec#messages(Messages) ArgSpec.messages} of all options and positional parameters in this command + * to the same {@code Messages} instance. Subcommands are not modified. + *

This method is preferable to {@link #resourceBundle(ResourceBundle)} for pre-Java 8

+ * @param resourceBundleBaseName the base name of the ResourceBundle to set, may be {@code null} + * @return this commandSpec + * @see #addSubcommand(String, CommandLine) + * @since 4.0 */ + public CommandSpec resourceBundleBaseName(String resourceBundleBaseName) { + ResourceBundle bundle = resourceBundleBaseName == null ? null : ResourceBundle.getBundle(resourceBundleBaseName); + setBundle(resourceBundleBaseName, bundle); + return this; + } /** Returns the resource bundle for this command. * @return the resource bundle from the {@linkplain UsageMessageSpec#messages()} * @since 3.6 */ @@ -3826,10 +3848,13 @@ protected CommandSpec commandLine(CommandLine commandLine) { * @see #addSubcommand(String, CommandLine) * @since 3.6 */ public CommandSpec resourceBundle(ResourceBundle bundle) { - usageMessage().messages(new Messages(this, bundle)); - updateArgSpecMessages(); + setBundle(Messages.extractName(bundle), bundle); return this; } + private void setBundle(String bundleBaseName, ResourceBundle bundle) { + usageMessage().messages(new Messages(this, bundleBaseName, bundle)); + updateArgSpecMessages(); + } private void updateArgSpecMessages() { for (OptionSpec opt : options()) { opt.messages(usageMessage().messages()); } for (PositionalParamSpec pos : positionalParameters()) { pos.messages(usageMessage().messages()); } @@ -3862,18 +3887,25 @@ public CommandSpec addSubcommand(String name, CommandLine subCommandLine) { previous = commands.put(alias, subCommandLine); if (previous != null && previous != subCommandLine) { throw new InitializationException("Alias '" + alias + "' for subcommand '" + name + "' is already used by another subcommand of '" + this.name() + "'"); } } - subSpec.initResourceBundle(resourceBundle()); + subSpec.initCommandHierarchyWithResourceBundle(resourceBundleBaseName()); return this; } - private void initResourceBundle(ResourceBundle bundle) { + private void initCommandHierarchyWithResourceBundle(String bundleBaseName) { if (resourceBundle() == null) { - resourceBundle(bundle); + resourceBundleBaseName(bundleBaseName); } for (CommandLine sub : commands.values()) { // percolate down the hierarchy - sub.getCommandSpec().initResourceBundle(resourceBundle()); + sub.getCommandSpec().initCommandHierarchyWithResourceBundle(bundleBaseName); } } + /** Returns whether method commands should be added as subcommands. Used by the annotation processor. + * @since 4.0 */ + public boolean isAddMethodSubcommands() { return (isAddMethodSubcommands == null) ? DEFAULT_IS_ADD_METHOD_SUBCOMMANDS : isAddMethodSubcommands; } + /** Sets whether method commands should be added as subcommands. Used by the annotation processor. + * @since 4.0 */ + public CommandSpec setAddMethodSubcommands(Boolean addMethodSubcommands) { isAddMethodSubcommands = addMethodSubcommands; return this; } + /** Reflects on the class of the {@linkplain #userObject() user object} and registers any command methods * (class methods annotated with {@code @Command}) as subcommands. * @@ -3899,6 +3931,7 @@ public CommandSpec addMethodSubcommands(IFactory factory) { CommandLine cmd = new CommandLine(method, factory); addSubcommand(cmd.getCommandName(), cmd); } + isAddMethodSubcommands = true; return this; } @@ -4180,7 +4213,30 @@ public CommandSpec mixinStandardHelpOptions(boolean newValue) { * @param newValue the string representation * @return this CommandSpec for method chaining */ public CommandSpec withToString(String newValue) { this.toString = newValue; return this; } - + + /** + * Updates the following attributes from the specified {@code @Command} annotation: + * aliases, {@link ParserSpec#separator() parser separator}, command name, version, help command, + * version provider, default provider and {@link UsageMessageSpec usage message spec}. + * @param cmd the {@code @Command} annotation to get attribute values from + * @param factory factory used to instantiate classes + * @since 3.7 + */ + public void updateCommandAttributes(Command cmd, IFactory factory) { + aliases(cmd.aliases()); + parser().updateSeparator(cmd.separator()); + updateName(cmd.name()); + updateVersion(cmd.version()); + updateHelpCommand(cmd.helpCommand()); + updateAddMethodSubcommands(cmd.addMethodSubcommands()); + usageMessage().updateFromCommand(cmd, this); + + if (factory != null) { + updateVersionProvider(cmd.versionProvider(), factory); + initDefaultValueProvider(cmd.defaultValueProvider(), factory); + } + } + void initName(String value) { if (initializable(name, value, DEFAULT_COMMAND_NAME)) {name = value;} } void initHelpCommand(boolean value) { if (initializable(isHelpCommand, value, DEFAULT_IS_HELP_COMMAND)) {isHelpCommand = value;} } void initVersion(String[] value) { if (initializable(version, value, UsageMessageSpec.DEFAULT_MULTI_LINE)) {version = value.clone();} } @@ -4191,6 +4247,7 @@ void initDefaultValueProvider(Class value, IFac } void updateName(String value) { if (isNonDefault(value, DEFAULT_COMMAND_NAME)) {name = value;} } void updateHelpCommand(boolean value) { if (isNonDefault(value, DEFAULT_IS_HELP_COMMAND)) {isHelpCommand = value;} } + void updateAddMethodSubcommands(boolean value) { if (isNonDefault(value, DEFAULT_IS_ADD_METHOD_SUBCOMMANDS)) {isAddMethodSubcommands = value;} } void updateVersion(String[] value) { if (isNonDefault(value, UsageMessageSpec.DEFAULT_MULTI_LINE)) {version = value.clone();} } void updateVersionProvider(Class value, IFactory factory) { if (isNonDefault(value, NoVersionProvider.class)) { versionProvider = (DefaultFactory.createVersionProvider(factory, value)); } @@ -4767,8 +4824,9 @@ void updateFromCommand(Command cmd, CommandSpec commandSpec) { if (isNonDefault(cmd.optionListHeading(), DEFAULT_SINGLE_VALUE)) {optionListHeading = cmd.optionListHeading();} if (isNonDefault(cmd.usageHelpWidth(), DEFAULT_USAGE_WIDTH)) {width(cmd.usageHelpWidth());} // validate - ResourceBundle rb = empty(cmd.resourceBundle()) ? null : ResourceBundle.getBundle(cmd.resourceBundle()); - if (rb != null) { messages(new Messages(commandSpec, rb)); } // else preserve superclass bundle + if (!empty(cmd.resourceBundle())) { // else preserve superclass bundle + messages(new Messages(commandSpec, cmd.resourceBundle())); + } } void initFromMixin(UsageMessageSpec mixin, CommandSpec commandSpec) { if (initializable(synopsisHeading, mixin.synopsisHeading(), DEFAULT_SYNOPSIS_HEADING)) {synopsisHeading = mixin.synopsisHeading();} @@ -4996,13 +5054,13 @@ public abstract static class ArgSpec { private final Help.Visibility showDefaultValue; private Messages messages; CommandSpec commandSpec; + private final Object userObject; // parser fields private final boolean interactive; private final boolean required; private final String splitRegex; - private final Class type; - private final Class[] auxiliaryTypes; + private final ITypeInfo typeInfo; private final ITypeConverter[] converters; private final Iterable completionCandidates; private final String defaultValue; @@ -5019,6 +5077,7 @@ public abstract static class ArgSpec { /** Constructs a new {@code ArgSpec}. */ private > ArgSpec(Builder builder) { + userObject = builder.userObject; description = builder.description == null ? new String[0] : builder.description; descriptionKey = builder.descriptionKey; splitRegex = builder.splitRegex == null ? "" : builder.splitRegex; @@ -5028,10 +5087,10 @@ private > ArgSpec(Builder builder) { showDefaultValue = builder.showDefaultValue == null ? Help.Visibility.ON_DEMAND : builder.showDefaultValue; hidden = builder.hidden; interactive = builder.interactive; - required = builder.required && builder.defaultValue == null; //#261 not required if it has a default - defaultValue = builder.defaultValue; initialValue = builder.initialValue; hasInitialValue = builder.hasInitialValue; + defaultValue = NO_DEFAULT_VALUE.equals(builder.defaultValue) ? null : builder.defaultValue; + required = builder.required && defaultValue == null; //#261 not required if it has a default toString = builder.toString; getter = builder.getter; setter = builder.setter; @@ -5046,38 +5105,17 @@ private > ArgSpec(Builder builder) { tempArity = tempArity.unspecified(true); } arity = tempArity; - - if (builder.type == null) { - if (builder.auxiliaryTypes == null || builder.auxiliaryTypes.length == 0) { - if (arity.isVariable || arity.max > 1) { - type = String[].class; - } else if (arity.max == 1) { - type = String.class; - } else { - type = isOption() ? boolean.class : String.class; - } - } else { - type = builder.auxiliaryTypes[0]; - } - } else { - type = builder.type; - } - if (builder.auxiliaryTypes == null || builder.auxiliaryTypes.length == 0) { - if (type.isArray()) { - auxiliaryTypes = new Class[]{type.getComponentType()}; - } else if (Collection.class.isAssignableFrom(type)) { // type is a collection but element type is unspecified - auxiliaryTypes = new Class[] {String.class}; // use String elements - } else if (Map.class.isAssignableFrom(type)) { // type is a map but element type is unspecified - auxiliaryTypes = new Class[] {String.class, String.class}; // use String keys and String values - } else { - auxiliaryTypes = new Class[] {type}; - } + + if (builder.typeInfo == null) { + this.typeInfo = RuntimeTypeInfo.create(builder.type, builder.auxiliaryTypes, + Collections.emptyList(), arity, (isOption() ? boolean.class : String.class)); } else { - auxiliaryTypes = builder.auxiliaryTypes; + this.typeInfo = builder.typeInfo; } - if (builder.completionCandidates == null && auxiliaryTypes[0].isEnum()) { + + if (builder.completionCandidates == null && typeInfo.isEnum()) { List list = new ArrayList(); - for (Object c : auxiliaryTypes[0].getEnumConstants()) { list.add(c.toString()); } + for (Object c : typeInfo.getEnumConstantNames()) { list.add(c.toString()); } completionCandidates = Collections.unmodifiableList(list); } else { completionCandidates = builder.completionCandidates; @@ -5143,7 +5181,7 @@ public String[] renderedDescription() { /** Returns auxiliary type information used when the {@link #type()} is a generic {@code Collection}, {@code Map} or an abstract class. * @see Option#type() */ - public Class[] auxiliaryTypes() { return auxiliaryTypes.clone(); } + public Class[] auxiliaryTypes() { return typeInfo.getAuxiliaryTypes(); } /** Returns one or more {@link CommandLine.ITypeConverter type converters} to use to convert the command line * argument into a strongly typed value (or key-value pair for map fields). This is useful when a particular @@ -5160,7 +5198,16 @@ public String[] renderedDescription() { public boolean hidden() { return hidden; } /** Returns the type to convert the option or positional parameter to before {@linkplain #setValue(Object) setting} the value. */ - public Class type() { return type; } + public Class type() { return typeInfo.getType(); } + + /** Returns the {@code ITypeInfo} that can be used both at compile time (by annotation processors) and at runtime. + * @since 4.0 */ + public ITypeInfo typeInfo() { return typeInfo; } + + /** Returns the user object associated with this option or positional parameters. + * @return may return the annotated program element, or some other useful object + * @since 4.0 */ + public Object userObject() { return userObject; } /** Returns the default value of this option or positional parameter, before splitting and type conversion. * This method returns the programmatically set value; this may differ from the default value that is actually used: @@ -5248,7 +5295,7 @@ public T setValue(T newValue) throws PicocliException { } /** Returns {@code true} if this argument's {@link #type()} is an array, a {@code Collection} or a {@code Map}, {@code false} otherwise. */ - public boolean isMultiValue() { return CommandLine.isMultiValue(type()); } + public boolean isMultiValue() { return typeInfo.isMultiValue(); } /** Returns {@code true} if this argument is a named option, {@code false} otherwise. */ public abstract boolean isOption(); /** Returns {@code true} if this argument is a positional parameter, {@code false} otherwise. */ @@ -5378,7 +5425,6 @@ private static String restoreQuotedValues(String part, Queue quotedValue protected boolean equalsImpl(ArgSpec other) { boolean result = Assert.equals(this.defaultValue, other.defaultValue) - && Assert.equals(this.type, other.type) && Assert.equals(this.arity, other.arity) && Assert.equals(this.hidden, other.hidden) && Assert.equals(this.paramLabel, other.paramLabel) @@ -5387,14 +5433,13 @@ protected boolean equalsImpl(ArgSpec other) { && Assert.equals(this.splitRegex, other.splitRegex) && Arrays.equals(this.description, other.description) && Assert.equals(this.descriptionKey, other.descriptionKey) - && Arrays.equals(this.auxiliaryTypes, other.auxiliaryTypes) + && this.typeInfo.equals(other.typeInfo) ; return result; } protected int hashCodeImpl() { return 17 + 37 * Assert.hashCode(defaultValue) - + 37 * Assert.hashCode(type) + 37 * Assert.hashCode(arity) + 37 * Assert.hashCode(hidden) + 37 * Assert.hashCode(paramLabel) @@ -5403,11 +5448,12 @@ protected int hashCodeImpl() { + 37 * Assert.hashCode(splitRegex) + 37 * Arrays.hashCode(description) + 37 * Assert.hashCode(descriptionKey) - + 37 * Arrays.hashCode(auxiliaryTypes) + + 37 * typeInfo.hashCode() ; } abstract static class Builder> { + private Object userObject; private Range arity; private String[] description; private String descriptionKey; @@ -5419,6 +5465,7 @@ abstract static class Builder> { private boolean hidden; private Class type; private Class[] auxiliaryTypes; + private ITypeInfo typeInfo; private ITypeConverter[] converters; private String defaultValue; private Object initialValue; @@ -5431,8 +5478,8 @@ abstract static class Builder> { Builder() {} Builder(ArgSpec original) { + userObject = original.userObject; arity = original.arity; - auxiliaryTypes = original.auxiliaryTypes; converters = original.converters; defaultValue = original.defaultValue; description = original.description; @@ -5447,10 +5494,79 @@ abstract static class Builder> { completionCandidates = original.completionCandidates; splitRegex = original.splitRegex; toString = original.toString; - type = original.type; descriptionKey = original.descriptionKey; + setTypeInfo(original.typeInfo); + } + Builder(IAnnotatedElement source) { + userObject = source.userObject(); + setTypeInfo(source.getTypeInfo()); + toString = source.getToString(); + getter = source.getter(); + setter = source.setter(); + hasInitialValue = source.hasInitialValue(); + try { initialValue = source.getter().get(); } catch (Exception ex) { initialValue = null; } + } + Builder(Option option, IAnnotatedElement source, IFactory factory) { + this(source); + arity = Range.optionArity(source); + required = option.required(); + + paramLabel = inferLabel(option.paramLabel(), source.getName(), source.getTypeInfo()); + + hideParamSyntax = option.hideParamSyntax(); + interactive = option.interactive(); + description = option.description(); + descriptionKey = option.descriptionKey(); + splitRegex = option.split(); + hidden = option.hidden(); + defaultValue = option.defaultValue(); + showDefaultValue = option.showDefaultValue(); + if (factory != null) { + converters = DefaultFactory.createConverter(factory, option.converter()); + if (!NoCompletionCandidates.class.equals(option.completionCandidates())) { + completionCandidates = DefaultFactory.createCompletionCandidates(factory, option.completionCandidates()); + } + } } - + Builder(Parameters parameters, IAnnotatedElement source, IFactory factory) { + this(source); + arity = Range.parameterArity(source); + required = arity.min > 0; + + // method parameters may be positional parameters without @Parameters annotation + if (parameters == null) { + paramLabel = inferLabel(null, source.getName(), source.getTypeInfo()); + } else { + paramLabel = inferLabel(parameters.paramLabel(), source.getName(), source.getTypeInfo()); + + hideParamSyntax = parameters.hideParamSyntax(); + interactive = parameters.interactive(); + description = parameters.description(); + descriptionKey = parameters.descriptionKey(); + splitRegex = parameters.split(); + hidden = parameters.hidden(); + defaultValue = parameters.defaultValue(); + showDefaultValue = parameters.showDefaultValue(); + if (factory != null) { // annotation processors will pass a null factory + converters = DefaultFactory.createConverter(factory, parameters.converter()); + if (!NoCompletionCandidates.class.equals(parameters.completionCandidates())) { + completionCandidates = DefaultFactory.createCompletionCandidates(factory, parameters.completionCandidates()); + } + } + } + } + private static String inferLabel(String label, String fieldName, ITypeInfo typeInfo) { + if (!empty(label)) { return label.trim(); } + String name = fieldName; + if (typeInfo.isMap()) { // #195 better param labels for map fields + List aux = typeInfo.getAuxiliaryTypeInfos(); + if (aux.size() < 2 || aux.get(0) == null || aux.get(1) == null) { + name = "String=String"; + } else { name = aux.get(0).getClassSimpleName() + "=" + aux.get(1).getClassSimpleName(); } + } + return "<" + name + ">"; + } + public abstract ArgSpec build(); protected abstract T self(); // subclasses must override to return "this" /** Returns whether this is a required option or positional parameter. @@ -5505,6 +5621,17 @@ abstract static class Builder> { /** Returns the type to convert the option or positional parameter to before {@linkplain #setValue(Object) setting} the value. */ public Class type() { return type; } + /** Returns the type info for this option or positional parameter. + * @return type information that does not require {@code Class} objects and be constructed both at runtime and compile time + * @since 4.0 + */ + public ITypeInfo typeInfo() { return typeInfo; } + + /** Returns the user object associated with this option or positional parameters. + * @return may return the annotated program element, or some other useful object + * @since 4.0 */ + public Object userObject() { return userObject; } + /** Returns the default value of this option or positional parameter, before splitting and type conversion. * A value of {@code null} means this option or positional parameter does not have a default. */ public String defaultValue() { return defaultValue; } @@ -5577,7 +5704,7 @@ abstract static class Builder> { /** Sets the completion candidates for this option or positional parameter, and returns this builder. * @since 3.2 */ - public T completionCandidates(Iterable completionCandidates) { this.completionCandidates = Assert.notNull(completionCandidates, "completionCandidates"); return self(); } + public T completionCandidates(Iterable completionCandidates) { this.completionCandidates = completionCandidates; return self(); } /** Sets whether this option should be excluded from the usage message, and returns this builder. */ public T hidden(boolean hidden) { this.hidden = hidden; return self(); } @@ -5585,11 +5712,31 @@ abstract static class Builder> { /** Sets the type to convert the option or positional parameter to before {@linkplain #setValue(Object) setting} the value, and returns this builder. * @param propertyType the type of this option or parameter. For multi-value options and positional parameters this can be an array, or a (sub-type of) Collection or Map. */ public T type(Class propertyType) { this.type = Assert.notNull(propertyType, "type"); return self(); } - + + /** Sets the type info for this option or positional parameter, and returns this builder. + * @param typeInfo type information that does not require {@code Class} objects and be constructed both at runtime and compile time + * @since 4.0 */ + public T typeInfo(ITypeInfo typeInfo) { + setTypeInfo(Assert.notNull(typeInfo, "typeInfo")); + return self(); + } + private void setTypeInfo(ITypeInfo newValue) { + this.typeInfo = newValue; + if (typeInfo != null) { + type = typeInfo.getType(); + auxiliaryTypes = typeInfo.getAuxiliaryTypes(); + } + } + + /** Sets the user object associated with this option or positional parameters, and returns this builder. + * @param userObject may be the annotated program element, or some other useful object + * @since 4.0 */ + public T userObject(Object userObject) { this.userObject = Assert.notNull(userObject, "userObject"); return self(); } + /** Sets the default value of this option or positional parameter to the specified value, and returns this builder. * Before parsing the command line, the result of {@linkplain #splitRegex() splitting} and {@linkplain #converters() type converting} * this default value is applied to the option or positional parameter. A value of {@code null} or {@code "__no_default_value__"} means no default. */ - public T defaultValue(String defaultValue) { this.defaultValue = NO_DEFAULT_VALUE.equals(defaultValue) ? null : defaultValue; return self(); } + public T defaultValue(String defaultValue) { this.defaultValue = defaultValue; return self(); } /** Sets the initial value of this option or positional parameter to the specified value, and returns this builder. * If {@link #hasInitialValue()} is true, the option will be reset to the initial value before parsing (regardless @@ -5657,7 +5804,7 @@ public static class OptionSpec extends ArgSpec { private boolean usageHelp; private boolean versionHelp; private int order; - + public static OptionSpec.Builder builder(String name, String... names) { String[] copy = new String[Assert.notNull(names, "names").length + 1]; copy[0] = Assert.notNull(name, "name"); @@ -5665,7 +5812,8 @@ public static OptionSpec.Builder builder(String name, String... names) { return new Builder(copy); } public static OptionSpec.Builder builder(String[] names) { return new Builder(names); } - + public static OptionSpec.Builder builder(IAnnotatedElement source, IFactory factory) { return new Builder(source, factory); } + /** Ensures all attributes of this {@code OptionSpec} have a valid value; throws an {@link InitializationException} if this cannot be achieved. */ private OptionSpec(Builder builder) { super(builder); @@ -5677,7 +5825,7 @@ private OptionSpec(Builder builder) { usageHelp = builder.usageHelp; versionHelp = builder.versionHelp; order = builder.order; - + if (names.length == 0 || Arrays.asList(names).contains("")) { throw new InitializationException("Invalid names: " + Arrays.toString(names)); } @@ -5724,8 +5872,9 @@ protected boolean internalShowDefaultValue(boolean usageMessageShowDefaults) { /** Returns the longest {@linkplain #names() option name}. */ public String longestName() { return Help.ShortestFirst.longestFirst(names.clone())[0]; } - /** Returns the shortest {@linkplain #names() option name}. */ - String shortestName() { return Help.ShortestFirst.sort(names.clone())[0]; } + /** Returns the shortest {@linkplain #names() option name}. + * @since 3.8 */ + public String shortestName() { return Help.ShortestFirst.sort(names.clone())[0]; } /** Returns the position in the options list in the usage help message at which this option should be shown. * Options with a lower number are shown before options with a higher number. @@ -5776,7 +5925,7 @@ public static class Builder extends ArgSpec.Builder { private boolean usageHelp; private boolean versionHelp; private int order = DEFAULT_ORDER; - + private Builder(String[] names) { this.names = names; } private Builder(OptionSpec original) { super(original); @@ -5786,7 +5935,16 @@ private Builder(OptionSpec original) { versionHelp = original.versionHelp; order = original.order; } - + private Builder(IAnnotatedElement member, IFactory factory) { + super(member.getAnnotation(Option.class), member, factory); + Option option = member.getAnnotation(Option.class); + names = option.names(); + help = option.help(); + usageHelp = option.usageHelp(); + versionHelp = option.versionHelp(); + order = option.order(); + } + /** Returns a valid {@code OptionSpec} instance. */ @Override public OptionSpec build() { return new OptionSpec(this); } /** Returns this builder. */ @@ -5886,6 +6044,8 @@ private PositionalParamSpec(Builder builder) { capacity = builder.capacity == null ? Range.parameterCapacity(arity(), index) : builder.capacity; if (toString == null) { toString = "positional parameter[" + index() + "]"; } } + public static Builder builder() { return new Builder(); } + public static Builder builder(IAnnotatedElement source, IFactory factory) { return new Builder(source, factory); } /** Returns a new Builder initialized with the attributes from this {@code PositionalParamSpec}. Calling {@code build} immediately will return a copy of this {@code PositionalParamSpec}. * @return a builder that can create a copy of this spec */ @@ -5914,7 +6074,6 @@ private PositionalParamSpec(Builder builder) { * @see Parameters#index() */ public Range index() { return index; } private Range capacity() { return capacity; } - public static Builder builder() { return new Builder(); } public int hashCode() { return super.hashCodeImpl() @@ -5946,6 +6105,11 @@ private Builder(PositionalParamSpec original) { index = original.index; capacity = original.capacity; } + private Builder(IAnnotatedElement member, IFactory factory) { + super(member.getAnnotation(Parameters.class), member, factory); + index = Range.parameterIndex(member); + capacity = Range.parameterCapacity(member); + } /** Returns a valid {@code PositionalParamSpec} instance. */ @Override public PositionalParamSpec build() { return new PositionalParamSpec(this); } /** Returns this builder. */ @@ -6010,8 +6174,9 @@ void addAll(String[] unmatched) { } } } - /** mock java.lang.reflect.Parameter (not available before Java 8) */ - static class MethodParam extends AccessibleObject { + /** Command method parameter, similar to java.lang.reflect.Parameter (not available before Java 8). + * @since 4.0 */ + public static class MethodParam extends AccessibleObject { final Method method; final int paramIndex; final String name; @@ -6044,25 +6209,215 @@ public MethodParam(Method method, int paramIndex) { @Override public boolean isAccessible() throws SecurityException { return method.isAccessible(); } @Override public String toString() { return method.toString() + ":" + getName(); } } - static class TypedMember { + + /** Encapculates type information for an option or parameter to make this information available both at runtime + * and at compile time (when {@code Class} values are not available). + * Most of the methods in this interface (but not all!) are safe to use by annotation processors. + * @since 4.0 + */ + public interface ITypeInfo { + /** Returns {@code true} if {@link #getType()} is {@code boolean} or {@code java.lang.Boolean}. */ + boolean isBoolean(); + /** Returns {@code true} if {@link #getType()} is an array, map or collection. */ + boolean isMultiValue(); + boolean isArray(); + boolean isCollection(); + boolean isMap(); + /** Returns {@code true} if {@link #getType()} is an enum. */ + boolean isEnum(); + List getEnumConstantNames(); + String getClassName(); + String getClassSimpleName(); + /** Returns type information of components or elements of a {@link #isMultiValue() multivalue} type. */ + List getAuxiliaryTypeInfos(); + /** Returns the names of the type arguments if this is a generic type. For example, returns {@code ["java.lang.String"]} if this type is {@code List}. */ + List getActualGenericTypeArguments(); + + /** Returns the class that the option or parameter value should be converted to when matched on the command + * line. This method is not safe for annotation processors to use. + * @return the class that the option or parameter value should be converted to + */ + Class getType(); + /** Returns the component class of an array, or the parameter type of a generic Collection, or the parameter + * types of the key and the value of a generic Map. + * This method is not safe for annotation processors to use. + * @return the component type or types of an array, Collection or Map type + */ + Class[] getAuxiliaryTypes(); + } + static class RuntimeTypeInfo implements ITypeInfo { + private final Class type; + private final Class[] auxiliaryTypes; + private final List actualGenericTypeArguments; + + private RuntimeTypeInfo(Class type, Class[] auxiliaryTypes, List actualGenericTypeArguments) { + this.type = Assert.notNull(type, "type"); + this.auxiliaryTypes = Assert.notNull(auxiliaryTypes, "auxiliaryTypes").clone(); + this.actualGenericTypeArguments = actualGenericTypeArguments == null ? Collections.emptyList() : Collections.unmodifiableList(new ArrayList(actualGenericTypeArguments)); + } + + static ITypeInfo createForAuxType(Class type) { + return create(type, new Class[0], (Type) null, Range.valueOf("1"), String.class); + } + public static ITypeInfo create(Class type, + Class[] annotationTypes, + Type genericType, + Range arity, + Class defaultType) { + Class[] auxiliaryTypes = RuntimeTypeInfo.inferTypes(type, annotationTypes, genericType); + List actualGenericTypeArguments = new ArrayList(); + if (genericType instanceof ParameterizedType) { + Class[] declaredTypeParameters = extractTypeParameters((ParameterizedType) genericType); + for (Class c : declaredTypeParameters) { actualGenericTypeArguments.add(c.getName()); } + } + return create(type, auxiliaryTypes, actualGenericTypeArguments, arity, defaultType); + } + + public static ITypeInfo create(Class type, Class[] auxiliaryTypes, List actualGenericTypeArguments, Range arity, Class defaultType) { + if (type == null) { + if (auxiliaryTypes == null || auxiliaryTypes.length == 0) { + if (arity.isVariable || arity.max > 1) { + type = String[].class; + } else if (arity.max == 1) { + type = String.class; + } else { + type = defaultType; + } + } else { + type = auxiliaryTypes[0]; + } + } + if (auxiliaryTypes == null || auxiliaryTypes.length == 0) { + if (type.isArray()) { + auxiliaryTypes = new Class[] {type.getComponentType()}; + } else if (Collection.class.isAssignableFrom(type)) { // type is a collection but element type is unspecified + auxiliaryTypes = new Class[] {String.class}; // use String elements + } else if (Map.class.isAssignableFrom(type)) { // type is a map but element type is unspecified + auxiliaryTypes = new Class[] {String.class, String.class}; // use String keys and String values + } else { + auxiliaryTypes = new Class[] {type}; + } + } + return new RuntimeTypeInfo(type, auxiliaryTypes, actualGenericTypeArguments); + } + static Class[] inferTypes(Class propertyType, Class[] annotationTypes, Type genericType) { + if (annotationTypes != null && annotationTypes.length > 0) { return annotationTypes; } + if (propertyType.isArray()) { return new Class[] { propertyType.getComponentType() }; } + if (CommandLine.isMultiValue(propertyType)) { + if (genericType instanceof ParameterizedType) {// e.g. Map + return extractTypeParameters((ParameterizedType) genericType); + } + return new Class[] {String.class, String.class}; // field is multi-value but not ParameterizedType + } + return new Class[] {propertyType}; // not a multi-value field + } + + static Class[] extractTypeParameters(ParameterizedType genericType) { + ParameterizedType parameterizedType = genericType; + Type[] paramTypes = parameterizedType.getActualTypeArguments(); // e.g. ? extends Number + Class[] result = new Class[paramTypes.length]; + for (int i = 0; i < paramTypes.length; i++) { + if (paramTypes[i] instanceof Class) { result[i] = (Class) paramTypes[i]; continue; } // e.g. Long + if (paramTypes[i] instanceof WildcardType) { // e.g. ? extends Number + WildcardType wildcardType = (WildcardType) paramTypes[i]; + Type[] lower = wildcardType.getLowerBounds(); // e.g. [] + if (lower.length > 0 && lower[0] instanceof Class) { result[i] = (Class) lower[0]; continue; } + Type[] upper = wildcardType.getUpperBounds(); // e.g. Number + if (upper.length > 0 && upper[0] instanceof Class) { result[i] = (Class) upper[0]; continue; } + } + Arrays.fill(result, String.class); return result; // too convoluted generic type, giving up + } + return result; // we inferred all types from ParameterizedType + } + + public boolean isBoolean() { return auxiliaryTypes[0] == boolean.class || auxiliaryTypes[0] == Boolean.class; } + public boolean isMultiValue() { return CommandLine.isMultiValue(type); } + public boolean isArray() { return type.isArray(); } + public boolean isCollection() { return Collection.class.isAssignableFrom(type); } + public boolean isMap() { return Map.class.isAssignableFrom(type); } + public boolean isEnum() { return auxiliaryTypes[0].isEnum(); } + public String getClassName() { return type.getName(); } + public String getClassSimpleName() { return type.getSimpleName(); } + public Class getType() { return type; } + public Class[] getAuxiliaryTypes() { return auxiliaryTypes; } + public List getActualGenericTypeArguments() { return actualGenericTypeArguments; } + + public List getAuxiliaryTypeInfos() { + List result = new ArrayList(); + for (Class c : auxiliaryTypes) { result.add(createForAuxType(c)); } + return result; + } + public List getEnumConstantNames() { + if (!isEnum()) { return Collections.emptyList(); } + List result = new ArrayList(); + for (Object c : auxiliaryTypes[0].getEnumConstants()) { result.add(c.toString()); } + return result; + } + + public boolean equals(Object obj) { + if (obj == this) { return true; } + if (!(obj instanceof RuntimeTypeInfo)) { return false; } + RuntimeTypeInfo other = (RuntimeTypeInfo) obj; + return Arrays.equals(other.auxiliaryTypes, auxiliaryTypes) && type.equals(other.type); + } + public int hashCode() { + return Arrays.hashCode(auxiliaryTypes) + 37 * Assert.hashCode(type); + } + public String toString() { + return String.format("RuntimeTypeInfo(%s, aux=%s, collection=%s, map=%s)", + type.getCanonicalName(), Arrays.toString(auxiliaryTypes), isCollection(), isMap()); + } + } + /** Internal interface to allow annotation processors to construct a command model at compile time. + * @since 4.0 */ + public interface IAnnotatedElement { + Object userObject(); + boolean isAnnotationPresent(Class annotationClass); + T getAnnotation(Class annotationClass); + String getName(); + String getMixinName(); + boolean isArgSpec(); + boolean isOption(); + boolean isParameter(); + boolean isMixin(); + boolean isUnmatched(); + boolean isInjectSpec(); + boolean isMultiValue(); + boolean hasInitialValue(); + boolean isMethodParameter(); + int getMethodParamPosition(); + CommandLine.Model.IGetter getter(); + CommandLine.Model.ISetter setter(); + ITypeInfo getTypeInfo(); + String getToString(); + } + + static class TypedMember implements IAnnotatedElement { final AccessibleObject accessible; final String name; - final Class type; - final Type genericType; + final ITypeInfo typeInfo; final boolean hasInitialValue; private IGetter getter; private ISetter setter; + static TypedMember createIfAnnotated(Field field, Object scope) { + return isAnnotated(field) ? new TypedMember(field, scope) : null; + } + static boolean isAnnotated(AnnotatedElement e) { + return false + || e.isAnnotationPresent(Option.class) + || e.isAnnotationPresent(Parameters.class) + || e.isAnnotationPresent(Unmatched.class) + || e.isAnnotationPresent(Mixin.class) + || e.isAnnotationPresent(Spec.class) + || e.isAnnotationPresent(ParentCommand.class); + } TypedMember(Field field) { accessible = Assert.notNull(field, "field"); accessible.setAccessible(true); name = field.getName(); - type = field.getType(); - genericType = field.getGenericType(); + typeInfo = createTypeInfo(field.getType(), field.getGenericType()); hasInitialValue = true; } - static TypedMember createIfAnnotated(Field field, Object scope) { - return isAnnotated(field) ? new TypedMember(field, scope) : null; - } private TypedMember(Field field, Object scope) { this(field); if (Proxy.isProxyClass(scope.getClass())) { @@ -6084,8 +6439,7 @@ private TypedMember(Method method, Object scope, CommandSpec spec) { if (isSetter == isGetter) { throw new InitializationException("Invalid method, must be either getter or setter: " + method); } if (isGetter) { hasInitialValue = true; - type = method.getReturnType(); - genericType = method.getGenericReturnType(); + typeInfo = createTypeInfo(method.getReturnType(), method.getGenericReturnType()); if (Proxy.isProxyClass(scope.getClass())) { PicocliInvocationHandler handler = (PicocliInvocationHandler) Proxy.getInvocationHandler(scope); PicocliInvocationHandler.ProxyBinding binding = handler.new ProxyBinding(method); @@ -6098,8 +6452,7 @@ private TypedMember(Method method, Object scope, CommandSpec spec) { } } else { hasInitialValue = false; - type = parameterTypes[0]; - genericType = method.getGenericParameterTypes()[0]; + typeInfo = createTypeInfo(parameterTypes[0], method.getGenericParameterTypes()[0]); MethodBinding binding = new MethodBinding(scope, method, spec); getter = binding; setter = binding; } @@ -6108,9 +6461,7 @@ private TypedMember(MethodParam param, Object scope) { accessible = Assert.notNull(param, "command method parameter"); accessible.setAccessible(true); name = param.getName(); - type = param.getType(); - genericType = param.getParameterizedType(); - + typeInfo = createTypeInfo(param.getType(), param.getParameterizedType()); // bind parameter ObjectBinding binding = new ObjectBinding(); getter = binding; setter = binding; @@ -6118,7 +6469,23 @@ private TypedMember(MethodParam param, Object scope) { hasInitialValue = true; } + private ITypeInfo createTypeInfo(Class type, Type genericType) { + Range arity = null; + if (isOption()) { arity = Range.valueOf(getAnnotation(Option.class).arity()); } + if (isParameter()) { arity = Range.valueOf(getAnnotation(Parameters.class).arity()); } + if (arity == null || arity.isUnspecified) { + if (isOption()) { + arity = (type == null || isBoolean(type)) ? Range.valueOf("0") : Range.valueOf("1"); + } else { + arity = Range.valueOf("1"); + } + arity = arity.unspecified(true); + } + return RuntimeTypeInfo.create(type, annotationTypes(), genericType, arity, (isOption() ? boolean.class : String.class)); + } + private void initializeInitialValue(Object arg) { + Class type = typeInfo.getType(); try { if (type == Boolean.TYPE ) { setter.set(false); } else if (type == Byte.TYPE ) { setter.set(Byte.valueOf((byte) 0)); } @@ -6133,37 +6500,40 @@ private void initializeInitialValue(Object arg) { throw new InitializationException("Could not set initial value for " + arg + ": " + ex.toString(), ex); } } - static boolean isAnnotated(AnnotatedElement e) { - return false - || e.isAnnotationPresent(Option.class) - || e.isAnnotationPresent(Parameters.class) - || e.isAnnotationPresent(Unmatched.class) - || e.isAnnotationPresent(Mixin.class) - || e.isAnnotationPresent(Spec.class) - || e.isAnnotationPresent(ParentCommand.class); + public Object userObject() { return accessible; } + public boolean isAnnotationPresent(Class annotationClass) { return accessible.isAnnotationPresent(annotationClass); } + public T getAnnotation(Class annotationClass) { return accessible.getAnnotation(annotationClass); } + public String getName() { return name; } + public boolean isArgSpec() { return isOption() || isParameter() || (isMethodParameter() && !isMixin()); } + public boolean isOption() { return isAnnotationPresent(Option.class); } + public boolean isParameter() { return isAnnotationPresent(Parameters.class); } + public boolean isMixin() { return isAnnotationPresent(Mixin.class); } + public boolean isUnmatched() { return isAnnotationPresent(Unmatched.class); } + public boolean isInjectSpec() { return isAnnotationPresent(Spec.class); } + public boolean isMultiValue() { return CommandLine.isMultiValue(getType()); } + public IGetter getter() { return getter; } + public ISetter setter() { return setter; } + public ITypeInfo getTypeInfo() { return typeInfo; } + public Class getType() { return typeInfo.getType(); } + public Class[] getAuxiliaryTypes() { return typeInfo.getAuxiliaryTypes(); } + private Class[] annotationTypes() { + if (isOption()) { return getAnnotation(Option.class).type(); } + if (isParameter()) { return getAnnotation(Parameters.class).type(); } + return new Class[0]; } - boolean isAnnotationPresent(Class annotationClass) { return accessible.isAnnotationPresent(annotationClass); } - T getAnnotation(Class annotationClass) { return accessible.getAnnotation(annotationClass); } - String name() { return name; } - boolean isArgSpec() { return isOption() || isParameter() || (isMethodParameter() && !isMixin()); } - boolean isOption() { return isAnnotationPresent(Option.class); } - boolean isParameter() { return isAnnotationPresent(Parameters.class); } - boolean isMixin() { return isAnnotationPresent(Mixin.class); } - boolean isUnmatched() { return isAnnotationPresent(Unmatched.class); } - boolean isInjectSpec() { return isAnnotationPresent(Spec.class); } - boolean isMultiValue() { return CommandLine.isMultiValue(getType()); } - IGetter getter() { return getter; } - ISetter setter() { return setter; } - Class getType() { return type; } - Type getGenericType() { return genericType; } public String toString() { return accessible.toString(); } - String toGenericString() { return accessible instanceof Field ? ((Field) accessible).toGenericString() : accessible instanceof Method ? ((Method) accessible).toGenericString() : ((MethodParam)accessible).toString(); } - boolean isMethodParameter() { return accessible instanceof MethodParam; } - String mixinName() { + public String getToString() { + if (isMixin()) { return abbreviate("mixin from member " + toGenericString()); } + return (accessible instanceof Field ? "field " : accessible instanceof Method ? "method " : accessible.getClass().getSimpleName() + " ") + abbreviate(toGenericString()); + } + public String toGenericString() { return accessible instanceof Field ? ((Field) accessible).toGenericString() : accessible instanceof Method ? ((Method) accessible).toGenericString() : ((MethodParam)accessible).toString(); } + public boolean hasInitialValue() { return hasInitialValue; } + public boolean isMethodParameter() { return accessible instanceof MethodParam; } + public int getMethodParamPosition() { return isMethodParameter() ? ((MethodParam) accessible).position : -1; } + public String getMixinName() { String annotationName = getAnnotation(Mixin.class).name(); - return empty(annotationName) ? name() : annotationName; + return empty(annotationName) ? getName() : annotationName; } - static String propertyName(String methodName) { if (methodName.length() > 3 && (methodName.startsWith("get") || methodName.startsWith("set"))) { return decapitalize(methodName.substring(3)); } return decapitalize(methodName); @@ -6174,6 +6544,12 @@ private static String decapitalize(String name) { chars[0] = Character.toLowerCase(chars[0]); return new String(chars); } + static String abbreviate(String text) { + return text.replace("private ", "") + .replace("protected ", "") + .replace("public ", "") + .replace("java.lang.", ""); + } } /** Utility class for getting resource bundle strings. @@ -6234,13 +6610,29 @@ private static String decapitalize(String name) { * @since 3.6 */ public static class Messages { private final CommandSpec spec; + private final String bundleBaseName; private final ResourceBundle rb; private final Set keys; + public Messages(CommandSpec spec, String baseName) { + this(spec, baseName, createBundle(baseName)); + } public Messages(CommandSpec spec, ResourceBundle rb) { + this(spec, extractName(rb), rb); + } + public Messages(CommandSpec spec, String baseName, ResourceBundle rb) { this.spec = Assert.notNull(spec, "CommandSpec"); + this.bundleBaseName = baseName; this.rb = rb; this.keys = keys(rb); } + private static ResourceBundle createBundle(String baseName) { + return ResourceBundle.getBundle(baseName); + } + private static String extractName(ResourceBundle rb) { + try { // ResourceBundle.getBaseBundleName was introduced in Java 8 + return (String) ResourceBundle.class.getDeclaredMethod("getBaseBundleName").invoke(rb); + } catch (Exception ignored) { return ""; } + } private static Set keys(ResourceBundle rb) { if (rb == null) { return Collections.emptySet(); } Set keys = new LinkedHashSet(); @@ -6254,7 +6646,7 @@ private static Set keys(ResourceBundle rb) { * @return a Messages object with the specified CommandSpec and the ResourceBundle of the specified Messages object */ public static Messages copy(CommandSpec spec, Messages original) { - return original == null ? null : new Messages(spec, original.rb); + return original == null ? null : new Messages(spec, original.bundleBaseName, original.rb); } /** Returns {@code true} if the specified {@code Messages} is {@code null} or has a {@code null ResourceBundle}. */ public static boolean empty(Messages messages) { return messages == null || messages.rb == null; } @@ -6301,8 +6693,14 @@ private static List addAllWithPrefix(ResourceBundle rb, String key, Set< } } } + /** Returns the ResourceBundle of the specified Messages object or {@code null} if the specified Messages object is {@code null}. + * @since 4.0 */ + public static String resourceBundleBaseName(Messages messages) { return messages == null ? null : messages.resourceBundleBaseName(); } /** Returns the ResourceBundle of the specified Messages object or {@code null} if the specified Messages object is {@code null}. */ public static ResourceBundle resourceBundle(Messages messages) { return messages == null ? null : messages.resourceBundle(); } + /** Returns the base name of the ResourceBundle of this object or {@code null}. + * @since 4.0 */ + public String resourceBundleBaseName() { return bundleBaseName; } /** Returns the ResourceBundle of this object or {@code null}. */ public ResourceBundle resourceBundle() { return rb; } /** Returns the CommandSpec of this object, never {@code null}. */ @@ -6345,8 +6743,12 @@ static CommandSpec extractCommandSpec(Object command, IFactory factory, boolean boolean mixinStandardHelpOptions = false; while (!hierarchy.isEmpty()) { cls = hierarchy.pop(); - boolean thisCommandHasAnnotation = updateCommandAttributes(cls, result, factory); - hasCommandAnnotation |= thisCommandHasAnnotation; + Command cmd = cls.getAnnotation(Command.class); + if (cmd != null) { + result.updateCommandAttributes(cmd, factory); + initSubcommands(cmd, result, factory); + hasCommandAnnotation = true; + } hasCommandAnnotation |= initFromAnnotatedFields(instance, cls, result, factory); if (cls.isAnnotationPresent(Command.class)) { mixinStandardHelpOptions |= cls.getAnnotation(Command.class).mixinStandardHelpOptions(); @@ -6357,7 +6759,11 @@ static CommandSpec extractCommandSpec(Object command, IFactory factory, boolean Method method = (Method) command; t.debug("Using method %s as command %n", method); commandClassName = method.toString(); - hasCommandAnnotation |= updateCommandAttributes(method, result, factory); + Command cmd = method.getAnnotation(Command.class); + result.updateCommandAttributes(cmd, factory); + result.setAddMethodSubcommands(false); // method commands don't have method subcommands + initSubcommands(cmd, result, factory); + hasCommandAnnotation = true; result.mixinStandardHelpOptions(method.getAnnotation(Command.class).mixinStandardHelpOptions()); initFromMethodParameters(instance, method, result, factory); // set command name to method name, unless @Command#name is set @@ -6369,31 +6775,6 @@ static CommandSpec extractCommandSpec(Object command, IFactory factory, boolean result.withToString(commandClassName).validate(); return result; } - - private static boolean updateCommandAttributes(Class cls, CommandSpec commandSpec, IFactory factory) { - // superclass values should not overwrite values if both class and superclass have a @Command annotation - if (!cls.isAnnotationPresent(Command.class)) { return false; } - - Command cmd = cls.getAnnotation(Command.class); - return updateCommandAttributes(cmd, commandSpec, factory); - } - private static boolean updateCommandAttributes(Method method, CommandSpec commandSpec, IFactory factory) { - Command cmd = method.getAnnotation(Command.class); - return updateCommandAttributes(cmd, commandSpec, factory); - } - private static boolean updateCommandAttributes(Command cmd, CommandSpec commandSpec, IFactory factory) { - commandSpec.aliases(cmd.aliases()); - commandSpec.parser().updateSeparator(cmd.separator()); - commandSpec.updateName(cmd.name()); - commandSpec.updateVersion(cmd.version()); - commandSpec.updateHelpCommand(cmd.helpCommand()); - commandSpec.updateVersionProvider(cmd.versionProvider(), factory); - commandSpec.initDefaultValueProvider(cmd.defaultValueProvider(), factory); - commandSpec.usageMessage().updateFromCommand(cmd, commandSpec); - - initSubcommands(cmd, commandSpec, factory); - return true; - } private static void initSubcommands(Command cmd, CommandSpec parent, IFactory factory) { for (Class sub : cmd.subcommands()) { try { @@ -6454,7 +6835,7 @@ private static boolean initFromAnnotatedTypedMembers(TypedMember member, Command if (member == null) { return result; } if (member.isMixin()) { validateMixin(member); - receiver.addMixin(member.mixinName(), buildMixinForField(member, factory)); + receiver.addMixin(member.getMixinName(), buildMixinForField(member, factory)); result = true; } if (member.isUnmatched()) { @@ -6464,9 +6845,9 @@ private static boolean initFromAnnotatedTypedMembers(TypedMember member, Command if (member.isArgSpec()) { validateArgSpecField(member); Messages msg = receiver.usageMessage.messages(); - if (member.isOption()) { receiver.addOption(ArgsReflection.extractOptionSpec(member, factory)); } - else if (member.isParameter()) { receiver.addPositional(ArgsReflection.extractPositionalParamSpec(member, factory)); } - else { receiver.addPositional(ArgsReflection.extractUnannotatedPositionalParamSpec(member, factory)); } + if (member.isOption()) { receiver.addOption(OptionSpec.builder(member, factory).build()); } + else if (member.isParameter()) { receiver.addPositional(PositionalParamSpec.builder(member, factory).build()); } + else { receiver.addPositional(PositionalParamSpec.builder(member, factory).build()); } } if (member.isInjectSpec()) { validateInjectSpec(member); @@ -6497,7 +6878,7 @@ private static void validateMixin(TypedMember member) { throw new DuplicateOptionAnnotationsException("A member cannot be both a @Mixin command and an @Unmatched but '" + member + "' is both."); } } - private static void validateUnmatched(TypedMember member) { + private static void validateUnmatched(IAnnotatedElement member) { if (member.isUnmatched() && member.isArgSpec()) { throw new DuplicateOptionAnnotationsException("A member cannot have both @Unmatched and @Option or @Parameters annotations, but '" + member + "' has both."); } @@ -6533,30 +6914,32 @@ private static void validateInjectSpec(TypedMember member) { if (member.isMixin()) { throw new DuplicateOptionAnnotationsException("A member cannot have both @Spec and @Mixin annotations, but '" + member + "' has both."); } - if (member.getType() != CommandSpec.class) { throw new InitializationException("@picocli.CommandLine.Spec annotation is only supported on fields of type " + CommandSpec.class.getName()); } + if (!CommandSpec.class.getName().equals(member.getTypeInfo().getClassName())) { + throw new InitializationException("@picocli.CommandLine.Spec annotation is only supported on fields of type " + CommandSpec.class.getName()); + } } - private static CommandSpec buildMixinForField(TypedMember member, IFactory factory) { + private static CommandSpec buildMixinForField(IAnnotatedElement member, IFactory factory) { try { Object userObject = member.getter().get(); if (userObject == null) { - userObject = factory.create(member.getType()); + userObject = factory.create(member.getTypeInfo().getType()); member.setter().set(userObject); } CommandSpec result = CommandSpec.forAnnotatedObject(userObject, factory); - return result.withToString(abbreviate("mixin from member " + member.toGenericString())); + return result.withToString(member.getToString()); } catch (InitializationException ex) { throw ex; } catch (Exception ex) { throw new InitializationException("Could not access or modify mixin member " + member + ": " + ex, ex); } } - private static UnmatchedArgsBinding buildUnmatchedForField(final TypedMember member) { - if (!(member.getType().equals(String[].class) || - (List.class.isAssignableFrom(member.getType()) && member.getGenericType() instanceof ParameterizedType - && ((ParameterizedType) member.getGenericType()).getActualTypeArguments()[0].equals(String.class)))) { + private static UnmatchedArgsBinding buildUnmatchedForField(final IAnnotatedElement member) { + ITypeInfo info = member.getTypeInfo(); + if (!(info.getClassName().equals(String[].class.getName()) || + (info.isCollection() && info.getActualGenericTypeArguments().equals(Arrays.asList(String.class.getName()))))) { throw new InitializationException("Invalid type for " + member + ": must be either String[] or List"); } - if (member.getType().equals(String[].class)) { + if (info.getClassName().equals(String[].class.getName())) { return UnmatchedArgsBinding.forStringArrayConsumer(member.setter()); } else { return UnmatchedArgsBinding.forStringCollectionSupplier(new IGetter() { @@ -6573,138 +6956,6 @@ private static UnmatchedArgsBinding buildUnmatchedForField(final TypedMember mem } } - /** Helper class to reflectively create OptionSpec and PositionalParamSpec objects from annotated elements. - * Package protected for testing. CONSIDER THIS CLASS PRIVATE. */ - static class ArgsReflection { - static OptionSpec extractOptionSpec(TypedMember member, IFactory factory) { - Option option = member.getAnnotation(Option.class); - OptionSpec.Builder builder = OptionSpec.builder(option.names()); - initCommon(builder, member); - - builder.order(option.order()); - builder.help(option.help()); - builder.usageHelp(option.usageHelp()); - builder.versionHelp(option.versionHelp()); - builder.showDefaultValue(option.showDefaultValue()); - if (!NoCompletionCandidates.class.equals(option.completionCandidates())) { - builder.completionCandidates(DefaultFactory.createCompletionCandidates(factory, option.completionCandidates())); - } - - builder.arity(Range.optionArity(member)); - builder.required(option.required()); - builder.interactive(option.interactive()); - Class[] elementTypes = inferTypes(member.getType(), option.type(), member.getGenericType()); - builder.auxiliaryTypes(elementTypes); - builder.paramLabel(inferLabel(option.paramLabel(), member.name(), member.getType(), elementTypes)); - builder.hideParamSyntax(option.hideParamSyntax()); - builder.description(option.description()); - builder.descriptionKey(option.descriptionKey()); - builder.splitRegex(option.split()); - builder.hidden(option.hidden()); - builder.defaultValue(option.defaultValue()); - builder.converters(DefaultFactory.createConverter(factory, option.converter())); - return builder.build(); - } - static PositionalParamSpec extractPositionalParamSpec(TypedMember member, IFactory factory) { - PositionalParamSpec.Builder builder = PositionalParamSpec.builder(); - initCommon(builder, member); - Range arity = Range.parameterArity(member); - builder.arity(arity); - builder.index(Range.parameterIndex(member)); - builder.capacity(Range.parameterCapacity(member)); - builder.required(arity.min > 0); - - Parameters parameters = member.getAnnotation(Parameters.class); - builder.interactive(parameters.interactive()); - Class[] elementTypes = inferTypes(member.getType(), parameters.type(), member.getGenericType()); - builder.auxiliaryTypes(elementTypes); - builder.paramLabel(inferLabel(parameters.paramLabel(), member.name(), member.getType(), elementTypes)); - builder.hideParamSyntax(parameters.hideParamSyntax()); - builder.description(parameters.description()); - builder.descriptionKey(parameters.descriptionKey()); - builder.splitRegex(parameters.split()); - builder.hidden(parameters.hidden()); - builder.defaultValue(parameters.defaultValue()); - builder.converters(DefaultFactory.createConverter(factory, parameters.converter())); - builder.showDefaultValue(parameters.showDefaultValue()); - if (!NoCompletionCandidates.class.equals(parameters.completionCandidates())) { - builder.completionCandidates(DefaultFactory.createCompletionCandidates(factory, parameters.completionCandidates())); - } - return builder.build(); - } - static PositionalParamSpec extractUnannotatedPositionalParamSpec(TypedMember member, IFactory factory) { - PositionalParamSpec.Builder builder = PositionalParamSpec.builder(); - initCommon(builder, member); - Range arity = Range.parameterArity(member); - builder.arity(arity); - builder.index(Range.parameterIndex(member)); - builder.capacity(Range.parameterCapacity(member)); - builder.required(arity.min > 0); - - builder.interactive(false); - Class[] elementTypes = inferTypes(member.getType(), new Class[] {}, member.getGenericType()); - builder.auxiliaryTypes(elementTypes); - builder.paramLabel(inferLabel(null, member.name(), member.getType(), elementTypes)); - builder.hideParamSyntax(false); - builder.description(new String[0]); - builder.splitRegex(""); - builder.hidden(false); - builder.defaultValue(null); - builder.converters(); - builder.showDefaultValue(Help.Visibility.ON_DEMAND); - return builder.build(); - } - private static void initCommon(ArgSpec.Builder builder, TypedMember member) { - builder.type(member.getType()); - builder.withToString((member.accessible instanceof Field ? "field " : member.accessible instanceof Method ? "method " : member.accessible.getClass().getSimpleName() + " ") + abbreviate(member.toGenericString())); - - builder.getter(member.getter()).setter(member.setter()); - builder.hasInitialValue(member.hasInitialValue); - try { builder.initialValue(member.getter().get()); } catch (Exception ex) { builder.initialValue(null); } - } - static String abbreviate(String text) { - return text.replace("private ", "") - .replace("protected ", "") - .replace("public ", "") - .replace("java.lang.", ""); - } - private static String inferLabel(String label, String fieldName, Class fieldType, Class[] types) { - if (!empty(label)) { return label.trim(); } - String name = fieldName; - if (Map.class.isAssignableFrom(fieldType)) { // #195 better param labels for map fields - Class[] paramTypes = types; - if (paramTypes.length < 2 || paramTypes[0] == null || paramTypes[1] == null) { - name = "String=String"; - } else { name = paramTypes[0].getSimpleName() + "=" + paramTypes[1].getSimpleName(); } - } - return "<" + name + ">"; - } - private static Class[] inferTypes(Class propertyType, Class[] annotationTypes, Type genericType) { - if (annotationTypes.length > 0) { return annotationTypes; } - if (propertyType.isArray()) { return new Class[] { propertyType.getComponentType() }; } - if (CommandLine.isMultiValue(propertyType)) { - if (genericType instanceof ParameterizedType) {// e.g. Map - ParameterizedType parameterizedType = (ParameterizedType) genericType; - Type[] paramTypes = parameterizedType.getActualTypeArguments(); // e.g. ? extends Number - Class[] result = new Class[paramTypes.length]; - for (int i = 0; i < paramTypes.length; i++) { - if (paramTypes[i] instanceof Class) { result[i] = (Class) paramTypes[i]; continue; } // e.g. Long - if (paramTypes[i] instanceof WildcardType) { // e.g. ? extends Number - WildcardType wildcardType = (WildcardType) paramTypes[i]; - Type[] lower = wildcardType.getLowerBounds(); // e.g. [] - if (lower.length > 0 && lower[0] instanceof Class) { result[i] = (Class) lower[0]; continue; } - Type[] upper = wildcardType.getUpperBounds(); // e.g. Number - if (upper.length > 0 && upper[0] instanceof Class) { result[i] = (Class) upper[0]; continue; } - } - Arrays.fill(result, String.class); return result; // too convoluted generic type, giving up - } - return result; // we inferred all types from ParameterizedType - } - return new Class[] {String.class, String.class}; // field is multi-value but not ParameterizedType - } - return new Class[] {propertyType}; // not a multi-value field - } - } static class FieldBinding implements IGetter, ISetter { private final Object scope; private final Field field; @@ -6726,6 +6977,10 @@ public T set(T value) throws PicocliException { throw new PicocliException("Could not set value for field " + field + " to " + value, ex); } } + public String toString() { + return String.format("%s(%s %s.%s)", getClass().getCanonicalName(), field.getType().getCanonicalName(), + field.getDeclaringClass().getCanonicalName(), field.getName()); + } } static class MethodBinding implements IGetter, ISetter { private final Object scope; @@ -6755,6 +7010,9 @@ private ParameterException createParameterException(Object value, Throwable t) { CommandLine cmd = spec.commandLine() == null ? new CommandLine(spec) : spec.commandLine(); return new ParameterException(cmd, "Could not invoke " + method + " with " + value, t); } + public String toString() { + return String.format("%s(%s)", getClass().getCanonicalName(), method); + } } private static class PicocliInvocationHandler implements InvocationHandler { final Map map = new HashMap(); @@ -6780,6 +7038,9 @@ public T set(T value) { this.value = value; return result; } + public String toString() { + return String.format("%s(value=%s)", getClass().getCanonicalName(), value); + } } } @@ -8437,7 +8698,7 @@ public interface IHelpSectionRenderer { */ String render(Help help); } - + /** * A collection of methods and inner classes that provide fine-grained control over the contents and layout of * the usage help message to display to end users when help is requested or invalid input values were specified. @@ -8537,7 +8798,7 @@ public Help(CommandSpec commandSpec, ColorScheme colorScheme) { /** Returns the {@code ColorScheme} model that this Help was constructed with. * @since 3.0 */ public ColorScheme colorScheme() { return colorScheme; } - + /** Returns the {@code IHelpFactory} that this Help was constructed with. * @since 3.9 */ private IHelpFactory getHelpFactory() { return commandSpec.usageMessage().helpFactory(); } @@ -8555,7 +8816,7 @@ public Help(CommandSpec commandSpec, ColorScheme colorScheme) { * of the {@link ParserSpec#separator()} at construction time. If the separator is modified after Help construction, you * may need to re-initialize this field by calling {@link #createDefaultParamLabelRenderer()} again. */ public IParamLabelRenderer parameterLabelRenderer() {return parameterLabelRenderer;} - + /** Registers all specified subcommands with this Help. * @param commands maps the command names to the associated CommandLine object * @return this Help instance (for method chaining) @@ -8608,7 +8869,7 @@ Help addSubcommand(List commandNames, CommandLine commandLine) { * @deprecated */ @Deprecated public Help addSubcommand(String commandName, Object command) { - commands.put(commandName, + commands.put(commandName, getHelpFactory().create(CommandSpec.forAnnotatedObject(command, commandSpec.commandLine().factory), defaultColorScheme(Ansi.AUTO))); return this; } @@ -8707,7 +8968,7 @@ protected Text createDetailedSynopsisOptionsText(Comparator optionSo StringBuilder clusteredOptional = new StringBuilder("-"); for (OptionSpec option : options) { if (option.hidden()) { continue; } - boolean isFlagOption = option.type() == boolean.class || option.type() == Boolean.class; + boolean isFlagOption = option.typeInfo().isBoolean(); if (isFlagOption && option.arity().max <= 0) { // #612 consider arity: boolean options may require a parameter String shortestName = option.shortestName(); if (shortestName.length() == 2 && shortestName.startsWith("-")) { diff --git a/src/test/java/picocli/ModelArgSpecTest.java b/src/test/java/picocli/ModelArgSpecTest.java index a3dabcbe3..48c8a65e0 100644 --- a/src/test/java/picocli/ModelArgSpecTest.java +++ b/src/test/java/picocli/ModelArgSpecTest.java @@ -3,10 +3,15 @@ import org.junit.Test; import picocli.CommandLine.Model.IGetter; import picocli.CommandLine.Model.ISetter; +import picocli.CommandLine.Model.ITypeInfo; import picocli.CommandLine.Model.PositionalParamSpec; +import picocli.CommandLine.Model.RuntimeTypeInfo; +import java.lang.reflect.Method; +import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.Map; import static org.junit.Assert.*; import static org.junit.Assert.assertArrayEquals; @@ -192,4 +197,46 @@ public void testArgSpecBuilderCompletionCandidates() { assertEquals(candidates, positional.completionCandidates()); } + + @Test + public void testArgSpecBuilderInferLabel() throws Exception{ + Method m = CommandLine.Model.ArgSpec.Builder.class.getDeclaredMethod("inferLabel", String.class, String.class, ITypeInfo.class); + m.setAccessible(true); + assertEquals("", m.invoke(null, "", "fieldName", typeInfo(new Class[0]))); + assertEquals("", m.invoke(null, "", "fieldName", typeInfo(new Class[]{Integer.class}))); + assertEquals("", m.invoke(null, "", "fieldName", typeInfo(new Class[]{null, Integer.class}))); + assertEquals("", m.invoke(null, "", "fieldName", typeInfo(new Class[]{Integer.class, null}))); + assertEquals("", m.invoke(null, "", "fieldName", typeInfo(new Class[]{Integer.class, Integer.class}))); + } + + private ITypeInfo typeInfo(final Class[] aux) { + return new TypeInfoAdapter() { + public boolean isMap() { return true; } + public List getAuxiliaryTypeInfos() { + List result = new ArrayList(); + for (final Class c : aux) { + if (c == null) { result.add(null); } + result.add(new TypeInfoAdapter() { + public String getClassSimpleName() { return c.getSimpleName(); } + }); + } + return result; + } + }; + } + static class TypeInfoAdapter implements ITypeInfo { + public boolean isMap() { return false; } + public List getAuxiliaryTypeInfos() { return null; } + public List getActualGenericTypeArguments() { return null; } + public boolean isBoolean() { return false; } + public boolean isMultiValue() { return false; } + public boolean isArray() { return false; } + public boolean isCollection() { return false; } + public boolean isEnum() { return false; } + public List getEnumConstantNames() { return null; } + public String getClassName() { return null; } + public String getClassSimpleName() { return null; } + public Class getType() { return null; } + public Class[] getAuxiliaryTypes() { return new Class[0]; } + } } diff --git a/src/test/java/picocli/ModelArgsReflectionTest.java b/src/test/java/picocli/ModelArgsReflectionTest.java deleted file mode 100644 index a4dda5a68..000000000 --- a/src/test/java/picocli/ModelArgsReflectionTest.java +++ /dev/null @@ -1,37 +0,0 @@ -package picocli; - -import org.junit.Test; - -import java.lang.reflect.Method; -import java.util.List; -import java.util.Map; - -import static org.junit.Assert.assertEquals; - -public class ModelArgsReflectionTest { - - @Test - public void testInstantiatable() { - new CommandLine.Model.ArgsReflection(); // no error - } - - @Test - public void testInferLabel() throws Exception{ - Method m = CommandLine.Model.ArgsReflection.class.getDeclaredMethod("inferLabel", String.class, String.class, Class.class, Class[].class); - m.setAccessible(true); - assertEquals("", m.invoke(null, "", "fieldName", Map.class, new Class[0])); - assertEquals("", m.invoke(null, "", "fieldName", Map.class, new Class[]{Integer.class})); - assertEquals("", m.invoke(null, "", "fieldName", Map.class, new Class[]{null, Integer.class})); - assertEquals("", m.invoke(null, "", "fieldName", Map.class, new Class[]{Integer.class, null})); - assertEquals("", m.invoke(null, "", "fieldName", Map.class, new Class[]{Integer.class, Integer.class})); - } - - @Test - public void testInferTypes() { - class App { - @CommandLine.Parameters - List[]>> list; - } - assertEquals("", CommandLine.Model.CommandSpec.forAnnotatedObject(new App()).positionalParameters().get(0).paramLabel()); - } -} diff --git a/src/test/java/picocli/ModelCommandReflectionTest.java b/src/test/java/picocli/ModelCommandReflectionTest.java index 4e0febab7..23609dc44 100644 --- a/src/test/java/picocli/ModelCommandReflectionTest.java +++ b/src/test/java/picocli/ModelCommandReflectionTest.java @@ -89,7 +89,7 @@ public void testValidateMixin() throws Exception { @Test public void testValidateUnmatched() throws Exception { Class reflection = Class.forName("picocli.CommandLine$Model$CommandReflection"); - Method validateUnmatched = reflection.getDeclaredMethod("validateUnmatched", CommandLine.Model.TypedMember.class); + Method validateUnmatched = reflection.getDeclaredMethod("validateUnmatched", CommandLine.Model.IAnnotatedElement.class); validateUnmatched.setAccessible(true); CommandLine.Model.TypedMember typedMember = new CommandLine.Model.TypedMember(TypedMemberObj.class.getDeclaredField("x")); diff --git a/src/test/java/picocli/ModelMessagesTest.java b/src/test/java/picocli/ModelMessagesTest.java index 99d9f705d..218d77f55 100644 --- a/src/test/java/picocli/ModelMessagesTest.java +++ b/src/test/java/picocli/ModelMessagesTest.java @@ -19,7 +19,7 @@ public void testMessagesCopyNull() { @Test public void testMessagesCopyNonNull() { - Messages orig = new Messages(CommandSpec.create(), null); + Messages orig = new Messages(CommandSpec.create(), (ResourceBundle) null); Messages copy = Messages.copy(CommandSpec.create(), orig); assertNull(copy.resourceBundle()); } @@ -27,21 +27,21 @@ public void testMessagesCopyNonNull() { @Test public void testMessagesCommandSpec() { CommandSpec spec = CommandSpec.create(); - Messages orig = new Messages(spec, null); + Messages orig = new Messages(spec, (ResourceBundle) null); assertSame(spec, orig.commandSpec()); } @Test public void testMessagesEmpty() { assertTrue(Messages.empty((Messages) null)); - assertTrue(Messages.empty(new Messages(CommandSpec.create(), null))); + assertTrue(Messages.empty(new Messages(CommandSpec.create(), (ResourceBundle) null))); } @Test public void testMessagesGetStringNullKey() { String def = "abc"; - assertSame(def, new Messages(CommandSpec.create(), null).getString(null, def)); - assertSame(def, new Messages(CommandSpec.create(), null).getString("help", def)); + assertSame(def, new Messages(CommandSpec.create(), (ResourceBundle) null).getString(null, def)); + assertSame(def, new Messages(CommandSpec.create(), (ResourceBundle) null).getString("help", def)); ResourceBundle rb = ResourceBundle.getBundle("picocli.SharedMessages"); assertSame(def, new Messages(CommandSpec.create(), rb).getString(null, def)); @@ -52,8 +52,8 @@ public void testMessagesGetStringNullKey() { @Test public void testMessagesGetStringArrayNullKey() { String[] def = {"abc"}; - assertSame(def, new Messages(CommandSpec.create(), null).getStringArray(null, def)); - assertSame(def, new Messages(CommandSpec.create(), null).getStringArray("help", def)); + assertSame(def, new Messages(CommandSpec.create(), (ResourceBundle) null).getStringArray(null, def)); + assertSame(def, new Messages(CommandSpec.create(), (ResourceBundle) null).getStringArray("help", def)); ResourceBundle rb = ResourceBundle.getBundle("picocli.SharedMessages"); assertSame(def, new Messages(CommandSpec.create(), rb).getStringArray(null, def)); diff --git a/src/test/java/picocli/ModelTestUtil.java b/src/test/java/picocli/ModelTestUtil.java index 6faaa44e0..fccccd412 100644 --- a/src/test/java/picocli/ModelTestUtil.java +++ b/src/test/java/picocli/ModelTestUtil.java @@ -33,7 +33,7 @@ public static OptionSpec option(Object obj, String fieldName) throws Exception { return option(obj, fieldName, CommandLine.defaultFactory()); } public static OptionSpec option(Object obj, String fieldName, CommandLine.IFactory factory) throws Exception { - return ArgsReflection.extractOptionSpec(TypedMember.createIfAnnotated(obj.getClass().getDeclaredField(fieldName), obj), factory); + return OptionSpec.builder(TypedMember.createIfAnnotated(obj.getClass().getDeclaredField(fieldName), obj), factory).build(); } public static OptionSpec[] options(Object obj, String... fieldNames) throws Exception { OptionSpec[] result = new OptionSpec[fieldNames.length]; diff --git a/src/test/java/picocli/ModelTypedMemberTest.java b/src/test/java/picocli/ModelTypedMemberTest.java new file mode 100644 index 000000000..f77b238c5 --- /dev/null +++ b/src/test/java/picocli/ModelTypedMemberTest.java @@ -0,0 +1,21 @@ +package picocli; + +import org.junit.Test; + +import java.lang.reflect.Method; +import java.util.List; +import java.util.Map; + +import static org.junit.Assert.assertEquals; + +public class ModelTypedMemberTest { + + @Test + public void testInferTypes() { + class App { + @CommandLine.Parameters + List[]>> list; + } + assertEquals("", CommandLine.Model.CommandSpec.forAnnotatedObject(new App()).positionalParameters().get(0).paramLabel()); + } +}