A
This commit is contained in:
@@ -18,6 +18,7 @@ include ':state_machines:inheritance_sample'
|
||||
include ':state_machines:layered_dispatcher_sample'
|
||||
include ':state_machines:ultimate_ecosystem_sm'
|
||||
include ':state_machines:enterprise_order_system'
|
||||
include ':state_machines:jms_abstract_template_sample'
|
||||
include ':state_machines:multi_module_sample:api-module'
|
||||
include ':state_machines:multi_module_sample:core-module'
|
||||
include ':state_machines:state_machine_enterprise'
|
||||
|
||||
@@ -90,9 +90,7 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
||||
List<CallChain> chains = new ArrayList<>();
|
||||
|
||||
for (EntryPoint ep : entryPoints) {
|
||||
String startMethod = ep.getClassName() + "." + ep.getMethodName();
|
||||
List<String> startMethods = new ArrayList<>();
|
||||
startMethods.add(startMethod);
|
||||
List<MessagingStartContext> startContexts = resolveMessagingStartContexts(ep, callGraph);
|
||||
|
||||
List<Map<String, String>> bindingVariants =
|
||||
EntryPointBindingExpander.expandEntryPointBindings(ep, context, callGraph);
|
||||
@@ -109,15 +107,23 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
||||
for (TriggerPoint tp : triggers) {
|
||||
String targetMethod = tp.getClassName() + "." + tp.getMethodName();
|
||||
List<List<String>> allPaths = new ArrayList<>();
|
||||
for (String sMethod : startMethods) {
|
||||
for (MessagingStartContext startContext : startContexts) {
|
||||
if (initialBindings.isEmpty()) {
|
||||
allPaths.addAll(pathFinder.findAllPaths(
|
||||
sMethod, targetMethod, callGraph, new HashSet<>(),
|
||||
ep.getClassName()));
|
||||
startContext.startMethod(),
|
||||
targetMethod,
|
||||
callGraph,
|
||||
new HashSet<>(),
|
||||
startContext.concreteSelfFqn()));
|
||||
} else {
|
||||
allPaths.addAll(pathFinder.findAllPaths(
|
||||
sMethod, targetMethod, callGraph, new HashSet<>(),
|
||||
pathBindingEvaluator, initialBindings, ep.getClassName()));
|
||||
startContext.startMethod(),
|
||||
targetMethod,
|
||||
callGraph,
|
||||
new HashSet<>(),
|
||||
pathBindingEvaluator,
|
||||
initialBindings,
|
||||
startContext.concreteSelfFqn()));
|
||||
}
|
||||
}
|
||||
Set<List<String>> uniquePaths = new LinkedHashSet<>(allPaths);
|
||||
@@ -146,13 +152,67 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
||||
}
|
||||
if (!foundAny && log.isDebugEnabled()) {
|
||||
log.debug("Entry point {} ({}) did not reach any trigger points. Graph nodes available: {}",
|
||||
chainEntryPoint.getName(), startMethod, callGraph.keySet());
|
||||
chainEntryPoint.getName(),
|
||||
startContexts.stream().map(MessagingStartContext::startMethod).toList(),
|
||||
callGraph.keySet());
|
||||
}
|
||||
}
|
||||
}
|
||||
return chains;
|
||||
}
|
||||
|
||||
/**
|
||||
* Messaging entries annotated on abstract/interface types must be searched once per concrete
|
||||
* runtime listener so template-method pruning has a concrete {@code concreteSelf}.
|
||||
* REST/CUSTOM entries keep a single start at the declaring type.
|
||||
*/
|
||||
private List<MessagingStartContext> resolveMessagingStartContexts(
|
||||
EntryPoint ep,
|
||||
Map<String, List<CallEdge>> callGraph) {
|
||||
String declaringClass = ep.getClassName();
|
||||
String methodName = ep.getMethodName();
|
||||
String declaringStart = declaringClass + "." + methodName;
|
||||
|
||||
if (!isMessagingEntryType(ep.getType()) || declaringClass == null || methodName == null) {
|
||||
return List.of(new MessagingStartContext(declaringStart, declaringClass));
|
||||
}
|
||||
|
||||
if (context.isConcreteClassType(declaringClass)) {
|
||||
return List.of(new MessagingStartContext(declaringStart, declaringClass));
|
||||
}
|
||||
|
||||
List<String> impls = context.getImplementations(declaringClass);
|
||||
if (impls == null || impls.isEmpty()) {
|
||||
return List.of(new MessagingStartContext(declaringStart, declaringClass));
|
||||
}
|
||||
|
||||
LinkedHashSet<MessagingStartContext> contexts = new LinkedHashSet<>();
|
||||
for (String implFqn : impls) {
|
||||
if (!context.isConcreteClassType(implFqn)) {
|
||||
continue;
|
||||
}
|
||||
String implStart = implFqn + "." + methodName;
|
||||
String startMethod = callGraph.containsKey(implStart) ? implStart : declaringStart;
|
||||
contexts.add(new MessagingStartContext(startMethod, implFqn));
|
||||
}
|
||||
|
||||
if (contexts.isEmpty()) {
|
||||
return List.of(new MessagingStartContext(declaringStart, declaringClass));
|
||||
}
|
||||
return List.copyOf(contexts);
|
||||
}
|
||||
|
||||
private static boolean isMessagingEntryType(EntryPoint.Type type) {
|
||||
return type == EntryPoint.Type.JMS
|
||||
|| type == EntryPoint.Type.RABBIT
|
||||
|| type == EntryPoint.Type.KAFKA
|
||||
|| type == EntryPoint.Type.SQS
|
||||
|| type == EntryPoint.Type.SNS;
|
||||
}
|
||||
|
||||
private record MessagingStartContext(String startMethod, String concreteSelfFqn) {
|
||||
}
|
||||
|
||||
protected TriggerPoint resolveTriggerPointParameters(TriggerPoint tp, List<String> path, Map<String, List<CallEdge>> callGraph) {
|
||||
return resolveTriggerPointParameters(tp, path, callGraph, Map.of());
|
||||
}
|
||||
@@ -1399,6 +1459,14 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* When a unique Spring bean can be proven for the receiver, return that concrete type FQN.
|
||||
* Default: none (Heuristic engine). {@link JdtCallGraphEngine} overrides via injection analysis.
|
||||
*/
|
||||
protected String resolveInjectedReceiverTypeFqn(Expression receiver) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* For CHA-expanded targets {@code Impl.method}, prefer the concrete impl class as the edge's
|
||||
* receiver type so pathfinding can prune sibling overrides inside that collaborator hierarchy.
|
||||
@@ -2596,6 +2664,11 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
||||
String receiver = getReceiverString(node.getExpression());
|
||||
String declaredReceiverType =
|
||||
resolveReceiverTypeFqn(node.getExpression(), td);
|
||||
String injectedReceiverType =
|
||||
resolveInjectedReceiverTypeFqn(node.getExpression());
|
||||
if (injectedReceiverType != null) {
|
||||
declaredReceiverType = injectedReceiverType;
|
||||
}
|
||||
for (String calledMethod : calledMethods) {
|
||||
String receiverTypeFqn =
|
||||
refineReceiverTypeForTarget(declaredReceiverType, calledMethod);
|
||||
|
||||
@@ -139,6 +139,23 @@ public class JdtCallGraphEngine extends AbstractCallGraphEngine {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String resolveInjectedReceiverTypeFqn(Expression receiver) {
|
||||
if (injectionAnalyzer == null || receiver == null) {
|
||||
return null;
|
||||
}
|
||||
IBinding nameBinding = null;
|
||||
if (receiver instanceof SimpleName sn) {
|
||||
nameBinding = sn.resolveBinding();
|
||||
} else if (receiver instanceof FieldAccess fa) {
|
||||
nameBinding = fa.resolveFieldBinding();
|
||||
}
|
||||
if (!(nameBinding instanceof IVariableBinding varBinding)) {
|
||||
return null;
|
||||
}
|
||||
return injectionAnalyzer.resolveInjectedBeanFqn(varBinding);
|
||||
}
|
||||
|
||||
private Expression unwrapMessageBuilder(Expression expr) {
|
||||
Expression peeled = ReactiveExpressionSupport.peelFactoryPayloadExpression(expr);
|
||||
return peeled != null ? peeled : expr;
|
||||
|
||||
@@ -0,0 +1,179 @@
|
||||
package click.kamil.springstatemachineexporter.analysis.service;
|
||||
|
||||
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
|
||||
import click.kamil.springstatemachineexporter.analysis.model.EntryPoint;
|
||||
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
|
||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* When {@code @JmsListener} is declared on an abstract base / interface, findChains must expand
|
||||
* to concrete implementations with per-impl {@code concreteSelf} so template hooks reach
|
||||
* {@code sendEvent} without sibling cross-wiring.
|
||||
*/
|
||||
class JmsAbstractEntryExpansionTest {
|
||||
|
||||
@Test
|
||||
void abstractJmsListenerExpandsToConcreteSubclassChains(@TempDir Path tempDir) throws Exception {
|
||||
Files.writeString(tempDir.resolve("Listeners.java"), """
|
||||
package com.example;
|
||||
|
||||
@interface JmsListener {
|
||||
String destination() default "";
|
||||
}
|
||||
|
||||
abstract class AbstractAA {
|
||||
@JmsListener(destination = "orders")
|
||||
void onMessage(String m) {
|
||||
processMessage(m);
|
||||
}
|
||||
void processMessage(String m) {
|
||||
doPMessage(m);
|
||||
}
|
||||
abstract void doPMessage(String m);
|
||||
}
|
||||
|
||||
class OrderAA extends AbstractAA {
|
||||
private final StateMachine sm = new StateMachine();
|
||||
@Override
|
||||
void doPMessage(String m) {
|
||||
sm.sendEvent(OrderEvent.PAY);
|
||||
}
|
||||
}
|
||||
|
||||
class DocumentAA extends AbstractAA {
|
||||
private final StateMachine sm = new StateMachine();
|
||||
@Override
|
||||
void doPMessage(String m) {
|
||||
sm.sendEvent(DocumentEvent.SUBMIT);
|
||||
}
|
||||
}
|
||||
|
||||
class StateMachine {
|
||||
void sendEvent(Object event) {}
|
||||
}
|
||||
enum OrderEvent { PAY }
|
||||
enum DocumentEvent { SUBMIT }
|
||||
""");
|
||||
|
||||
CodebaseContext context = scan(tempDir);
|
||||
List<EntryPoint> entryPoints = detectJms(context);
|
||||
assertThat(entryPoints)
|
||||
.anySatisfy(ep -> assertThat(ep.getClassName()).isEqualTo("com.example.AbstractAA"));
|
||||
|
||||
HeuristicCallGraphEngine engine = new HeuristicCallGraphEngine(context);
|
||||
List<CallChain> chains = engine.findChains(
|
||||
entryPoints.stream().filter(ep -> "onMessage".equals(ep.getMethodName())).toList(),
|
||||
List.of(TriggerPoint.builder()
|
||||
.className("com.example.StateMachine")
|
||||
.methodName("sendEvent")
|
||||
.event("event")
|
||||
.build()));
|
||||
|
||||
assertThat(chains)
|
||||
.as("abstract JMS EP must expand to concrete subclass paths")
|
||||
.isNotEmpty();
|
||||
assertThat(chains)
|
||||
.anyMatch(chain -> chain.getMethodChain().stream()
|
||||
.anyMatch(hop -> hop.contains("OrderAA.doPMessage")));
|
||||
assertThat(chains)
|
||||
.anyMatch(chain -> chain.getMethodChain().stream()
|
||||
.anyMatch(hop -> hop.contains("DocumentAA.doPMessage")));
|
||||
assertThat(chains)
|
||||
.filteredOn(chain -> chain.getMethodChain().stream()
|
||||
.anyMatch(hop -> hop.contains("OrderAA.doPMessage")))
|
||||
.allSatisfy(chain -> assertThat(chain.getMethodChain())
|
||||
.noneMatch(hop -> hop.contains("DocumentAA")));
|
||||
assertThat(chains)
|
||||
.filteredOn(chain -> chain.getMethodChain().stream()
|
||||
.anyMatch(hop -> hop.contains("DocumentAA.doPMessage")))
|
||||
.allSatisfy(chain -> assertThat(chain.getMethodChain())
|
||||
.noneMatch(hop -> hop.contains("OrderAA")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void interfaceJmsListenerWithMultipleImplsYieldsIsolatedChains(@TempDir Path tempDir) throws Exception {
|
||||
Files.writeString(tempDir.resolve("Iface.java"), """
|
||||
package com.example;
|
||||
|
||||
@interface JmsListener {
|
||||
String destination() default "";
|
||||
}
|
||||
|
||||
interface OrderCommands {
|
||||
@JmsListener(destination = "orders")
|
||||
void onMessage(String m);
|
||||
}
|
||||
|
||||
class PayListener implements OrderCommands {
|
||||
private final StateMachine sm = new StateMachine();
|
||||
@Override
|
||||
public void onMessage(String m) {
|
||||
sm.sendEvent(OrderEvent.PAY);
|
||||
}
|
||||
}
|
||||
|
||||
class ShipListener implements OrderCommands {
|
||||
private final StateMachine sm = new StateMachine();
|
||||
@Override
|
||||
public void onMessage(String m) {
|
||||
sm.sendEvent(OrderEvent.SHIP);
|
||||
}
|
||||
}
|
||||
|
||||
class StateMachine {
|
||||
void sendEvent(Object event) {}
|
||||
}
|
||||
enum OrderEvent { PAY, SHIP }
|
||||
""");
|
||||
|
||||
CodebaseContext context = scan(tempDir);
|
||||
List<EntryPoint> entryPoints = detectJms(context);
|
||||
assertThat(entryPoints)
|
||||
.anySatisfy(ep -> assertThat(ep.getClassName()).isEqualTo("com.example.OrderCommands"));
|
||||
|
||||
HeuristicCallGraphEngine engine = new HeuristicCallGraphEngine(context);
|
||||
List<CallChain> chains = engine.findChains(
|
||||
entryPoints,
|
||||
List.of(TriggerPoint.builder()
|
||||
.className("com.example.StateMachine")
|
||||
.methodName("sendEvent")
|
||||
.event("event")
|
||||
.build()));
|
||||
|
||||
assertThat(chains).isNotEmpty();
|
||||
assertThat(chains)
|
||||
.anyMatch(chain -> chain.getMethodChain().stream()
|
||||
.anyMatch(hop -> hop.contains("PayListener")));
|
||||
assertThat(chains)
|
||||
.anyMatch(chain -> chain.getMethodChain().stream()
|
||||
.anyMatch(hop -> hop.contains("ShipListener")));
|
||||
assertThat(chains)
|
||||
.filteredOn(chain -> chain.getMethodChain().stream()
|
||||
.anyMatch(hop -> hop.contains("PayListener")))
|
||||
.allSatisfy(chain -> assertThat(chain.getMethodChain())
|
||||
.noneMatch(hop -> hop.contains("ShipListener")));
|
||||
}
|
||||
|
||||
private static List<EntryPoint> detectJms(CodebaseContext context) {
|
||||
MessagingDetector detector = new MessagingDetector(context);
|
||||
return context.getCompilationUnits().stream()
|
||||
.flatMap(cu -> detector.detect(cu).stream())
|
||||
.filter(ep -> ep.getType() == EntryPoint.Type.JMS)
|
||||
.toList();
|
||||
}
|
||||
|
||||
private static CodebaseContext scan(Path tempDir) throws Exception {
|
||||
CodebaseContext context = new CodebaseContext();
|
||||
context.setResolveBindings(true);
|
||||
context.scan(tempDir);
|
||||
return context;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
package click.kamil.springstatemachineexporter.analysis.service;
|
||||
|
||||
import click.kamil.springstatemachineexporter.exporter.JsonExporter;
|
||||
import click.kamil.springstatemachineexporter.service.ExportService;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
import java.util.stream.StreamSupport;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Export-level gate for {@code state_machines/jms_abstract_template_sample}: abstract
|
||||
* {@code @JmsListener} must produce JMS callChains through concrete subclass hooks.
|
||||
*/
|
||||
class JmsAbstractTemplateSampleExportTest {
|
||||
|
||||
@Test
|
||||
void exportShouldProduceJmsCallChainsThroughConcreteSubclassHooks(@TempDir Path tempDir) throws Exception {
|
||||
Path root = findProjectRoot();
|
||||
Path sample = root.resolve("state_machines/jms_abstract_template_sample");
|
||||
assertThat(sample).isDirectory();
|
||||
|
||||
Path outputDir = tempDir.resolve("out");
|
||||
ExportService exportService = new ExportService(List.of(new JsonExporter()));
|
||||
exportService.runExporter(
|
||||
sample,
|
||||
outputDir,
|
||||
List.of("json"),
|
||||
true,
|
||||
List.of(),
|
||||
null,
|
||||
null,
|
||||
click.kamil.springstatemachineexporter.exporter.EnumFormat.fn,
|
||||
click.kamil.springstatemachineexporter.exporter.EnumFormat.fn);
|
||||
|
||||
Path jsonFile = Files.walk(outputDir)
|
||||
.filter(path -> path.getFileName().toString().endsWith(".json"))
|
||||
.findFirst()
|
||||
.orElseThrow();
|
||||
|
||||
JsonNode rootJson = new ObjectMapper().readTree(jsonFile.toFile());
|
||||
List<JsonNode> callChains = StreamSupport.stream(
|
||||
rootJson.path("metadata").path("callChains").spliterator(), false)
|
||||
.toList();
|
||||
|
||||
assertThat(callChains)
|
||||
.as("abstract JMS template sample must export at least one callChain")
|
||||
.isNotEmpty();
|
||||
|
||||
assertThat(callChains)
|
||||
.anySatisfy(chain -> {
|
||||
String entry = chain.path("entryPoint").path("name").asText();
|
||||
assertThat(entry).containsIgnoringCase("JMS");
|
||||
String methods = chain.path("methodChain").toString();
|
||||
assertThat(methods).containsAnyOf("OrderAA", "DocumentAA", "AbstractAA", "sendEvent");
|
||||
});
|
||||
|
||||
assertThat(callChains.stream()
|
||||
.filter(chain -> chain.path("methodChain").toString().contains("OrderAA"))
|
||||
.toList())
|
||||
.allSatisfy(chain -> assertThat(chain.path("methodChain").toString())
|
||||
.doesNotContain("DocumentAA.doPMessage"));
|
||||
}
|
||||
|
||||
private static Path findProjectRoot() {
|
||||
Path current = Path.of(".").toAbsolutePath().normalize();
|
||||
while (current != null && !Files.exists(current.resolve("settings.gradle"))) {
|
||||
current = current.getParent();
|
||||
}
|
||||
if (current == null) {
|
||||
throw new IllegalStateException("Could not find project root");
|
||||
}
|
||||
return current;
|
||||
}
|
||||
}
|
||||
29
state_machines/jms_abstract_template_sample/build.gradle
Normal file
29
state_machines/jms_abstract_template_sample/build.gradle
Normal file
@@ -0,0 +1,29 @@
|
||||
plugins {
|
||||
id 'java'
|
||||
id 'org.springframework.boot' version '3.5.3'
|
||||
id 'io.spring.dependency-management' version '1.1.7'
|
||||
}
|
||||
|
||||
group = 'click.kamil'
|
||||
version = '0.0.1-SNAPSHOT'
|
||||
|
||||
java {
|
||||
toolchain {
|
||||
languageVersion = JavaLanguageVersion.of(21)
|
||||
}
|
||||
}
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation 'org.springframework.boot:spring-boot-starter'
|
||||
implementation 'org.springframework.boot:spring-boot-starter-artemis'
|
||||
implementation 'org.springframework.statemachine:spring-statemachine-starter:3.2.0'
|
||||
compileOnly 'org.projectlombok:lombok'
|
||||
annotationProcessor 'org.projectlombok:lombok'
|
||||
}
|
||||
|
||||
bootJar { enabled = false }
|
||||
jar { enabled = true }
|
||||
@@ -0,0 +1,21 @@
|
||||
package click.kamil.examples.jmsabstract;
|
||||
|
||||
import org.springframework.jms.annotation.JmsListener;
|
||||
|
||||
/**
|
||||
* Template-method JMS base: {@code @JmsListener} lives on the abstract type; subclasses
|
||||
* implement {@link #doPMessage(String)}.
|
||||
*/
|
||||
public abstract class AbstractAA {
|
||||
|
||||
@JmsListener(destination = "orders.commands")
|
||||
public void onMessage(String message) {
|
||||
processMessage(message);
|
||||
}
|
||||
|
||||
protected void processMessage(String message) {
|
||||
doPMessage(message);
|
||||
}
|
||||
|
||||
protected abstract void doPMessage(String message);
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package click.kamil.examples.jmsabstract;
|
||||
|
||||
import org.springframework.statemachine.StateMachine;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public class DocumentAA extends AbstractAA {
|
||||
|
||||
private final StateMachine<OrderStates, OrderEvents> stateMachine;
|
||||
|
||||
public DocumentAA(StateMachine<OrderStates, OrderEvents> stateMachine) {
|
||||
this.stateMachine = stateMachine;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doPMessage(String message) {
|
||||
stateMachine.sendEvent(OrderEvents.SUBMIT);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package click.kamil.examples.jmsabstract;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.jms.annotation.EnableJms;
|
||||
|
||||
@SpringBootApplication
|
||||
@EnableJms
|
||||
public class JmsAbstractTemplateApplication {
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(JmsAbstractTemplateApplication.class, args);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package click.kamil.examples.jmsabstract;
|
||||
|
||||
import org.springframework.statemachine.StateMachine;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public class OrderAA extends AbstractAA {
|
||||
|
||||
private final StateMachine<OrderStates, OrderEvents> stateMachine;
|
||||
|
||||
public OrderAA(StateMachine<OrderStates, OrderEvents> stateMachine) {
|
||||
this.stateMachine = stateMachine;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doPMessage(String message) {
|
||||
stateMachine.sendEvent(OrderEvents.PAY);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package click.kamil.examples.jmsabstract;
|
||||
|
||||
public enum OrderEvents {
|
||||
PAY, SUBMIT
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package click.kamil.examples.jmsabstract;
|
||||
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.statemachine.config.EnableStateMachine;
|
||||
import org.springframework.statemachine.config.StateMachineConfigurerAdapter;
|
||||
import org.springframework.statemachine.config.builders.StateMachineStateConfigurer;
|
||||
import org.springframework.statemachine.config.builders.StateMachineTransitionConfigurer;
|
||||
|
||||
@Configuration
|
||||
@EnableStateMachine
|
||||
public class OrderStateMachineConfig extends StateMachineConfigurerAdapter<OrderStates, OrderEvents> {
|
||||
|
||||
@Override
|
||||
public void configure(StateMachineStateConfigurer<OrderStates, OrderEvents> states) throws Exception {
|
||||
states.withStates()
|
||||
.initial(OrderStates.NEW)
|
||||
.state(OrderStates.PAID)
|
||||
.state(OrderStates.SUBMITTED);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configure(StateMachineTransitionConfigurer<OrderStates, OrderEvents> transitions) throws Exception {
|
||||
transitions
|
||||
.withExternal().source(OrderStates.NEW).target(OrderStates.PAID).event(OrderEvents.PAY)
|
||||
.and()
|
||||
.withExternal().source(OrderStates.NEW).target(OrderStates.SUBMITTED).event(OrderEvents.SUBMIT);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package click.kamil.examples.jmsabstract;
|
||||
|
||||
public enum OrderStates {
|
||||
NEW, PAID, SUBMITTED
|
||||
}
|
||||
Reference in New Issue
Block a user