Skip to content

Commit

Permalink
Add support for @command(subCommands={...}) param
Browse files Browse the repository at this point in the history
See `ApplicationContextAwarePicocliFactory`

fixes #9 and remkop/picocli#658
related to #7

---

- [x] Impl
- [x] Test
- [ ] Sample
- [x] Doc
  • Loading branch information
kakawait committed Apr 22, 2019
1 parent 24defdb commit 7f499ea
Show file tree
Hide file tree
Showing 5 changed files with 221 additions and 62 deletions.
72 changes: 58 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,9 +117,64 @@ But the following example will not be candidate for _Main_ command
class MainCommand {}
```

#### Nested sub-commands using beans
#### Nested sub-commands

Picocli allows [_nested sub-commands_](http://picocli.info/#_nested_sub_subcommands), in order to describe a _nested sub-command_, starter is offering you nested classes scanning capability.
Picocli allows [_nested sub-commands_](http://picocli.info/#_nested_sub_subcommands), in order to describe a _nested sub-command_, starter is offering two ways to describe your structure.

Please refer to next points to see how to construct this following command line application using both way:

```
Commands:
flyway [-h, --help]
migrate
repair
```

`java -jar <name>.jar flyway migrate` will execute _Flyway_ migration.

##### Using `subCommands` from `@Command` annotation

```java
@Component
@Command(name = "flyway", subCommands = { MigrateCommand.class, RepairCommand.class })
class FlywayCommand extends HelpAwareContainerPicocliCommand {}

@Component
@Command(name = "migrate")
class MigrateCommand implements Runnable {

private final Flyway flyway;

public MigrateCommand(Flyway flyway) {
this.flyway = flyway;
}

@Override
public void run() {
flyway.migrate();
}
}

@Component
@Command(name = "repair")
class RepairCommand implements Runnable {
private final Flyway flyway;

public RepairCommand(Flyway flyway) {
this.flyway = flyway;
}

@Override
public void run() {
flyway.repair();
}
}
```

By default starter is providing a custom implementation [`ApplicationContextAwarePicocliFactory`](picocli-spring-boot-autoconfigure/src/main/java/com/kakawait/spring/boot/picocli/autoconfigure/ApplicationContextAwarePicocliFactory.java) of [`CommandLine.IFactory`](https://picocli.info/apidocs/picocli/CommandLine.IFactory.html) that will delegate instance creation to _Spring_ `ApplicationContext` in order to load bean if exists.
**ATTENTION** If subCommand is not a defined bean, [`ApplicationContextAwarePicocliFactory`](picocli-spring-boot-autoconfigure/src/main/java/com/kakawait/spring/boot/picocli/autoconfigure/ApplicationContextAwarePicocliFactory.java) will only instanciate class without any _autowiring_ capability.

##### Using java nested class hierarchy

That means, if you're defining **bean** structure like following:

Expand Down Expand Up @@ -161,20 +216,9 @@ class FlywayCommand extends HelpAwareContainerPicocliCommand {
}
```

Will generate command line

```
Commands:
flyway [-h, --help]
migrate
repair
```

Thus `java -jar <name>.jar flyway migrate` will execute _Flyway_ migration.

**ATTENTION** every classes must be a bean (`@Component`) with `@Command` annotation without forgetting to file `name` attribute.

There is **no limitation** about nesting level.
Otherwise, there is **no limitation** about nesting level.

### Additional configuration

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package com.kakawait.spring.boot.picocli.autoconfigure;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.ApplicationContext;
import picocli.CommandLine;

import java.lang.reflect.Constructor;

/**
* @author Thibaud Leprêtre
*/
public class ApplicationContextAwarePicocliFactory implements CommandLine.IFactory {
private static final Logger logger = LoggerFactory.getLogger(ApplicationContextAwarePicocliFactory.class);

private final ApplicationContext applicationContext;

public ApplicationContextAwarePicocliFactory(ApplicationContext applicationContext) {
this.applicationContext = applicationContext;
}

@Override
public <K> K create(Class<K> aClass) throws Exception {
try {
return applicationContext.getBean(aClass);
} catch (Exception e) {
logger.info("unable to get bean of class {}, use standard factory creation", aClass);
try {
return aClass.newInstance();
} catch (Exception ex) {
Constructor<K> constructor = aClass.getDeclaredConstructor();
constructor.setAccessible(true);
return constructor.newInstance();
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@

import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;

import org.slf4j.Logger;
Expand Down Expand Up @@ -41,6 +42,12 @@
@Import(PicocliAutoConfiguration.CommandlineConfiguration.class)
class PicocliAutoConfiguration {

@Bean
@ConditionalOnMissingBean(CommandLine.IFactory.class)
CommandLine.IFactory applicationAwarePicocliFactory(ApplicationContext applicationContext) {
return new ApplicationContextAwarePicocliFactory(applicationContext);
}

@Bean
@ConditionalOnMissingBean(PicocliCommandLineRunner.class)
@ConditionalOnBean(CommandLine.class)
Expand All @@ -52,19 +59,25 @@ CommandLineRunner picocliCommandLineRunner(CommandLine cli) {
@Conditional(CommandCondition.class)
static class CommandlineConfiguration {

private final Logger logger = LoggerFactory.getLogger(CommandlineConfiguration.class);
private static final Logger logger = LoggerFactory.getLogger(CommandlineConfiguration.class);

private final CommandLine.IFactory applicationAwarePicocliFactory;

public CommandlineConfiguration(CommandLine.IFactory applicationAwarePicocliFactory) {
this.applicationAwarePicocliFactory = applicationAwarePicocliFactory;
}

@Bean
CommandLine picocliCommandLine(ApplicationContext applicationContext) {
Collection<Object> commands = applicationContext.getBeansWithAnnotation(Command.class).values();
List<Object> mainCommands = getMainCommands(commands);
Object mainCommand = mainCommands.isEmpty() ? new HelpAwarePicocliCommand() {} : mainCommands.get(0);
if (mainCommands.size() > 1) {
throw new RuntimeException("Multiple mains command founds: " + Arrays.asList(mainCommands));
throw new RuntimeException("Multiple mains command founds: " + Collections.singletonList(mainCommands));
}
commands.removeAll(mainCommands);

CommandLine cli = new CommandLine(mainCommand);
CommandLine cli = new CommandLine(mainCommand, applicationAwarePicocliFactory);
registerCommands(cli, commands);

applicationContext.getBeansOfType(PicocliConfigurer.class).values().forEach(c -> c.configure(cli));
Expand Down Expand Up @@ -161,26 +174,24 @@ private void registerCommands(CommandLine cli, Collection<Object> commands) {
}

if (children.isEmpty()) {
if(!current.getSubcommands().containsKey(commandName)) {
current.addSubcommand(commandName, command);
}
} else {
CommandLine sub = null;
if(!current.getSubcommands().containsKey(commandName)){
sub = new CommandLine(command);
current.addSubcommand(commandName, sub);
if(!current.getSubcommands().containsKey(commandName)) {
current.addSubcommand(commandName, command);
}
else {
// get the reference of subCommands from current, instead of creating new one
sub = current.getSubcommands().get(commandName);
} else {
CommandLine sub;
if(!current.getSubcommands().containsKey(commandName)) {
sub = new CommandLine(command, applicationAwarePicocliFactory);
current.addSubcommand(commandName, sub);
} else {
// get the reference of subCommands from current, instead of creating new one
sub = current.getSubcommands().get(commandName);
}

for (Object child : children) {
String childCommandName = getCommandName(child);
if(!sub.getSubcommands().containsKey(childCommandName)) {
sub.addSubcommand(childCommandName, new CommandLine(child));
}

String childCommandName = getCommandName(child);
if (!sub.getSubcommands().containsKey(childCommandName)) {
sub.addSubcommand(childCommandName, new CommandLine(child, applicationAwarePicocliFactory));
}
}
current = sub;
}
Expand Down Expand Up @@ -220,26 +231,26 @@ public boolean equals(Object o) {

Node node = (Node) o;

return clazz != null ? clazz.equals(node.clazz) : node.clazz == null;
return Objects.equals(clazz, node.clazz);
}

@Override
public int hashCode() {
return clazz != null ? clazz.hashCode() : 0;
}

@Override
public String toString() {
StringBuilder builder = new StringBuilder();
builder.append("Node [clazz=");
builder.append(clazz);
builder.append(", object=");
builder.append(object);
builder.append(", parent=");
builder.append(parent);
builder.append("]");
return builder.toString();
}
@Override
public String toString() {
StringBuilder builder = new StringBuilder();
builder.append("Node [clazz=");
builder.append(clazz);
builder.append(", object=");
builder.append(object);
builder.append(", parent=");
builder.append(parent);
builder.append("]");
return builder.toString();
}
}
}

Expand Down
Loading

0 comments on commit 7f499ea

Please sign in to comment.