FeedReaderCommandRunner.java
package net.sasasin.sreader.runner;
import java.util.Arrays;
import java.util.List;
import net.sasasin.sreader.cli.PicocliFactory;
import net.sasasin.sreader.cli.SreaderCommand;
import net.sasasin.sreader.config.FeedReaderProperties;
import net.sasasin.sreader.scheduler.FeedReaderScheduler;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.stereotype.Component;
import picocli.CommandLine;
@Component
public class FeedReaderCommandRunner implements CommandLineRunner {
private static final List<String> SPRING_PROP_PREFIXES =
List.of("--spring.", "--sreader.", "--server.", "--management.");
private final FeedReaderProperties properties;
private final FeedReaderScheduler scheduler;
private final ConfigurableApplicationContext applicationContext;
private final SreaderCommand sreaderCommand;
private final PicocliFactory picocliFactory;
public FeedReaderCommandRunner(
FeedReaderProperties properties,
FeedReaderScheduler scheduler,
ConfigurableApplicationContext applicationContext,
SreaderCommand sreaderCommand,
PicocliFactory picocliFactory) {
this.properties = properties;
this.scheduler = scheduler;
this.applicationContext = applicationContext;
this.sreaderCommand = sreaderCommand;
this.picocliFactory = picocliFactory;
}
@Override
public void run(String... args) throws Exception {
String[] filtered = filterSpringBootOptions(args);
if (isExplicitCliInvocation(filtered)) {
CommandLine cli = new CommandLine(sreaderCommand, picocliFactory);
// picocli default: 2 for usage errors (bad option / missing required).
// Map execution exceptions (runtime errors inside commands) to 1.
cli.setExitCodeExceptionMapper(ex -> ex instanceof CommandLine.ParameterException ? 2 : 1);
int exitCode = cli.execute(filtered);
exitApplication(exitCode);
return;
}
if (properties.job().runOnce()) {
scheduler.runIfIdle();
exitApplication(0);
}
// otherwise fall through: application keeps running as daemon
// (scheduler will trigger periodic jobs per sreader.scheduler.* config)
}
/**
* Remove Spring Boot / sreader property arguments before passing to picocli. Supports usage like:
* java -jar app.jar --sreader.scheduler.enabled=false feeds import --input feeds.toml
*/
private String[] filterSpringBootOptions(String[] args) {
return Arrays.stream(args)
.filter(a -> SPRING_PROP_PREFIXES.stream().noneMatch(a::startsWith))
.toArray(String[]::new);
}
/**
* Returns true when the (filtered) args indicate an explicit CLI subcommand or help request.
* Non-matching cases fall back to the run-once property / normal daemon startup behavior.
*/
private boolean isExplicitCliInvocation(String[] args) {
if (args.length == 0) {
return false;
}
for (String a : args) {
if ("feeds".equals(a)
|| "run-once".equals(a)
|| "probe".equals(a)
|| "--help".equals(a)
|| "-h".equals(a)
|| "-?".equals(a)) {
return true;
}
}
return false;
}
private void exitApplication(int exitCode) {
int resolvedExitCode = SpringApplication.exit(applicationContext, () -> exitCode);
if (resolvedExitCode != 0) {
exitProcess(Math.max(exitCode, resolvedExitCode));
}
}
/** Invokes the JVM exit operation after Spring has closed the application context. */
void exitProcess(int exitCode) {
System.exit(exitCode);
}
}