Skip to content

Commit

Permalink
[#259] Added @Inject annotation to inject CommandSpec into applic…
Browse files Browse the repository at this point in the history
…ation field.

Closes #259
  • Loading branch information
remkop committed Jun 19, 2018
1 parent b906f8c commit 21b6871
Show file tree
Hide file tree
Showing 4 changed files with 132 additions and 10 deletions.
30 changes: 25 additions & 5 deletions RELEASE-NOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ Picocli follows [semantic versioning](http://semver.org/).
* [Potential breaking changes](#3.2.0-breaking-changes)

## <a name="3.2.0-new"></a> New and Noteworthy
### JLine Tab-Completion Support
### <a name="3.2.0-jline"></a> JLine Tab-Completion Support

This release adds support for JLine Tab-Completion.

Expand Down Expand Up @@ -59,7 +59,7 @@ public class PicocliJLineCompleter implements Completer {
}
```

### Completion Candidates
### <a name="3.2.0-completion-candidates"></a> Completion Candidates
From this release, `@Options` and `@Parameters` have a new `completionCandidates` attribute that can be used to generate a list of completions for this option or positional parameter. For example:

```java
Expand All @@ -76,7 +76,7 @@ class ValidValuesDemo {
This will generate completion option values `A`, `B` and `C` in the generated bash auto-completion script and in JLine.


### `${DEFAULT-VALUE}` Variable
### <a name="3.2.0-default-variable"></a> `${DEFAULT-VALUE}` Variable
From picocli 3.2, it is possible to embed the default values in the description for an option or positional parameter by
specifying the variable `${DEFAULT-VALUE}` in the description text.
Picocli uses reflection to get the default values from the annotated fields.
Expand All @@ -99,7 +99,7 @@ Usage: <main class> -f=<file>
-f, --file=<file> the file to use (default: config.xml)
```

### `${COMPLETION-CANDIDATES}` Variable
### <a name="3.2.0-completion-variable"></a> `${COMPLETION-CANDIDATES}` Variable
Similarly, it is possible to embed the completion candidates in the description for an option or positional parameter by
specifying the variable `${COMPLETION-CANDIDATES}` in the description text.

Expand Down Expand Up @@ -130,7 +130,26 @@ Usage: <main class> -l=<lang> -o=<option>
-o=<option> Candidates: A, B, C
```

### Lenient Parse Mode
### <a name="3.2.0-Inject"></a> `@Inject` Annotation
A new `@Inject` annotation is now available that injects the `CommandSpec` model of the command into an command field.

This is useful when a command needs to use the picocli API, for example to walk the command hierarchy and iterate over its sibling commands.
This complements the `@ParentCommand` annotation; the `@ParentCommand` annotation injects a user-defined command object, whereas this annotation injects a picocli class.

```java
class InjectExample implements Runnable {
@Inject CommandSpec commandSpec;
//...
public void run() {
// do something with the injected object
}
}

```



### <a name="3.2.0-lenient-parse"></a> Lenient Parse Mode

This release adds the ability to continue parsing invalid input to the end.
When `collectErrors` is set to `true`, and a problem occurs during parsing, an `Exception` is added to the `ParseResult.errors()` list and parsing continues. The default behaviour (when `collectErrors` is `false`) is to abort parsing by throwing the `Exception`.
Expand All @@ -152,6 +171,7 @@ No features have been promoted in this picocli release.
- [#391] New feature: Add API to get completion candidates for option and positional parameter values of any type.
- [#393] New feature: Add support for JLine completers.
- [#395] New feature: Allow embedding default values anywhere in description for `@Option` or `@Parameters`.
- [#259] New Feature: Added `@Inject` annotation to inject `CommandSpec` into application field.

## <a name="3.2.0-deprecated"></a> Deprecations
No features were deprecated in this release.
Expand Down
29 changes: 25 additions & 4 deletions docs/index.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -1853,7 +1853,7 @@ private void handleParseResult(List<CommandLine> parsed) {
You may be interested in the <<Convenience Methods for Subcommands,convenience methods for subcommands>> to reduce error handling and other boilerplate code in your application.


=== `@ParentCommand` annotation
=== `@ParentCommand` Annotation
In command line applications with subcommands, options of the top level command are often intended as "global" options that apply to all the subcommands. Prior to picocli 2.2, subcommands had no easy way to access their parent command options unless the parent command made these values available in a global variable.

The `@ParentCommand` annotation introduced in picocli 2.2 makes it easy for subcommands to access their parent command options: subcommand fields annotated with `@ParentCommand` are initialized with a reference to the parent command. For example:
Expand Down Expand Up @@ -1972,7 +1972,7 @@ in <<Expanded Example>>.
From picocli 3.1, the usage help synopsis of the subcommand shows not only the subcommand name but also the parent command name.
For example, if the `git` command has a `commit` subcommand, the usage help for the `commit` subcommand shows `Usage: git commit <options>`.

=== Hidden subcommands
=== Hidden Subcommands

Commands with the `hidden` attribute set to `true` will not be shown in the usage help message of their parent command.

Expand All @@ -1996,7 +1996,7 @@ Commands:
foo This is a visible subcommand
----

=== Help subcommands
=== Help Subcommands

Commands with the `helpCommand` attribute set to `true` are treated as help commands.
When picocli encounters a help command on the command line, required options and required positional parameters of the parent command
Expand All @@ -2009,7 +2009,7 @@ See <<Custom Help Subcommands>> for more details on creating help subcommands.
@Command(helpCommand = true)
----

=== Nested sub-subcommands
=== Nested sub-Subcommands
The specified object can be an annotated object or a
`CommandLine` instance with its own nested subcommands. For example:

Expand Down Expand Up @@ -2301,6 +2301,27 @@ Usage: <main class>

Custom handlers can extend `AbstractHandler` to inherit this behaviour.

=== `@Inject` Annotation
Picocli 3.2 introduces a custom `@Inject` annotation (not `javax.inject.Inject`) for injecting the `CommandSpec` model of the command into a command field.

This is useful when a command needs to use the picocli API, for example to walk the command hierarchy and iterate over its sibling commands.
This complements the `@ParentCommand` annotation; the `@ParentCommand` annotation injects a user-defined command object, whereas this annotation injects a picocli class.

Currently only the `CommandSpec` model of the command can be injected.

[source,java]
----
class InjectExample implements Runnable {
@Inject CommandSpec commandSpec;
//...
public void run() {
// do something with the injected object
}
}
----


=== Custom Factory

Expand Down
36 changes: 35 additions & 1 deletion src/main/java/picocli/CommandLine.java
Original file line number Diff line number Diff line change
Expand Up @@ -2182,7 +2182,23 @@ private static class NoCompletionCandidates implements Iterable<String> {
* @return a String to register the mixin object with, or an empty String if the name of the annotated field should be used */
String name() default "";
}

/**
* Fields annotated with {@code @Inject} will be initialized with the {@code CommandLine} or {@code CommandSpec} for the command the field is part of. Example usage:
* <pre>
* class InjectExample implements Runnable {
* &#064;Inject CommandLine commandLine; // usually you inject either the CommandLine
* &#064;Inject CommandSpec commandSpec; // or the CommandSpec
* //...
* public void run() {
* // do something with the injected objects
* }
* }
* </pre>
* @since 3.2
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface Inject { }
/**
* <p>Annotate your class with {@code @Command} when you want more control over the format of the generated help
* message.
Expand Down Expand Up @@ -4361,6 +4377,11 @@ private static boolean initFromAnnotatedFields(Object scope, Class<?> cls, Comma
if (isOption(field)) { receiver.addOption(ArgsReflection.extractOptionSpec(scope, field, factory)); }
if (isParameter(field)) { receiver.addPositional(ArgsReflection.extractPositionalParamSpec(scope, field, factory)); }
}
if (isInject(field)) {
validateInject(field);
field.setAccessible(true);
new FieldBinding(scope, field).set(receiver);
}
}
return result;
}
Expand Down Expand Up @@ -4398,6 +4419,18 @@ private static void validateCommandSpec(CommandSpec result, boolean hasCommandAn
throw new InitializationException(command.getClass().getName() + " is not a command: it has no @Command, @Option, @Parameters or @Unmatched annotations");
}
}
private static void validateInject(Field field) {
if (isInject(field) && (isOption(field) || isParameter(field))) {
throw new DuplicateOptionAnnotationsException("A field cannot have both @Inject and @Option or @Parameters annotations, but '" + field + "' has both.");
}
if (isInject(field) && isUnmatched(field)) {
throw new DuplicateOptionAnnotationsException("A field cannot have both @Inject and @Unmatched annotations, but '" + field + "' has both.");
}
if (isInject(field) && isMixin(field)) {
throw new DuplicateOptionAnnotationsException("A field cannot have both @Inject and @Mixin annotations, but '" + field + "' has both.");
}
if (field.getType() != CommandSpec.class) { throw new InitializationException("@picocli.CommandLine.Inject annotation is only supported on fields of type " + CommandSpec.class.getName()); }
}
private static CommandSpec buildMixinForField(Field field, Object scope, IFactory factory) {
try {
field.setAccessible(true);
Expand Down Expand Up @@ -4442,6 +4475,7 @@ private static UnmatchedArgsBinding buildUnmatchedForField(final Field field, fi
static boolean isParameter(Field f) { return f.isAnnotationPresent(Parameters.class); }
static boolean isMixin(Field f) { return f.isAnnotationPresent(Mixin.class); }
static boolean isUnmatched(Field f) { return f.isAnnotationPresent(Unmatched.class); }
static boolean isInject(Field f) { return f.isAnnotationPresent(Inject.class); }
}

/** Helper class to reflectively create OptionSpec and PositionalParamSpec objects from annotated elements.
Expand Down
47 changes: 47 additions & 0 deletions src/test/java/picocli/CommandLineModelTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -1642,4 +1642,51 @@ public void testSubcommandNameNotOverwrittenWhenAddedToParent() {
" -x x option%n");
assertEquals(expected, systemOutRule.getLog());
}

@Test
public void testInject_AnnotatedFieldInjected() {
class Injected {
@Inject CommandSpec commandSpec;
@Parameters String[] params;
}
Injected injected = new Injected();
assertNull(injected.commandSpec);

CommandLine cmd = new CommandLine(injected);
assertSame(cmd.getCommandSpec(), injected.commandSpec);
}

@Test
public void testInject_AnnotatedFieldInjectedForSubcommand() {
class Injected {
@Inject CommandSpec commandSpec;
@Parameters String[] params;
}
Injected injected = new Injected();
Injected sub = new Injected();

assertNull(injected.commandSpec);
assertNull(sub.commandSpec);

CommandLine cmd = new CommandLine(injected);
assertSame(cmd.getCommandSpec(), injected.commandSpec);

CommandLine subcommand = new CommandLine(sub);
assertSame(subcommand.getCommandSpec(), sub.commandSpec);
}

@Test
public void testInject_FieldMustBeCommandSpec() {
class Injected {
@Inject CommandLine commandLine;
@Parameters String[] params;
}
Injected injected = new Injected();
try {
new CommandLine(injected);
fail("Expect exception");
} catch (InitializationException ex) {
assertEquals("@picocli.CommandLine.Inject annotation is only supported on fields of type picocli.CommandLine$Model$CommandSpec", ex.getMessage());
}
}
}

0 comments on commit 21b6871

Please sign in to comment.