Harden enum identifier validation for rawName sync, polymorphic events, and matched transitions.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-12 12:22:39 +02:00
parent 393c998b66
commit 7ffd157105
5 changed files with 489 additions and 18 deletions

View File

@@ -471,6 +471,65 @@ public final class MachineEnumCanonicalizer {
/**
* Derives {@code EnumFormat.fn} display form ({@code OrderEvent.PAY}) from a package-canonical identifier.
*/
public static String expectedFnRawName(String canonicalFqn, String enumTypeFqn) {
return toFnForm(canonicalFqn, enumTypeFqn);
}
/**
* Returns whether {@code rawName} is the fn-form counterpart of a package-canonical {@code fullIdentifier}.
*/
public static boolean isRawNameConsistentWithFullIdentifier(
String rawName,
String fullIdentifier,
String enumTypeFqn) {
if (fullIdentifier == null || fullIdentifier.isBlank() || isStringOrPrimitiveType(enumTypeFqn)) {
return true;
}
if (fullIdentifier.startsWith("<") || fullIdentifier.startsWith("\"")) {
return true;
}
if (isDynamicTriggerExpression(fullIdentifier)) {
return true;
}
String canonical = canonicalizeLabel(fullIdentifier, enumTypeFqn);
if (!canonical.equals(fullIdentifier)) {
return true;
}
if (!constantName(fullIdentifier).matches("[A-Z_][A-Z0-9_]*")) {
return true;
}
String expectedRaw = toFnForm(fullIdentifier, enumTypeFqn);
if (expectedRaw == null) {
return true;
}
return expectedRaw.equals(rawName);
}
/**
* Polymorphic event lists must contain only package-canonical enum constants or symbolic wildcards.
*/
public static boolean isValidPolymorphicEventForm(String event) {
if (event == null || event.isBlank()) {
return false;
}
TriggerEventKind kind = classifyTriggerEvent(event);
if (kind != TriggerEventKind.CANONICAL_ENUM && kind != TriggerEventKind.SYMBOLIC) {
return false;
}
return !hasCorruptedPackageDynamicEvent(event);
}
/**
* True when every entry is a concrete package-canonical enum constant (not symbolic/dynamic).
*/
public static boolean hasOnlyConcretePolymorphicEvents(List<String> polymorphicEvents) {
if (polymorphicEvents == null || polymorphicEvents.isEmpty()) {
return false;
}
return polymorphicEvents.stream()
.allMatch(pe -> classifyTriggerEvent(pe) == TriggerEventKind.CANONICAL_ENUM);
}
static String toFnForm(String canonicalFqn, String enumTypeFqn) {
if (canonicalFqn == null || canonicalFqn.isBlank() || isStringOrPrimitiveType(enumTypeFqn)) {
return null;

View File

@@ -2,6 +2,7 @@ package click.kamil.springstatemachineexporter.analysis.validation;
import click.kamil.springstatemachineexporter.analysis.model.AnalysisResult;
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
import click.kamil.springstatemachineexporter.analysis.model.LifecycleTriggerMarkers;
import click.kamil.springstatemachineexporter.analysis.model.MatchedTransition;
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
import click.kamil.springstatemachineexporter.analysis.resolver.MachineEnumCanonicalizer;
@@ -91,12 +92,14 @@ public final class AnalysisCanonicalFormValidator {
List<Violation> violations) {
validateTransitions(result.getTransitions(), machineTypes, violations);
validateStateCollection(result.getStates(), machineTypes.stateTypeFqn(), "states", violations);
validateStateRawNameSync(result.getStates(), machineTypes.stateTypeFqn(), "states", violations);
validateStateLabels(result.getStartStates(), machineTypes.stateTypeFqn(), "startStates", violations);
validateStateLabels(result.getEndStates(), machineTypes.stateTypeFqn(), "endStates", violations);
if (result.getMetadata() != null) {
validateTriggers(result.getMetadata().getTriggers(), machineTypes, violations);
validateCallChains(result.getMetadata().getCallChains(), machineTypes, violations);
validateCallChains(
result.getMetadata().getCallChains(), result.getTransitions(), machineTypes, violations);
}
}
@@ -124,13 +127,21 @@ public final class AnalysisCanonicalFormValidator {
transition.getEvent().fullIdentifier(),
machineTypes.eventTypeFqn(),
violations);
requireRawNameSync(
prefix + ".event.rawName",
transition.getEvent().rawName(),
transition.getEvent().fullIdentifier(),
machineTypes.eventTypeFqn(),
violations);
}
validateStateList(transition.getSourceStates(), machineTypes.stateTypeFqn(), prefix + ".sourceStates", violations);
validateStateList(transition.getTargetStates(), machineTypes.stateTypeFqn(), prefix + ".targetStates", violations);
validateStateListWithRawSync(
transition.getSourceStates(), machineTypes.stateTypeFqn(), prefix + ".sourceStates", violations);
validateStateListWithRawSync(
transition.getTargetStates(), machineTypes.stateTypeFqn(), prefix + ".targetStates", violations);
}
}
private static void validateStateList(
private static void validateStateListWithRawSync(
List<State> states,
String stateTypeFqn,
String pathPrefix,
@@ -139,7 +150,32 @@ public final class AnalysisCanonicalFormValidator {
return;
}
for (int i = 0; i < states.size(); i++) {
requireCanonical(pathPrefix + "[" + i + "].fullIdentifier", states.get(i).fullIdentifier(), stateTypeFqn, violations);
State state = states.get(i);
requireCanonical(pathPrefix + "[" + i + "].fullIdentifier", state.fullIdentifier(), stateTypeFqn, violations);
requireRawNameSync(
pathPrefix + "[" + i + "].rawName",
state.rawName(),
state.fullIdentifier(),
stateTypeFqn,
violations);
}
}
private static void validateStateRawNameSync(
Set<State> states,
String stateTypeFqn,
String pathPrefix,
List<Violation> violations) {
if (states == null) {
return;
}
int i = 0;
for (State state : states) {
requireRawNameSync(
pathPrefix + "[" + i++ + "].rawName",
state.rawName(),
state.fullIdentifier(),
stateTypeFqn,
violations);
}
}
@@ -187,14 +223,14 @@ public final class AnalysisCanonicalFormValidator {
requireCanonical(prefix + ".event", trigger.getEvent(), eventTypeFqn, violations);
validateTriggerEventForm(prefix + ".event", trigger.getEvent(), violations);
requireCanonical(prefix + ".sourceState", trigger.getSourceState(), stateTypeFqn, violations);
validateDynamicIdentifierForm(prefix + ".sourceState", trigger.getSourceState(), violations);
if (trigger.getPolymorphicEvents() != null) {
for (int j = 0; j < trigger.getPolymorphicEvents().size(); j++) {
requireCanonical(
prefix + ".polymorphicEvents[" + j + "]",
trigger.getPolymorphicEvents().get(j),
eventTypeFqn,
violations);
String polyPath = prefix + ".polymorphicEvents[" + j + "]";
String polyEvent = trigger.getPolymorphicEvents().get(j);
requireCanonical(polyPath, polyEvent, eventTypeFqn, violations);
validatePolymorphicEventForm(polyPath, polyEvent, violations);
}
}
}
@@ -202,6 +238,7 @@ public final class AnalysisCanonicalFormValidator {
private static void validateCallChains(
List<CallChain> callChains,
List<Transition> transitions,
StateMachineTypeResolver.MachineTypes machineTypes,
List<Violation> violations) {
if (callChains == null) {
@@ -218,15 +255,17 @@ public final class AnalysisCanonicalFormValidator {
requireCanonical(triggerPrefix + ".event", trigger.getEvent(), eventTypeFqn, violations);
validateTriggerEventForm(triggerPrefix + ".event", trigger.getEvent(), violations);
requireCanonical(triggerPrefix + ".sourceState", trigger.getSourceState(), stateTypeFqn, violations);
validateDynamicIdentifierForm(triggerPrefix + ".sourceState", trigger.getSourceState(), violations);
if (trigger.getPolymorphicEvents() != null) {
for (int j = 0; j < trigger.getPolymorphicEvents().size(); j++) {
requireCanonical(
triggerPrefix + ".polymorphicEvents[" + j + "]",
trigger.getPolymorphicEvents().get(j),
eventTypeFqn,
violations);
String polyPath = triggerPrefix + ".polymorphicEvents[" + j + "]";
String polyEvent = trigger.getPolymorphicEvents().get(j);
requireCanonical(polyPath, polyEvent, eventTypeFqn, violations);
validatePolymorphicEventForm(polyPath, polyEvent, violations);
}
}
validateMatchedTransitionsWhenResolvable(
prefix, chain, trigger, transitions, machineTypes.eventTypeFqn(), violations);
}
if (chain.getMatchedTransitions() != null) {
for (int j = 0; j < chain.getMatchedTransitions().size(); j++) {
@@ -261,6 +300,124 @@ public final class AnalysisCanonicalFormValidator {
}
}
private static void requireRawNameSync(
String path,
String rawName,
String fullIdentifier,
String enumTypeFqn,
List<Violation> violations) {
if (MachineEnumCanonicalizer.isRawNameConsistentWithFullIdentifier(rawName, fullIdentifier, enumTypeFqn)) {
return;
}
String expected = MachineEnumCanonicalizer.expectedFnRawName(fullIdentifier, enumTypeFqn);
violations.add(new Violation(path, rawName, expected));
}
private static void validatePolymorphicEventForm(String path, String event, List<Violation> violations) {
if (event == null || event.isBlank()) {
return;
}
if (!MachineEnumCanonicalizer.isValidPolymorphicEventForm(event)) {
MachineEnumCanonicalizer.TriggerEventKind kind = MachineEnumCanonicalizer.classifyTriggerEvent(event);
String expected = kind == MachineEnumCanonicalizer.TriggerEventKind.DYNAMIC_EXPRESSION
&& MachineEnumCanonicalizer.hasCorruptedPackageDynamicEvent(event)
? MachineEnumCanonicalizer.denormalizeCorruptedDynamicEvent(event)
: "CANONICAL_ENUM or SYMBOLIC";
violations.add(new Violation(path, event, expected));
}
}
private static void validateDynamicIdentifierForm(String path, String value, List<Violation> violations) {
if (value == null || value.isBlank()) {
return;
}
if (MachineEnumCanonicalizer.hasCorruptedPackageDynamicEvent(value)) {
violations.add(new Violation(
path,
value,
MachineEnumCanonicalizer.denormalizeCorruptedDynamicEvent(value)));
}
}
private static void validateMatchedTransitionsWhenResolvable(
String chainPrefix,
CallChain chain,
TriggerPoint trigger,
List<Transition> transitions,
String eventTypeFqn,
List<Violation> violations) {
if (trigger.isExternal() || trigger.isAmbiguous()) {
return;
}
if (LifecycleTriggerMarkers.isLifecycle(trigger.getEvent())) {
return;
}
if (!MachineEnumCanonicalizer.hasOnlyConcretePolymorphicEvents(trigger.getPolymorphicEvents())) {
return;
}
if (MachineEnumCanonicalizer.classifyTriggerEvent(trigger.getEvent())
!= MachineEnumCanonicalizer.TriggerEventKind.DYNAMIC_EXPRESSION) {
return;
}
if (chain.getMatchedTransitions() != null && !chain.getMatchedTransitions().isEmpty()) {
return;
}
if (!polymorphicEventsMatchAnyTransition(trigger.getPolymorphicEvents(), transitions, eventTypeFqn)) {
return;
}
violations.add(new Violation(
chainPrefix + ".matchedTransitions",
"empty",
"non-empty when dynamic trigger has concrete polymorphicEvents matching transitions"));
}
private static boolean polymorphicEventsMatchAnyTransition(
List<String> polymorphicEvents,
List<Transition> transitions,
String eventTypeFqn) {
if (transitions == null || transitions.isEmpty()) {
return false;
}
for (String polyEvent : polymorphicEvents) {
for (Transition transition : transitions) {
if (transition.getEvent() == null) {
continue;
}
String smEvent = transition.getEvent().fullIdentifier() != null
? transition.getEvent().fullIdentifier()
: transition.getEvent().rawName();
if (eventsMatch(polyEvent, smEvent, eventTypeFqn)) {
return true;
}
}
}
return false;
}
private static boolean eventsMatch(String triggerEvent, String smEvent, String eventTypeFqn) {
if (triggerEvent == null || smEvent == null) {
return false;
}
if (triggerEvent.equals(smEvent)) {
return true;
}
String triggerConst = constantName(triggerEvent);
String smConst = constantName(smEvent);
if (!triggerConst.equals(smConst)) {
return false;
}
if (MachineEnumCanonicalizer.isMachineEnumReference(triggerEvent, eventTypeFqn)
&& MachineEnumCanonicalizer.isMachineEnumReference(smEvent, eventTypeFqn)) {
return true;
}
return false;
}
private static String constantName(String ref) {
int dot = ref.lastIndexOf('.');
return dot >= 0 ? ref.substring(dot + 1) : ref;
}
private static void validateTriggerEventForm(String path, String event, List<Violation> violations) {
if (event == null || event.isBlank()) {
return;

View File

@@ -211,6 +211,28 @@ class MachineEnumCanonicalizerTest {
"com.example.order.OrderEvent.PAY");
}
@Test
void shouldValidateRawNameConsistencyForCanonicalHelpers() {
assertThat(MachineEnumCanonicalizer.isRawNameConsistentWithFullIdentifier(
"OrderEvent.PAY", "com.example.order.OrderEvent.PAY", "com.example.order.OrderEvent"))
.isTrue();
assertThat(MachineEnumCanonicalizer.isRawNameConsistentWithFullIdentifier(
"UNDEFINED", "com.example.order.OrderEvent.PAY", "com.example.order.OrderEvent"))
.isFalse();
assertThat(MachineEnumCanonicalizer.isValidPolymorphicEventForm("com.example.order.OrderEvent.PAY"))
.isTrue();
assertThat(MachineEnumCanonicalizer.isValidPolymorphicEventForm("<SYMBOLIC: OrderEvent.*>"))
.isTrue();
assertThat(MachineEnumCanonicalizer.isValidPolymorphicEventForm("OrderEvent.valueOf(event)"))
.isFalse();
assertThat(MachineEnumCanonicalizer.hasOnlyConcretePolymorphicEvents(
List.of("com.example.order.OrderEvent.PAY")))
.isTrue();
assertThat(MachineEnumCanonicalizer.hasOnlyConcretePolymorphicEvents(
List.of("<SYMBOLIC: OrderEvent.*>")))
.isFalse();
}
@Test
void shouldDedupeStatesByCanonicalFullIdentifier(@TempDir Path tempDir) throws IOException {
writeSampleConfig(tempDir);

View File

@@ -82,9 +82,83 @@ class ExportPipelineIdentifierConsistencyTest {
.resolve("com.example.config.OrderStateMachineConfiguration.json"));
assertNoMixedEnumIdentifiers(exported);
assertRawNameSync(exported);
assertThat(exported.getMetadata().getCallChains().get(0).getMatchedTransitions()).isNotEmpty();
}
static void assertRawNameSync(AnalysisResult result) {
String stateTypeFqn = result.getStateTypeFqn();
String eventTypeFqn = result.getEventTypeFqn();
if (stateTypeFqn == null || !stateTypeFqn.contains(".")
|| MachineEnumCanonicalizer.isStringOrPrimitiveType(stateTypeFqn)) {
return;
}
if (result.getStates() != null) {
for (State state : result.getStates()) {
assertThat(MachineEnumCanonicalizer.isRawNameConsistentWithFullIdentifier(
state.rawName(), state.fullIdentifier(), stateTypeFqn))
.as("state rawName sync for %s", state.fullIdentifier())
.isTrue();
}
}
if (result.getTransitions() != null) {
for (Transition transition : result.getTransitions()) {
if (transition.getEvent() != null) {
assertThat(MachineEnumCanonicalizer.isRawNameConsistentWithFullIdentifier(
transition.getEvent().rawName(),
transition.getEvent().fullIdentifier(),
eventTypeFqn))
.as("event rawName sync for %s", transition.getEvent().fullIdentifier())
.isTrue();
}
assertStateListRawSync(transition.getSourceStates(), stateTypeFqn);
assertStateListRawSync(transition.getTargetStates(), stateTypeFqn);
}
}
}
private static void assertStateListRawSync(List<State> states, String stateTypeFqn) {
if (states == null) {
return;
}
for (State state : states) {
assertThat(MachineEnumCanonicalizer.isRawNameConsistentWithFullIdentifier(
state.rawName(), state.fullIdentifier(), stateTypeFqn))
.as("state rawName sync for %s", state.fullIdentifier())
.isTrue();
}
}
private static void assertPolymorphicEventForms(AnalysisResult result) {
if (result.getMetadata() == null) {
return;
}
collectTriggers(result).forEach(trigger -> {
if (trigger.getPolymorphicEvents() != null) {
for (String poly : trigger.getPolymorphicEvents()) {
assertThat(MachineEnumCanonicalizer.isValidPolymorphicEventForm(poly))
.as("polymorphic event form: %s", poly)
.isTrue();
}
}
});
}
private static List<TriggerPoint> collectTriggers(AnalysisResult result) {
List<TriggerPoint> triggers = new java.util.ArrayList<>();
if (result.getMetadata().getTriggers() != null) {
triggers.addAll(result.getMetadata().getTriggers());
}
if (result.getMetadata().getCallChains() != null) {
for (CallChain chain : result.getMetadata().getCallChains()) {
if (chain.getTriggerPoint() != null) {
triggers.add(chain.getTriggerPoint());
}
}
}
return triggers;
}
@Test
void astExportShouldNotMixShortAndPackageCanonicalStateIdentifiers(@TempDir Path tempDir) throws Exception {
Path enterprise = findProjectRoot().resolve("state_machines/state_machine_enterprise");
@@ -105,7 +179,9 @@ class ExportPipelineIdentifierConsistencyTest {
}
AnalysisResult result = new JsonImportService().importAnalysisResult(json);
assertNoMixedEnumIdentifiers(result);
assertRawNameSync(result);
assertTriggerEventForms(result);
assertPolymorphicEventForms(result);
}
}
}

View File

@@ -171,6 +171,151 @@ class AnalysisCanonicalFormValidatorTest {
.noneMatch(v -> v.path().contains(".event"));
}
@Test
void shouldFailWhenEventRawNameDoesNotMatchFnForm() {
Transition transition = new Transition();
transition.setEvent(Event.of("UNDEFINED", "com.example.order.OrderEvent.PAY"));
transition.setSourceStates(List.of(
State.of("OrderState.NEW", "com.example.order.OrderState.NEW")));
AnalysisResult result = AnalysisResult.builder()
.name("com.example.config.OrderStateMachineConfiguration")
.stateTypeFqn("com.example.order.OrderState")
.eventTypeFqn("com.example.order.OrderEvent")
.transitions(List.of(transition))
.build();
List<AnalysisCanonicalFormValidator.Violation> violations =
AnalysisCanonicalFormValidator.validateWithMachineTypes(
result,
new click.kamil.springstatemachineexporter.analysis.resolver.StateMachineTypeResolver.MachineTypes(
"com.example.order.OrderState", "com.example.order.OrderEvent"));
assertThat(violations)
.extracting(AnalysisCanonicalFormValidator.Violation::path)
.contains("transitions[0].event.rawName");
}
@Test
void shouldFailWhenPolymorphicEventIsDynamicExpression() {
AnalysisResult result = AnalysisResult.builder()
.name("com.example.config.OrderStateMachineConfiguration")
.stateTypeFqn("com.example.order.OrderState")
.eventTypeFqn("com.example.order.OrderEvent")
.metadata(CodebaseMetadata.builder()
.triggers(List.of(TriggerPoint.builder()
.event("OrderEvent.valueOf(eventString)")
.className("com.example.web.OrderController")
.methodName("dispatch")
.sourceFile("OrderController.java")
.polymorphicEvents(List.of("OrderEvent.valueOf(eventString)"))
.build()))
.build())
.build();
List<AnalysisCanonicalFormValidator.Violation> violations =
AnalysisCanonicalFormValidator.validateWithMachineTypes(
result,
new click.kamil.springstatemachineexporter.analysis.resolver.StateMachineTypeResolver.MachineTypes(
"com.example.order.OrderState", "com.example.order.OrderEvent"));
assertThat(violations)
.extracting(AnalysisCanonicalFormValidator.Violation::path)
.contains("metadata.triggers[0].polymorphicEvents[0]");
}
@Test
void shouldFailWhenSourceStateHasCorruptedPackageValueOfForm() {
AnalysisResult result = AnalysisResult.builder()
.name("com.example.config.OrderStateMachineConfiguration")
.stateTypeFqn("com.example.order.OrderState")
.eventTypeFqn("com.example.order.OrderEvent")
.metadata(CodebaseMetadata.builder()
.triggers(List.of(TriggerPoint.builder()
.event("com.example.order.OrderEvent.PAY")
.sourceState("com.example.order.OrderState.valueOf(stateString)")
.className("com.example.web.OrderController")
.methodName("dispatch")
.sourceFile("OrderController.java")
.build()))
.build())
.build();
List<AnalysisCanonicalFormValidator.Violation> violations =
AnalysisCanonicalFormValidator.validateWithMachineTypes(
result,
new click.kamil.springstatemachineexporter.analysis.resolver.StateMachineTypeResolver.MachineTypes(
"com.example.order.OrderState", "com.example.order.OrderEvent"));
assertThat(violations)
.extracting(AnalysisCanonicalFormValidator.Violation::path)
.contains("metadata.triggers[0].sourceState");
}
@Test
void shouldFailWhenDynamicTriggerHasConcretePolymorphicEventsButNoMatchedTransitions() {
AnalysisResult result = AnalysisResult.builder()
.name("com.example.config.OrderStateMachineConfiguration")
.stateTypeFqn("com.example.order.OrderState")
.eventTypeFqn("com.example.order.OrderEvent")
.transitions(List.of(transition(
"com.example.order.OrderEvent.PAY",
"com.example.order.OrderState.NEW",
"com.example.order.OrderState.PAID")))
.metadata(CodebaseMetadata.builder()
.callChains(List.of(CallChain.builder()
.triggerPoint(TriggerPoint.builder()
.event("event.getType()")
.className("com.example.web.OrderController")
.methodName("dispatch")
.sourceFile("OrderController.java")
.polymorphicEvents(List.of("com.example.order.OrderEvent.PAY"))
.build())
.build()))
.build())
.build();
List<AnalysisCanonicalFormValidator.Violation> violations =
AnalysisCanonicalFormValidator.validateWithMachineTypes(
result,
new click.kamil.springstatemachineexporter.analysis.resolver.StateMachineTypeResolver.MachineTypes(
"com.example.order.OrderState", "com.example.order.OrderEvent"));
assertThat(violations)
.extracting(AnalysisCanonicalFormValidator.Violation::path)
.contains("metadata.callChains[0].matchedTransitions");
}
@Test
void shouldAllowSymbolicPolymorphicEventsWithoutMatchedTransitions() {
AnalysisResult result = AnalysisResult.builder()
.name("com.example.config.OrderStateMachineConfiguration")
.stateTypeFqn("com.example.order.OrderState")
.eventTypeFqn("com.example.order.OrderEvent")
.transitions(List.of(transition(
"com.example.order.OrderEvent.PAY",
"com.example.order.OrderState.NEW",
"com.example.order.OrderState.PAID")))
.metadata(CodebaseMetadata.builder()
.callChains(List.of(CallChain.builder()
.triggerPoint(TriggerPoint.builder()
.event("event.getType()")
.className("com.example.web.OrderController")
.methodName("dispatch")
.sourceFile("OrderController.java")
.polymorphicEvents(List.of("<SYMBOLIC: OrderEvent.*>"))
.build())
.build()))
.build())
.build();
assertThat(AnalysisCanonicalFormValidator.validateWithMachineTypes(
result,
new click.kamil.springstatemachineexporter.analysis.resolver.StateMachineTypeResolver.MachineTypes(
"com.example.order.OrderState", "com.example.order.OrderEvent")))
.noneMatch(v -> v.path().contains("matchedTransitions"));
}
@Test
void shouldSkipValidationForStringStateMachines(@TempDir Path tempDir) throws IOException {
Path configPkg = tempDir.resolve("com/example/config");
@@ -195,13 +340,25 @@ class AnalysisCanonicalFormValidatorTest {
}
private static Transition transition(String eventFqn, String sourceFqn, String targetFqn) {
String eventRaw = fnForm(eventFqn);
String sourceRaw = fnForm(sourceFqn);
String targetRaw = fnForm(targetFqn);
Transition transition = new Transition();
transition.setEvent(Event.of(eventFqn, eventFqn));
transition.setSourceStates(List.of(State.of(sourceFqn, sourceFqn)));
transition.setTargetStates(List.of(State.of(targetFqn, targetFqn)));
transition.setEvent(Event.of(eventRaw, eventFqn));
transition.setSourceStates(List.of(State.of(sourceRaw, sourceFqn)));
transition.setTargetStates(List.of(State.of(targetRaw, targetFqn)));
return transition;
}
private static String fnForm(String identifier) {
if (identifier == null || !identifier.contains(".")) {
return identifier;
}
int lastDot = identifier.lastIndexOf('.');
int prevDot = identifier.lastIndexOf('.', lastDot - 1);
return prevDot > 0 ? identifier.substring(prevDot + 1) : identifier;
}
private static void writeSampleConfig(Path tempDir) throws IOException {
Path orderPkg = tempDir.resolve("com/example/order");
Path configPkg = tempDir.resolve("com/example/config");