Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 63 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@

[![Travis](https://img.shields.io/travis/kakawait/picocli-spring-boot-starter.svg)](https://travis-ci.org/kakawait/picocli-spring-boot-starter)
[![SonarQube Coverage](https://img.shields.io/sonar/https/sonarcloud.io/com.kakawait%3Apicocli-spring-boot-starter-parent/coverage.svg)](https://sonarcloud.io/component_measures?id=com.kakawait%3Apicocli-spring-boot-starter-parent&metric=coverage)
[![Maven Central](https://img.shields.io/maven-central/v/com.kakawait/picocli-spring-boot-starter.svg)](https://search.maven.org/#artifactdetails%7Ccom.kakawait%7Cpicocli-spring-boot-starter%7C0.2.0%7Cjar)
[![Maven Central](https://img.shields.io/maven-central/v/com.kakawait/picocli-spring-boot-starter.svg)](https://search.maven.org/#artifactdetails%7Ccom.kakawait%7Cpicocli-spring-boot-starter%7C1.0.0-beta-1%7Cjar)
[![license](https://img.shields.io/github/license/kakawait/picocli-spring-boot-starter.svg)](https://github.com/kakawait/picocli-spring-boot-starter/blob/master/LICENSE.md)
[![Twitter Follow](https://img.shields.io/twitter/follow/thibaudlepretre.svg?style=social&label=%40thibaudlepretre)](https://twitter.com/intent/follow?screen_name=thibaudlepretre)

> A Spring boot starter for [Picocli](http://picocli.info/) command line tools. That let you easily write CLI for Spring boot application!

Expand All @@ -24,7 +25,7 @@ Add the Spring boot starter to your project
<dependency>
<groupId>com.kakawait</groupId>
<artifactId>picocli-spring-boot-starter</artifactId>
<version>0.2.0</version>
<version>1.0.0-beta-1</version>
</dependency>
```

Expand Down Expand Up @@ -116,9 +117,66 @@ 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.

You can mix methods but is not recommended!

##### 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 @@ -160,20 +218,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
26 changes: 17 additions & 9 deletions picocli-spring-boot-autoconfigure/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<modelVersion>4.0.0</modelVersion>
<groupId>com.kakawait</groupId>
<artifactId>picocli-spring-boot-autoconfigure</artifactId>
<version>0.2.0</version>
<version>1.0.0-beta-1</version>
<packaging>jar</packaging>

<name>Picocli spring boot autoconfigure</name>
Expand Down Expand Up @@ -41,17 +41,17 @@
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>

<spring-boot.version>1.5.4.RELEASE</spring-boot.version>
<spring-boot.version>2.1.4.RELEASE</spring-boot.version>

<picocli.version>0.9.8</picocli.version>
<picocli.version>3.9.6</picocli.version>
<slf4j-api.version>1.7.25</slf4j-api.version>

<assertj-core.version>3.8.0</assertj-core.version>
<assertj-core.version>3.12.2</assertj-core.version>
<java-hamcrest.version>2.0.0.0</java-hamcrest.version>
<mockito-core.version>2.15.0</mockito-core.version>
<mockito-core.version>2.27.0</mockito-core.version>

<maven-source-plugin.version>3.0.1</maven-source-plugin.version>
<maven-javadoc-plugin.version>2.10.4</maven-javadoc-plugin.version>
<maven-javadoc-plugin.version>3.0.1</maven-javadoc-plugin.version>
<maven-gpg-plugin.version>1.6</maven-gpg-plugin.version>
<nexus-staging-maven-plugin.version>1.6.8</nexus-staging-maven-plugin.version>
</properties>
Expand Down Expand Up @@ -81,7 +81,6 @@
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>${slf4j-api.version}</version>
</dependency>

<dependency>
Expand All @@ -95,7 +94,16 @@
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>

<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
Expand Down Expand Up @@ -142,7 +150,7 @@
<profile>
<id>release</id>
<properties>
<gpg.executable>gpg2</gpg.executable>
<gpg.executable>gpg</gpg.executable>
</properties>
<build>
<plugins>
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
@@ -1,10 +1,20 @@
package com.kakawait.spring.boot.picocli.autoconfigure;

import java.lang.reflect.Method;
import java.util.ArrayList;
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;
import org.slf4j.LoggerFactory;
import org.springframework.aop.support.AopUtils;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.condition.ConditionMessage;
import org.springframework.boot.autoconfigure.condition.ConditionOutcome;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
Expand All @@ -20,18 +30,9 @@
import org.springframework.context.annotation.Import;
import org.springframework.core.type.AnnotatedTypeMetadata;
import org.springframework.util.ReflectionUtils;
import picocli.CommandLine;

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

import static picocli.CommandLine.Command;
import picocli.CommandLine;
import picocli.CommandLine.Command;

/**
* @author Thibaud Leprêtre
Expand All @@ -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) {
logger.warn("Multiple mains command founds [{}], selected first one {}", mainCommands, mainCommand);
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 @@ -159,13 +172,26 @@ private void registerCommands(CommandLine cli, Collection<Object> commands) {
} else if (node.getParent() == null) {
current = cli;
}

if (children.isEmpty()) {
current.addSubcommand(commandName, command);
if (!current.getSubcommands().containsKey(commandName)) {
current.addSubcommand(commandName, command);
}
} else {
CommandLine sub = new CommandLine(command);
current.addSubcommand(commandName, sub);
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) {
sub.addSubcommand(getCommandName(child), 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 @@ -205,13 +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();
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ public void run(String... args) throws Exception {
cli.usage(System.err, Ansi.AUTO);
return;
}
if (isHelpRequested(cli.getCommand())) {
if (isHelpRequested((Object) cli.getCommand())) {
cli.usage(System.out, Ansi.AUTO);
return;
}
Expand Down
Loading