mirror of
https://github.com/jlengrand/picocli.git
synced 2026-03-10 08:41:17 +00:00
For now I prefer the simplicity of having a single module. The [docs](https://docs.spring.io/spring-boot/docs/current/reference/html/boot-features-developing-auto-configuration.html#boot-features-custom-starter-naming) mention that if you only have one module that combines [the starter and auto-configuration], name it XXX-spring-boot-starter.
This commit is contained in:
@@ -6,19 +6,20 @@ plugins {
|
||||
}
|
||||
|
||||
group 'info.picocli'
|
||||
description 'Picocli Spring Boot Starter - Dependency Descriptor to Easily get Started with Picocli and Spring.'
|
||||
description 'Picocli Spring Boot Starter - Enables Spring Dependency Injection and Spring Boot AutoConfiguration in Picocli Commands.'
|
||||
version "$projectVersion"
|
||||
|
||||
sourceCompatibility = 1.8
|
||||
targetCompatibility = 1.8
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compile "info.picocli:picocli-spring-boot-autoconfigure:$projectVersion"
|
||||
compile rootProject
|
||||
compileOnly project(':picocli-codegen')
|
||||
compile "org.springframework.boot:spring-boot-starter:$springBootVersion"
|
||||
compileOnly "org.springframework.boot:spring-boot-configuration-processor:$springBootVersion"
|
||||
annotationProcessor "org.springframework.boot:spring-boot-configuration-processor:$springBootVersion"
|
||||
}
|
||||
|
||||
jar {
|
||||
@@ -29,7 +30,7 @@ jar {
|
||||
'Implementation-Title' : 'Picocli Spring Boot Starter',
|
||||
'Implementation-Vendor' : 'Remko Popma',
|
||||
'Implementation-Version': version,
|
||||
'Automatic-Module-Name' : 'info.picocli.spring.boot.starter'
|
||||
'Automatic-Module-Name' : 'info.picocli.spring'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,7 +89,7 @@ publishing {
|
||||
root.appendNode('name', bintrayPackage)
|
||||
root.appendNode('description', description)
|
||||
root.appendNode('url', 'http://picocli.info')
|
||||
root.appendNode('inceptionYear', '2018')
|
||||
root.appendNode('inceptionYear', '2019')
|
||||
root.children().last() + pomConfig
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
package picocli.spring;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import picocli.CommandLine;
|
||||
|
||||
/**
|
||||
* @author Thibaud Leprêtre
|
||||
*/
|
||||
public class PicocliSpringFactory implements CommandLine.IFactory {
|
||||
private static final Logger logger = LoggerFactory.getLogger(PicocliSpringFactory.class);
|
||||
|
||||
private final ApplicationContext applicationContext;
|
||||
|
||||
public PicocliSpringFactory(ApplicationContext applicationContext) {
|
||||
this.applicationContext = applicationContext;
|
||||
}
|
||||
|
||||
@Override
|
||||
public <K> K create(Class<K> clazz) throws Exception {
|
||||
try {
|
||||
return getBeanOrCreate(clazz);
|
||||
} catch (Exception e) {
|
||||
logger.warn("Unable to get bean of class {}, using default Picocli factory", clazz);
|
||||
return CommandLine.defaultFactory().create(clazz);
|
||||
}
|
||||
}
|
||||
|
||||
private <K> K getBeanOrCreate(Class<K> clazz) {
|
||||
try {
|
||||
return applicationContext.getBean(clazz);
|
||||
} catch (Exception e) {
|
||||
return applicationContext.getAutowireCapableBeanFactory().createBean(clazz);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package picocli.spring.boot.autoconfigure;
|
||||
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Primary;
|
||||
import picocli.CommandLine;
|
||||
import picocli.CommandLine.IFactory;
|
||||
import picocli.spring.PicocliSpringFactory;
|
||||
|
||||
/**
|
||||
* @author Thibaud Leprêtre
|
||||
*/
|
||||
@Configuration
|
||||
@ConditionalOnClass(CommandLine.class)
|
||||
public class PicocliAutoConfiguration {
|
||||
|
||||
@Primary
|
||||
@Bean
|
||||
@ConditionalOnMissingBean(IFactory.class)
|
||||
public IFactory picocliSpringFactory(ApplicationContext applicationContext) {
|
||||
return new PicocliSpringFactory(applicationContext);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean(PicocliSpringFactory.class)
|
||||
public PicocliSpringFactory picocliSpringFactoryImpl(ApplicationContext applicationContext) {
|
||||
return new PicocliSpringFactory(applicationContext);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
org.springframework.boot.autoconfigure.EnableAutoConfiguration=picocli.spring.boot.autoconfigure.PicocliAutoConfiguration
|
||||
@@ -0,0 +1,86 @@
|
||||
package picocli.spring;
|
||||
|
||||
import org.junit.After;
|
||||
import org.junit.Test;
|
||||
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
|
||||
import picocli.CommandLine;
|
||||
import picocli.CommandLine.IFactory;
|
||||
import picocli.CommandLine.ParseResult;
|
||||
import picocli.spring.boot.autoconfigure.sample.MyCommand;
|
||||
import picocli.spring.boot.autoconfigure.sample.MySpringApp;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
public class PicocliSpringFactoryTest {
|
||||
|
||||
private AnnotationConfigApplicationContext context;
|
||||
|
||||
@After
|
||||
public void tearDown() {
|
||||
if (this.context != null) {
|
||||
this.context.close();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void defaultPicocliSpringFactory() {
|
||||
load(MySpringApp.class);
|
||||
IFactory factory = this.context.getBean(IFactory.class);
|
||||
assertNotNull(factory);
|
||||
assertTrue(factory instanceof PicocliSpringFactory);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testParseTopLevelCommand() {
|
||||
load(MySpringApp.class);
|
||||
IFactory factory = this.context.getBean(IFactory.class);
|
||||
MyCommand userObject = this.context.getBean(MyCommand.class);
|
||||
CommandLine cmd = new CommandLine(userObject, factory);
|
||||
cmd.parseArgs("-x", "abc", "xyz");
|
||||
assertEquals("abc", userObject.x);
|
||||
assertEquals(Arrays.asList("xyz"), userObject.positionals);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testParseSubCommand() {
|
||||
load(MySpringApp.class);
|
||||
IFactory factory = this.context.getBean(IFactory.class);
|
||||
MyCommand userObject = this.context.getBean(MyCommand.class);
|
||||
CommandLine cmd = new CommandLine(userObject, factory);
|
||||
ParseResult parseResult = cmd.parseArgs("sub", "-y", "abc", "xyz");
|
||||
assertNull(userObject.x);
|
||||
assertNull(userObject.positionals);
|
||||
|
||||
assertTrue(parseResult.hasSubcommand());
|
||||
MyCommand.Sub sub = (MyCommand.Sub) parseResult.subcommand().commandSpec().userObject();
|
||||
assertEquals("abc", sub.y);
|
||||
assertEquals(Arrays.asList("xyz"), sub.positionals);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testParseSubSubCommand() {
|
||||
load(MySpringApp.class);
|
||||
IFactory factory = this.context.getBean(IFactory.class);
|
||||
MyCommand userObject = this.context.getBean(MyCommand.class);
|
||||
CommandLine cmd = new CommandLine(userObject, factory);
|
||||
ParseResult parseResult = cmd.parseArgs("sub", "subsub", "-z", "abc");
|
||||
assertNull(userObject.x);
|
||||
assertNull(userObject.positionals);
|
||||
|
||||
assertTrue(parseResult.hasSubcommand());
|
||||
assertTrue(parseResult.subcommand().hasSubcommand());
|
||||
MyCommand.SubSub subsub = (MyCommand.SubSub) parseResult.subcommand().subcommand().commandSpec().userObject();
|
||||
assertEquals("abc", subsub.z);
|
||||
assertEquals("something", subsub.service.service());
|
||||
}
|
||||
|
||||
private void load(Class<?> config, String... environment) {
|
||||
AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext();
|
||||
//EnvironmentTestUtils.addEnvironment(applicationContext, environment);
|
||||
applicationContext.register(config);
|
||||
applicationContext.refresh();
|
||||
this.context = applicationContext;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
package picocli.spring.boot.autoconfigure;
|
||||
|
||||
import org.junit.After;
|
||||
import org.junit.Test;
|
||||
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.stereotype.Component;
|
||||
import picocli.CommandLine;
|
||||
import picocli.CommandLine.Command;
|
||||
import picocli.CommandLine.IFactory;
|
||||
import picocli.spring.PicocliSpringFactory;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
public class PicocliAutoConfigurationTest {
|
||||
|
||||
private AnnotationConfigApplicationContext context;
|
||||
|
||||
@After
|
||||
public void tearDown() {
|
||||
if (this.context != null) {
|
||||
this.context.close();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void defaultPicocliSpringFactory() {
|
||||
load(EmptyConfiguration.class);
|
||||
IFactory factory = this.context.getBean(PicocliSpringFactory.class);
|
||||
assertNotNull(factory);
|
||||
assertTrue(factory instanceof PicocliSpringFactory);
|
||||
CommandLine cmd = new CommandLine(new MyCommand(), factory);
|
||||
cmd.parseArgs();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void defaultFactory() {
|
||||
load(EmptyConfiguration.class);
|
||||
IFactory factory = this.context.getBean(IFactory.class);
|
||||
assertNotNull(factory);
|
||||
assertTrue(factory instanceof PicocliSpringFactory);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testEnableAutoConfigurationRequired() {
|
||||
load(EmptyNoAutoConfiguration.class);
|
||||
try {
|
||||
this.context.getBean(IFactory.class);
|
||||
fail("Expected exception");
|
||||
} catch (NoSuchBeanDefinitionException ok) {
|
||||
assertEquals("No qualifying bean of type 'picocli.CommandLine$IFactory' available", ok.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testEnableAutoConfigurationImplRequired() {
|
||||
load(EmptyNoAutoConfiguration.class);
|
||||
try {
|
||||
this.context.getBean(PicocliSpringFactory.class);
|
||||
fail("Expected exception");
|
||||
} catch (NoSuchBeanDefinitionException ok) {
|
||||
assertEquals("No qualifying bean of type 'picocli.spring.PicocliSpringFactory' available", ok.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void configuredFactory() {
|
||||
load(CommandIFactoryConfiguration.class);
|
||||
IFactory factory = this.context.getBean(IFactory.class);
|
||||
assertNotNull(factory);
|
||||
assertTrue(factory instanceof PicocliSpringFactory);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void configuredFactoryImpl() {
|
||||
load(CommandPicocliSpringFactoryConfiguration.class);
|
||||
IFactory factory = this.context.getBean(IFactory.class);
|
||||
assertNotNull(factory);
|
||||
assertTrue(factory instanceof PicocliSpringFactory);
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableAutoConfiguration
|
||||
static class EmptyConfiguration {}
|
||||
|
||||
@Configuration
|
||||
static class EmptyNoAutoConfiguration {}
|
||||
|
||||
@Configuration
|
||||
@EnableAutoConfiguration
|
||||
static class CommandIFactoryConfiguration {
|
||||
|
||||
@Autowired
|
||||
IFactory factory;
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableAutoConfiguration
|
||||
static class CommandPicocliSpringFactoryConfiguration {
|
||||
|
||||
@Autowired
|
||||
PicocliSpringFactory factory;
|
||||
}
|
||||
|
||||
@Component
|
||||
@Command
|
||||
static class MyCommand {}
|
||||
|
||||
private void load(Class<?> config, String... environment) {
|
||||
AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext();
|
||||
//EnvironmentTestUtils.addEnvironment(applicationContext, environment);
|
||||
applicationContext.register(config);
|
||||
applicationContext.refresh();
|
||||
this.context = applicationContext;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package picocli.spring.boot.autoconfigure.sample;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
import picocli.CommandLine.Command;
|
||||
import picocli.CommandLine.Option;
|
||||
import picocli.CommandLine.Parameters;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.concurrent.Callable;
|
||||
|
||||
// NOTE: inner classes and fields are public for testing
|
||||
|
||||
@Component
|
||||
@Command(name = "mycommand", mixinStandardHelpOptions = true, subcommands = MyCommand.Sub.class)
|
||||
public class MyCommand implements Callable<Integer> {
|
||||
@Option(names = "-x", description = "optional option")
|
||||
public String x;
|
||||
|
||||
@Parameters(description = "positional params")
|
||||
public List<String> positionals;
|
||||
|
||||
@Override
|
||||
public Integer call() {
|
||||
System.out.printf("mycommand was called with -x=%s and positionals: %s%n", x, positionals);
|
||||
return 23;
|
||||
}
|
||||
|
||||
@Component
|
||||
@Command(name = "sub", mixinStandardHelpOptions = true, subcommands = MyCommand.SubSub.class,
|
||||
exitCodeOnExecutionException = 34)
|
||||
public static class Sub implements Callable<Integer> {
|
||||
@Option(names = "-y", description = "optional option")
|
||||
public String y;
|
||||
|
||||
@Parameters(description = "positional params")
|
||||
public List<String> positionals;
|
||||
|
||||
@Override
|
||||
public Integer call() {
|
||||
System.out.printf("mycommand sub was called with -y=%s and positionals: %s%n", y, positionals);
|
||||
throw new RuntimeException("mycommand sub failing on purpose");
|
||||
//return 33;
|
||||
}
|
||||
}
|
||||
|
||||
@Component
|
||||
@Command(name = "subsub", mixinStandardHelpOptions = true,
|
||||
exitCodeOnExecutionException = 44)
|
||||
public static class SubSub implements Callable<Integer> {
|
||||
@Option(names = "-z", description = "optional option")
|
||||
public String z;
|
||||
|
||||
@Autowired
|
||||
public SomeService service;
|
||||
|
||||
@Override
|
||||
public Integer call() {
|
||||
System.out.printf("mycommand sub subsub was called with -z=%s. Service says: '%s'%n", z, service.service());
|
||||
throw new RuntimeException("mycommand sub subsub failing on purpose");
|
||||
//return 43;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package picocli.spring.boot.autoconfigure.sample;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.CommandLineRunner;
|
||||
import org.springframework.boot.ExitCodeGenerator;
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.ComponentScan;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import picocli.CommandLine;
|
||||
import picocli.CommandLine.IFactory;
|
||||
|
||||
@Configuration
|
||||
@ComponentScan
|
||||
@EnableAutoConfiguration
|
||||
public class MySpringApp implements CommandLineRunner, ExitCodeGenerator {
|
||||
private int exitCode;
|
||||
|
||||
@Autowired
|
||||
IFactory factory;
|
||||
|
||||
@Autowired
|
||||
MyCommand myCommand;
|
||||
|
||||
@Bean
|
||||
ServiceDependency dependency() {
|
||||
return new ServiceDependency();
|
||||
}
|
||||
|
||||
@Bean
|
||||
SomeService someService(ServiceDependency dependency) {
|
||||
return new SomeService(dependency);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run(String... args) {
|
||||
exitCode = new CommandLine(myCommand, factory).execute(args);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getExitCode() {
|
||||
return exitCode;
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
System.exit(SpringApplication.exit(SpringApplication.run(MySpringApp.class, args)));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package picocli.spring.boot.autoconfigure.sample;
|
||||
|
||||
public class ServiceDependency {
|
||||
public String provideSomething() {
|
||||
return "something";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package picocli.spring.boot.autoconfigure.sample;
|
||||
|
||||
public class SomeService {
|
||||
private final ServiceDependency dependency;
|
||||
|
||||
public SomeService(ServiceDependency dependency) {
|
||||
this.dependency = dependency;
|
||||
}
|
||||
|
||||
public String service() {
|
||||
return dependency.provideSomething();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user