Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 38 additions & 3 deletions src/main/java/liquidjava/fsm/StateMachineParser.java
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ public class StateMachineParser {

private static final String STATE_SET_ANNOTATION = "StateSet";
private static final String STATE_REFINEMENT_ANNOTATION = "StateRefinement";
private static final String REFINEMENT_ANNOTATION = "Refinement";
private static final String EXTERNAL_REFINEMENTS_FOR_ANNOTATION = "ExternalRefinementsFor";

private record TransitionSource(String from, String cond) {}
Expand Down Expand Up @@ -66,10 +67,14 @@ private static StateMachine parse(String uri, SourcePosition declarationPosition
String simpleClassName = Utils.getSimpleName(className);

// get initial transitions and method transitions
List<StateMachineInitialTransition> factoryTransitions = getFactoryTransitions(ctType, simpleClassName, states);
List<StateMachineInitialTransition> initialTransitions = getInitialTransitions(ctType, simpleClassName, states);
if (initialTransitions.isEmpty()) {
// interfaces only have an implicit initial state when there are no factory methods
if (initialTransitions.isEmpty() && (ctType instanceof CtClass<?> || factoryTransitions.isEmpty())) {
initialTransitions = getDefaultInitialTransitions(ctType, simpleClassName, states.get(0));
}
initialTransitions = new ArrayList<>(initialTransitions);
initialTransitions.addAll(factoryTransitions);
List<StateMachineTransition> transitions = getTransitions(ctType, simpleClassName, states);
if (transitions.isEmpty())
return null; // no transitions found
Expand Down Expand Up @@ -219,9 +224,39 @@ private static List<StateMachineInitialTransition> getDefaultInitialTransitions(
}

private static String getConstructorSignature(CtExecutable<?> element, String className) {
return "new " + className + "(" + String.join(", ", element.getParameters().stream()
return "new " + className + "(" + getParameterTypes(element) + ")";
}

/**
* Gets the initial transitions from static factory methods (methods that return a new object in a given state)
* For classes these must be static, for interfaces any method returning the class type is considered
* @param ctType the CtType (class or interface)
* @param className the class name
* @param states the list of states
* @return initial transitions
*/
private static List<StateMachineInitialTransition> getFactoryTransitions(CtType<?> ctType, String className, List<String> states) {
List<StateMachineInitialTransition> initialTransitions = new ArrayList<>();
for (CtMethod<?> method : ctType.getMethods()) {
if (ctType.isClass() && !method.isStatic())
continue;
if (method.getType() == null || !method.getType().getSimpleName().equals(className))
continue;
String signature = className + "." + method.getSimpleName() + "(" + getParameterTypes(method) + ")";
for (CtAnnotation<?> annotation : method.getAnnotations()) {
if (annotation.getAnnotationType().getSimpleName().equals(REFINEMENT_ANNOTATION)) {
String refinement = annotation.getValueAsString("value");
initialTransitions.addAll(parseInitialTransitions(refinement, states, signature));
}
}
}
return initialTransitions;
}

private static String getParameterTypes(CtExecutable<?> element) {
return String.join(", ", element.getParameters().stream()
.map(parameter -> parameter.getType() == null ? "?" : parameter.getType().getSimpleName())
.toList()) + ")";
.toList());
}

/**
Expand Down
41 changes: 41 additions & 0 deletions src/test/java/liquidjava/fsm/StateMachineParserTests.java
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,47 @@ public void testGuardedSelfLoopDoesNotDuplicateCondition() {
assertStateMachineEquals(expectedSm, sm);
}

@Test
public void testStaticFactoryMethods() {
// static methods returning the class are initial transitions
StateMachine sm = StateMachineParser.parse(uri("StaticFactory.java"));
StateMachine expectedSm = stateMachine("StaticFactory", List.of("open", "closed"),
List.of(new StateMachineTransition("open", "closed", "close")),
List.of(new StateMachineInitialTransition("open"),
new StateMachineInitialTransition("closed"),
new StateMachineInitialTransition("open", "flag"),
new StateMachineInitialTransition("closed", "!flag")));
assertStateMachineEquals(expectedSm, sm);
assertEquals(List.of("new StaticFactory()", "StaticFactory.closedInstance()",
"StaticFactory.of(boolean)", "StaticFactory.of(boolean)"),
sm.initialTransitions().stream().map(StateMachineInitialTransition::constructorSignature).toList());
}

@Test
public void testExternalStaticFactoryMethods() {
// in interfaces any method returning the class is a factory method
StateMachine sm = StateMachineParser.parse(uri("ExternalStaticFactory.java"));
StateMachine expectedSm = stateMachine("java.util.UUID",
List.of("dceSecurityNameRandom", "maybeTime", "timeBased"),
List.of(new StateMachineTransition("timeBased", "timeBased", "timestamp")),
List.of(new StateMachineInitialTransition("timeBased", "mostSigBits / 4096 % 16 == 1"),
new StateMachineInitialTransition("dceSecurityNameRandom", "!(mostSigBits / 4096 % 16 == 1)"),
new StateMachineInitialTransition("maybeTime"),
new StateMachineInitialTransition("dceSecurityNameRandom")));
assertStateMachineEquals(expectedSm, sm);
assertEquals(List.of("new UUID(long, long)", "new UUID(long, long)", "UUID.fromString(String)",
"UUID.randomUUID()"),
sm.initialTransitions().stream().map(StateMachineInitialTransition::constructorSignature).toList());
}

@Test
public void testExternalFactoryWithoutConstructorHasNoDefaultInitialState() {
StateMachine sm = StateMachineParser.parse(uri("ExternalFactoryOnly.java"));
assertNotNull(sm);
assertEquals(List.of(new StateMachineInitialTransition("timeBased", null, "UUID.fromString(String)")),
sm.initialTransitions());
}

private static void assertStateMachineEquals(StateMachine expected, StateMachine actual) {
assertNotNull(actual, "State machine should not be null");
assertEquals(expected.className(), actual.className(), "Class names should match");
Expand Down
19 changes: 19 additions & 0 deletions src/test/resources/fsm/ExternalFactoryOnly.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package fsm;

import java.util.UUID;

import liquidjava.specification.ExternalRefinementsFor;
import liquidjava.specification.Refinement;
import liquidjava.specification.StateRefinement;
import liquidjava.specification.StateSet;

@ExternalRefinementsFor("java.util.UUID")
@StateSet({"dceSecurityNameRandom", "timeBased"})
public interface ExternalFactoryOnly {

@Refinement("timeBased(return)")
UUID fromString(String name);

@StateRefinement(from="timeBased(this)")
long timestamp();
}
25 changes: 25 additions & 0 deletions src/test/resources/fsm/ExternalStaticFactory.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package fsm;

import java.util.UUID;

import liquidjava.specification.ExternalRefinementsFor;
import liquidjava.specification.Refinement;
import liquidjava.specification.StateRefinement;
import liquidjava.specification.StateSet;

@ExternalRefinementsFor("java.util.UUID")
@StateSet({"dceSecurityNameRandom", "maybeTime", "timeBased"})
public interface ExternalStaticFactory {

@StateRefinement(to="((mostSigBits/4096) % 16 == 1) ? timeBased(this) : dceSecurityNameRandom(this)")
void UUID(long mostSigBits, long leastSigBits);

@Refinement("maybeTime(return)")
UUID fromString(String name);

@Refinement("dceSecurityNameRandom(return)")
UUID randomUUID();

@StateRefinement(from="timeBased(this)")
long timestamp();
}
30 changes: 30 additions & 0 deletions src/test/resources/fsm/StaticFactory.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package fsm;

import liquidjava.specification.Refinement;
import liquidjava.specification.StateRefinement;
import liquidjava.specification.StateSet;

@StateSet({"open", "closed"})
public class StaticFactory {

@StateRefinement(to="open(this)")
public StaticFactory() {}

// static factory methods are initial transitions
@Refinement("closed(return)")
public static StaticFactory closedInstance() { return null; }

@Refinement("flag ? open(return) : closed(return)")
public static StaticFactory of(boolean flag) { return null; }

// ignored: not static
@Refinement("closed(return)")
public StaticFactory copy() { return null; }

// ignored: does not return the class type
@Refinement("_ > 0")
public static int count() { return 1; }

@StateRefinement(from="open(this)", to="closed(this)")
public void close() {}
}
Loading