diff --git a/settings.gradle b/settings.gradle index bd1dff4..6ac1451 100644 --- a/settings.gradle +++ b/settings.gradle @@ -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' diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/AbstractCallGraphEngine.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/AbstractCallGraphEngine.java index 9facd02..e615dc4 100644 --- a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/AbstractCallGraphEngine.java +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/AbstractCallGraphEngine.java @@ -90,9 +90,7 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine { List chains = new ArrayList<>(); for (EntryPoint ep : entryPoints) { - String startMethod = ep.getClassName() + "." + ep.getMethodName(); - List startMethods = new ArrayList<>(); - startMethods.add(startMethod); + List startContexts = resolveMessagingStartContexts(ep, callGraph); List> 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> 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> 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 resolveMessagingStartContexts( + EntryPoint ep, + Map> 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 impls = context.getImplementations(declaringClass); + if (impls == null || impls.isEmpty()) { + return List.of(new MessagingStartContext(declaringStart, declaringClass)); + } + + LinkedHashSet 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 path, Map> 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); diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/JdtCallGraphEngine.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/JdtCallGraphEngine.java index ee9aabf..584dd04 100644 --- a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/JdtCallGraphEngine.java +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/JdtCallGraphEngine.java @@ -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; diff --git a/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/JmsAbstractEntryExpansionTest.java b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/JmsAbstractEntryExpansionTest.java new file mode 100644 index 0000000..71f2bef --- /dev/null +++ b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/JmsAbstractEntryExpansionTest.java @@ -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 entryPoints = detectJms(context); + assertThat(entryPoints) + .anySatisfy(ep -> assertThat(ep.getClassName()).isEqualTo("com.example.AbstractAA")); + + HeuristicCallGraphEngine engine = new HeuristicCallGraphEngine(context); + List 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 entryPoints = detectJms(context); + assertThat(entryPoints) + .anySatisfy(ep -> assertThat(ep.getClassName()).isEqualTo("com.example.OrderCommands")); + + HeuristicCallGraphEngine engine = new HeuristicCallGraphEngine(context); + List 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 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; + } +} diff --git a/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/JmsAbstractTemplateSampleExportTest.java b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/JmsAbstractTemplateSampleExportTest.java new file mode 100644 index 0000000..e68fe1c --- /dev/null +++ b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/JmsAbstractTemplateSampleExportTest.java @@ -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 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; + } +} diff --git a/state_machines/jms_abstract_template_sample/build.gradle b/state_machines/jms_abstract_template_sample/build.gradle new file mode 100644 index 0000000..9e2ab24 --- /dev/null +++ b/state_machines/jms_abstract_template_sample/build.gradle @@ -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 } diff --git a/state_machines/jms_abstract_template_sample/src/main/java/click/kamil/examples/jmsabstract/AbstractAA.java b/state_machines/jms_abstract_template_sample/src/main/java/click/kamil/examples/jmsabstract/AbstractAA.java new file mode 100644 index 0000000..21834e6 --- /dev/null +++ b/state_machines/jms_abstract_template_sample/src/main/java/click/kamil/examples/jmsabstract/AbstractAA.java @@ -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); +} diff --git a/state_machines/jms_abstract_template_sample/src/main/java/click/kamil/examples/jmsabstract/DocumentAA.java b/state_machines/jms_abstract_template_sample/src/main/java/click/kamil/examples/jmsabstract/DocumentAA.java new file mode 100644 index 0000000..7e3e073 --- /dev/null +++ b/state_machines/jms_abstract_template_sample/src/main/java/click/kamil/examples/jmsabstract/DocumentAA.java @@ -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 stateMachine; + + public DocumentAA(StateMachine stateMachine) { + this.stateMachine = stateMachine; + } + + @Override + protected void doPMessage(String message) { + stateMachine.sendEvent(OrderEvents.SUBMIT); + } +} diff --git a/state_machines/jms_abstract_template_sample/src/main/java/click/kamil/examples/jmsabstract/JmsAbstractTemplateApplication.java b/state_machines/jms_abstract_template_sample/src/main/java/click/kamil/examples/jmsabstract/JmsAbstractTemplateApplication.java new file mode 100644 index 0000000..6268de8 --- /dev/null +++ b/state_machines/jms_abstract_template_sample/src/main/java/click/kamil/examples/jmsabstract/JmsAbstractTemplateApplication.java @@ -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); + } +} diff --git a/state_machines/jms_abstract_template_sample/src/main/java/click/kamil/examples/jmsabstract/OrderAA.java b/state_machines/jms_abstract_template_sample/src/main/java/click/kamil/examples/jmsabstract/OrderAA.java new file mode 100644 index 0000000..4a77368 --- /dev/null +++ b/state_machines/jms_abstract_template_sample/src/main/java/click/kamil/examples/jmsabstract/OrderAA.java @@ -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 stateMachine; + + public OrderAA(StateMachine stateMachine) { + this.stateMachine = stateMachine; + } + + @Override + protected void doPMessage(String message) { + stateMachine.sendEvent(OrderEvents.PAY); + } +} diff --git a/state_machines/jms_abstract_template_sample/src/main/java/click/kamil/examples/jmsabstract/OrderEvents.java b/state_machines/jms_abstract_template_sample/src/main/java/click/kamil/examples/jmsabstract/OrderEvents.java new file mode 100644 index 0000000..f4b7c41 --- /dev/null +++ b/state_machines/jms_abstract_template_sample/src/main/java/click/kamil/examples/jmsabstract/OrderEvents.java @@ -0,0 +1,5 @@ +package click.kamil.examples.jmsabstract; + +public enum OrderEvents { + PAY, SUBMIT +} diff --git a/state_machines/jms_abstract_template_sample/src/main/java/click/kamil/examples/jmsabstract/OrderStateMachineConfig.java b/state_machines/jms_abstract_template_sample/src/main/java/click/kamil/examples/jmsabstract/OrderStateMachineConfig.java new file mode 100644 index 0000000..41fd7a7 --- /dev/null +++ b/state_machines/jms_abstract_template_sample/src/main/java/click/kamil/examples/jmsabstract/OrderStateMachineConfig.java @@ -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 { + + @Override + public void configure(StateMachineStateConfigurer states) throws Exception { + states.withStates() + .initial(OrderStates.NEW) + .state(OrderStates.PAID) + .state(OrderStates.SUBMITTED); + } + + @Override + public void configure(StateMachineTransitionConfigurer 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); + } +} diff --git a/state_machines/jms_abstract_template_sample/src/main/java/click/kamil/examples/jmsabstract/OrderStates.java b/state_machines/jms_abstract_template_sample/src/main/java/click/kamil/examples/jmsabstract/OrderStates.java new file mode 100644 index 0000000..24d4941 --- /dev/null +++ b/state_machines/jms_abstract_template_sample/src/main/java/click/kamil/examples/jmsabstract/OrderStates.java @@ -0,0 +1,5 @@ +package click.kamil.examples.jmsabstract; + +public enum OrderStates { + NEW, PAID, SUBMITTED +}