Remove dependency to Spring Shell

This commit updates the samples to use Java's
standard CLI utilities instead of Spring Shell.

Resolves #1184
This commit is contained in:
Mahmoud Ben Hassine
2025-04-15 14:08:19 +02:00
parent 5170c0e2d2
commit 1a017dced2
71 changed files with 733 additions and 2024 deletions

View File

@@ -15,18 +15,21 @@
*/
package demo;
import java.util.Iterator;
import java.util.Map.Entry;
import java.util.Set;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.shell.command.annotation.Command;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.statemachine.StateMachine;
import org.springframework.statemachine.state.State;
import org.springframework.util.StringUtils;
@Command
import java.io.InputStream;
import java.util.Iterator;
import java.util.Map.Entry;
import java.util.Scanner;
import java.util.Set;
@Configuration
public class AbstractStateMachineCommands<S, E> {
@Autowired
@@ -36,54 +39,74 @@ public class AbstractStateMachineCommands<S, E> {
return stateMachine;
}
@Autowired
@Qualifier("stateChartModel")
private String stateChartModel;
@Command(command = "sm state", description = "Prints current state")
public String state() {
State<S, E> state = stateMachine.getState();
if (state != null) {
return StringUtils.collectionToCommaDelimitedString(state.getIds());
} else {
return "No state";
}
}
@Command(command = "sm start", description = "Start a state machine")
public String start() {
stateMachine.startReactively().subscribe();
return "State machine started";
}
@Command(command = "sm stop", description = "Stop a state machine")
public String stop() {
stateMachine.stopReactively().subscribe();
return "State machine stopped";
}
@Command(command = "sm print", description = "Print state machine")
public String print() {
return stateChartModel;
}
@Command(command = "sm variables", description = "Prints extended state variables")
public String variables() {
StringBuilder buf = new StringBuilder();
Set<Entry<Object, Object>> entrySet = stateMachine.getExtendedState().getVariables().entrySet();
Iterator<Entry<Object, Object>> iterator = entrySet.iterator();
if (entrySet.size() > 0) {
while (iterator.hasNext()) {
Entry<Object, Object> e = iterator.next();
buf.append(e.getKey() + "=" + e.getValue());
if (iterator.hasNext()) {
buf.append("\n");
@Bean
public Command state() {
return new BasicCommand("state", "Prints current state") {
public String execute(String[] args) {
State<S, E> state = stateMachine.getState();
if (state != null) {
return StringUtils.collectionToCommaDelimitedString(state.getIds());
} else {
return "No state";
}
}
} else {
buf.append("No variables");
}
return buf.toString();
};
}
}
@Bean
public Command start() {
return new BasicCommand("start", "Start a state machine") {
public String execute(String[] args) {
stateMachine.startReactively().subscribe();
return "State machine started";
}
};
}
@Bean
public Command stop() {
return new BasicCommand("stop", "Stop a state machine") {
public String execute(String[] args) {
stateMachine.stopReactively().subscribe();
return "State machine stopped";
}
};
}
@Bean
public Command print() {
return new BasicCommand("print", "Print state machine") {
public String execute(String[] args) throws Exception {
ClassPathResource model = new ClassPathResource("statechartmodel.txt");
InputStream inputStream = model.getInputStream();
Scanner scanner = new Scanner(inputStream);
String content = scanner.useDelimiter("\\Z").next();
scanner.close();
return content;
}
};
}
@Bean
public Command variables() {
return new BasicCommand("variables", "Prints extended state variables") {
public String execute(String[] args) {
StringBuilder buf = new StringBuilder();
Set<Entry<Object, Object>> entrySet = stateMachine.getExtendedState().getVariables().entrySet();
Iterator<Entry<Object, Object>> iterator = entrySet.iterator();
if (entrySet.size() > 0) {
while (iterator.hasNext()) {
Entry<Object, Object> e = iterator.next();
buf.append(e.getKey() + "=" + e.getValue());
if (iterator.hasNext()) {
buf.append("\n");
}
}
} else {
buf.append("No variables");
}
return buf.toString();
}
};
}
}

View File

@@ -0,0 +1,55 @@
/*
* Copyright 2015-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package demo;
/**
* Basic implementation of the {@link Command} interface.
* This class provides a simple way to create commands with a name and description.
* It can be extended to implement specific command logic.
*
* @author Mahmoud Ben Hassine
*/
public class BasicCommand implements Command {
private final String name;
private final String description;
/**
* Create a new {@link BasicCommand} with a name and description.
*
* @param name the command name
* @param description the command description
*/
public BasicCommand(String name, String description) {
this.name = name;
this.description = description;
}
@Override
public String getName() {
return this.name;
}
@Override
public String getDescription() {
return this.description;
}
@Override
public String execute(String[] args) throws Exception {
throw new UnsupportedOperationException("Not implemented yet");
}
}

View File

@@ -15,19 +15,31 @@
*/
package demo;
import org.jline.utils.AttributedString;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.shell.jline.PromptProvider;
import org.springframework.stereotype.Component;
/**
* Simple interface for command line commands.
*
* @author Mahmoud Ben Hassine
*/
public interface Command {
@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class StateMachinePromptProvider implements PromptProvider {
/**
* The command name.
* @return the command name
*/
String getName();
@Override
public AttributedString getPrompt() {
return AttributedString.fromAnsi("sm>");
}
/**
* The command description.
* @return the command description
*/
String getDescription();
/**
* Execute the command.
* @param args the command arguments
* @return the message to be printed to the console
* @throws Exception in case of error
*/
String execute(String[] args) throws Exception;
}

View File

@@ -0,0 +1,96 @@
/*
* Copyright 2015-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package demo;
import org.springframework.beans.BeansException;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.core.Ordered;
import org.springframework.stereotype.Component;
import java.io.Console;
import java.util.Map;
import java.util.stream.Stream;
/**
* Command line runner that executes commands.
* <p>
* This class implements the {@link ApplicationRunner} interface and is responsible for
* executing commands in the command line interface. It retrieves all beans of type {@link Command}
* from the application context and allows the user to execute them interactively.
*
* @author Mahmoud Ben Hassine
*/
@Component
public class CommandRunner implements ApplicationRunner, ApplicationContextAware, Ordered {
private ApplicationContext applicationContext;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
@Override
public int getOrder() {
return Ordered.LOWEST_PRECEDENCE;
}
@Override
public void run(ApplicationArguments args) throws Exception {
Map<String, Command> commands = this.applicationContext.getBeansOfType(Command.class);
Console console = System.console();
if (console == null) {
System.err.println("No console available.");
return;
}
final String lineSeparator = System.lineSeparator();
console.printf("Available commands:" + lineSeparator);
for (Command command : commands.values()) {
console.printf(" " + command.getName() + ": " + command.getDescription() + lineSeparator);
}
console.printf(" quit: exit" + lineSeparator);
String commandString = "";
while (!commandString.equalsIgnoreCase("quit")) {
console.printf("sm>");
commandString = console.readLine();
String[] tokens = commandString.split(" ");
String commandName = tokens[0];
String[] commandArgs = Stream.of(tokens).skip(1).limit(tokens.length).toArray(String[]::new);
if (commandName.equalsIgnoreCase("quit")) {
continue;
}
try {
Command command = commands.get(commandName);
if (command == null) {
console.printf("Command not found: " + commandString + lineSeparator);
continue;
}
console.printf(command.execute(commandArgs));
console.printf(lineSeparator);
} catch (Exception exception) {
console.printf("Error while executing command: " + commandString + lineSeparator);
exception.printStackTrace();
}
}
console.printf("bye!" + lineSeparator);
}
}

View File

@@ -1,79 +0,0 @@
/*
* Copyright 2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package demo;
import java.io.IOException;
import java.io.InputStream;
import java.util.Scanner;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.context.ApplicationListener;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.statemachine.event.OnStateEntryEvent;
import org.springframework.statemachine.event.OnStateExitEvent;
import org.springframework.statemachine.event.OnTransitionEvent;
import org.springframework.statemachine.event.StateMachineEvent;
import org.springframework.statemachine.transition.TransitionKind;
@Configuration
public class CommonConfiguration {
private final static Log log = LogFactory.getLog(CommonConfiguration.class);
@Configuration
static class ApplicationConfig {
@Bean
public TestEventListener testEventListener() {
return new TestEventListener();
}
@Bean
public String stateChartModel() throws IOException {
ClassPathResource model = new ClassPathResource("statechartmodel.txt");
InputStream inputStream = model.getInputStream();
Scanner scanner = new Scanner(inputStream);
String content = scanner.useDelimiter("\\Z").next();
scanner.close();
return content;
}
}
static class TestEventListener implements ApplicationListener<StateMachineEvent> {
@Override
public void onApplicationEvent(StateMachineEvent event) {
if (event instanceof OnStateEntryEvent) {
OnStateEntryEvent e = (OnStateEntryEvent)event;
log.info("Entry state " + e.getState().getId());
} else if (event instanceof OnStateExitEvent) {
OnStateExitEvent e = (OnStateExitEvent)event;
log.info("Exit state " + e.getState().getId());
} else if (event instanceof OnTransitionEvent) {
OnTransitionEvent e = (OnTransitionEvent)event;
if (e.getTransition().getKind() == TransitionKind.INTERNAL) {
log.info("Internal transition source=" + e.getTransition().getSource().getId());
}
}
}
}
}

View File

@@ -0,0 +1,31 @@
package demo;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.context.ApplicationListener;
import org.springframework.statemachine.event.OnStateEntryEvent;
import org.springframework.statemachine.event.OnStateExitEvent;
import org.springframework.statemachine.event.OnTransitionEvent;
import org.springframework.statemachine.event.StateMachineEvent;
import org.springframework.statemachine.transition.TransitionKind;
import org.springframework.stereotype.Component;
@Component
class StateMachineEventListener implements ApplicationListener<StateMachineEvent> {
private final static Log log = LogFactory.getLog(StateMachineEventListener.class);
@Override
public void onApplicationEvent(StateMachineEvent stateMachineEvent) {
if (stateMachineEvent instanceof OnStateEntryEvent event) {
log.info("Entry state " + event.getState().getId());
} else if (stateMachineEvent instanceof OnStateExitEvent event) {
log.info("Exit state " + event.getState().getId());
} else if (stateMachineEvent instanceof OnTransitionEvent event) {
if (event.getTransition().getKind() == TransitionKind.INTERNAL) {
log.info("Internal transition source=" + event.getTransition().getSource().getId());
}
}
}
}

View File

@@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">
<context:component-scan base-package="demo" />
</beans>