Add layered regression tests for functional-interface dataflow linking.
Lock Supplier.get(), Runnable.run(), and call-site enum resolution to the dataflow pipeline so a parallel resolver cannot creep back in unnoticed. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,507 @@
|
|||||||
|
package click.kamil.springstatemachineexporter.analysis.service;
|
||||||
|
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.model.CallEdge;
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.model.EntryPoint;
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
|
||||||
|
import click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver;
|
||||||
|
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||||
|
import click.kamil.springstatemachineexporter.ast.common.DataFlowModel;
|
||||||
|
import click.kamil.springstatemachineexporter.ast.common.JdtDataFlowModel;
|
||||||
|
import org.eclipse.jdt.core.dom.Expression;
|
||||||
|
import org.eclipse.jdt.core.dom.ExpressionStatement;
|
||||||
|
import org.eclipse.jdt.core.dom.LambdaExpression;
|
||||||
|
import org.eclipse.jdt.core.dom.MethodDeclaration;
|
||||||
|
import org.eclipse.jdt.core.dom.MethodInvocation;
|
||||||
|
import org.eclipse.jdt.core.dom.TypeDeclaration;
|
||||||
|
import org.eclipse.jdt.core.dom.VariableDeclarationFragment;
|
||||||
|
import org.eclipse.jdt.core.dom.VariableDeclarationStatement;
|
||||||
|
import org.junit.jupiter.api.Nested;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.io.TempDir;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Regression suite for functional-interface event resolution ({@code Supplier.get()},
|
||||||
|
* {@code Runnable.run()}, side-effect lambdas). Must stay on the dataflow pipeline:
|
||||||
|
* {@link JdtDataFlowModel} → {@link VariableTracer} → {@link PathBindingEvaluator} → call graph.
|
||||||
|
* Do not reintroduce a parallel {@code CallSiteArgumentResolver} bridge.
|
||||||
|
*/
|
||||||
|
class FunctionalInterfaceDataFlowRegressionTest {
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
class JdtDataFlowModelLayer {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldResolveEnumFromSupplierGetOnLocalLambda(@TempDir Path tempDir) throws IOException {
|
||||||
|
CodebaseContext context = scan(tempDir, """
|
||||||
|
package com.example;
|
||||||
|
import java.util.function.Supplier;
|
||||||
|
public enum OrderCommand { PAY, SHIP }
|
||||||
|
public class Hooks {
|
||||||
|
OrderCommand read() {
|
||||||
|
Supplier<OrderCommand> supplier = () -> OrderCommand.PAY;
|
||||||
|
return supplier.get();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""");
|
||||||
|
|
||||||
|
MethodInvocation getCall = findReturnMethodInvocation(context, "com.example.Hooks", "read");
|
||||||
|
assertThat(getCall.getName().getIdentifier()).isEqualTo("get");
|
||||||
|
|
||||||
|
DataFlowModel model = new JdtDataFlowModel(context);
|
||||||
|
List<String> constants = new ArrayList<>();
|
||||||
|
for (Expression def : model.getReachingDefinitions(getCall)) {
|
||||||
|
new ConstantExtractor(context, context.getConstantResolver(), null)
|
||||||
|
.extractConstantsFromExpression(def, constants);
|
||||||
|
}
|
||||||
|
assertThat(constants).anyMatch(c -> c.contains("PAY"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldResolveEnumLiteralThroughDataflow(@TempDir Path tempDir) throws IOException {
|
||||||
|
CodebaseContext context = scan(tempDir, """
|
||||||
|
package com.example;
|
||||||
|
public enum OrderCommand { PAY, SHIP }
|
||||||
|
public class Service {
|
||||||
|
public void fireEvent() {
|
||||||
|
sendEvent(OrderCommand.PAY);
|
||||||
|
}
|
||||||
|
protected void sendEvent(OrderCommand event) {}
|
||||||
|
}
|
||||||
|
""");
|
||||||
|
|
||||||
|
MethodInvocation sendEventCall = findSideEffectCall(context, "com.example.Service", "fireEvent");
|
||||||
|
Expression eventArg = (Expression) sendEventCall.arguments().get(0);
|
||||||
|
|
||||||
|
DataFlowModel model = new JdtDataFlowModel(context);
|
||||||
|
List<String> fromDataflow = new ArrayList<>();
|
||||||
|
for (Expression def : model.getReachingDefinitions(eventArg)) {
|
||||||
|
new ConstantExtractor(context, context.getConstantResolver(), null)
|
||||||
|
.extractConstantsFromExpression(def, fromDataflow);
|
||||||
|
}
|
||||||
|
assertThat(fromDataflow).anyMatch(c -> c.contains("PAY"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldNotTreatPlainObjectGetAsFunctionalSupplier(@TempDir Path tempDir) throws IOException {
|
||||||
|
CodebaseContext context = scan(tempDir, """
|
||||||
|
package com.example;
|
||||||
|
public class Runner {
|
||||||
|
static class Box {
|
||||||
|
public String get() { return "X"; }
|
||||||
|
}
|
||||||
|
public void run() {
|
||||||
|
Box box = new Box();
|
||||||
|
String value = box.get();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""");
|
||||||
|
|
||||||
|
DataFlowModel model = new JdtDataFlowModel(context);
|
||||||
|
Expression initializer = findInitializer(context, "com.example.Runner", "run", "value");
|
||||||
|
assertThat(model.resolveValue(initializer, context)).isEqualTo("X");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
class VariableTracerLayer {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldResolveSupplierLambdaAtCallSite(@TempDir Path tempDir) throws IOException {
|
||||||
|
RunnableProject project = RunnableProject.supplierOnly(tempDir);
|
||||||
|
VariableTracer tracer = tracer(project.context());
|
||||||
|
|
||||||
|
List<String> constants = tracer.resolveConstantsFromCallSiteArgument(
|
||||||
|
"com.example.web.OrderController.process",
|
||||||
|
"com.example.service.OrderService.fireEvent",
|
||||||
|
0);
|
||||||
|
assertThat(constants).containsExactly("OrderCommand.PAY");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldResolveRunnableSideEffectLambdaAtCallSite(@TempDir Path tempDir) throws IOException {
|
||||||
|
RunnableProject project = RunnableProject.withExecutor(tempDir);
|
||||||
|
VariableTracer tracer = tracer(project.context());
|
||||||
|
|
||||||
|
assertThat(tracer.findFunctionalParameterIndex("com.example.executor.TaskExecutor.execute"))
|
||||||
|
.isZero();
|
||||||
|
assertThat(tracer.findCallSiteArgumentExpression(
|
||||||
|
"com.example.web.OrderController.process",
|
||||||
|
"com.example.executor.TaskExecutor.execute",
|
||||||
|
0)).isInstanceOf(LambdaExpression.class);
|
||||||
|
|
||||||
|
List<String> constants = tracer.resolveConstantsFromCallSiteArgument(
|
||||||
|
"com.example.service.AbstractOrderService.fireEvent",
|
||||||
|
"com.example.service.AbstractOrderService.sendEvent",
|
||||||
|
0);
|
||||||
|
assertThat(constants).containsExactly("OrderCommand.PAY");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
class PathBindingEvaluatorLayer {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldPropagateRunnableLambdaEnumThroughPathBindings(@TempDir Path tempDir) throws IOException {
|
||||||
|
RunnableProject project = RunnableProject.withExecutor(tempDir);
|
||||||
|
HeuristicCallGraphEngine engine = new HeuristicCallGraphEngine(project.context());
|
||||||
|
Map<String, List<CallEdge>> graph = engine.buildCallGraph();
|
||||||
|
CallGraphPathFinder pathFinder = new CallGraphPathFinder(project.context());
|
||||||
|
PathBindingEvaluator evaluator = new PathBindingEvaluator(
|
||||||
|
project.context(), engine.variableTracer, project.context().getConstantResolver(), engine.typeResolver);
|
||||||
|
|
||||||
|
List<String> path = List.of(
|
||||||
|
"com.example.web.OrderController.process",
|
||||||
|
"com.example.executor.TaskExecutor.execute",
|
||||||
|
runnableRunHop(graph),
|
||||||
|
"com.example.service.AbstractOrderService.fireEvent",
|
||||||
|
"com.example.service.AbstractOrderService.sendEvent");
|
||||||
|
|
||||||
|
Map<String, String> bindings = evaluator.traceBindings(path, graph, pathFinder);
|
||||||
|
assertThat(bindings.values()).anyMatch(v -> v.contains("PAY"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldPropagateSupplierLambdaThroughParameterMap(@TempDir Path tempDir) throws IOException {
|
||||||
|
RunnableProject project = RunnableProject.supplierOnly(tempDir);
|
||||||
|
HeuristicCallGraphEngine engine = new HeuristicCallGraphEngine(project.context());
|
||||||
|
Map<String, List<CallEdge>> graph = engine.buildCallGraph();
|
||||||
|
|
||||||
|
Map<String, String> paramValues = engine.variableTracer.buildParameterValuesMap(
|
||||||
|
"com.example.web.OrderController.process",
|
||||||
|
"com.example.service.OrderService.fireEvent",
|
||||||
|
graph,
|
||||||
|
List.of(
|
||||||
|
"com.example.web.OrderController.process",
|
||||||
|
"com.example.service.OrderService.fireEvent"),
|
||||||
|
1);
|
||||||
|
|
||||||
|
assertThat(paramValues.get("provider")).contains("PAY");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
class CallGraphEndToEndLayer {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldLinkSupplierLambdaToSingleEnum(@TempDir Path tempDir) throws IOException {
|
||||||
|
CodebaseContext context = scan(tempDir, """
|
||||||
|
package com.example;
|
||||||
|
import java.util.function.Supplier;
|
||||||
|
public class MyController {
|
||||||
|
private StateMachine machine;
|
||||||
|
public void process() {
|
||||||
|
fireEvent(() -> TransitionEnum.STATE_L);
|
||||||
|
}
|
||||||
|
private void fireEvent(Supplier<TransitionEnum> eventSupplier) {
|
||||||
|
machine.fire(eventSupplier.get());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
class StateMachine {
|
||||||
|
public void fire(TransitionEnum event) {}
|
||||||
|
}
|
||||||
|
enum TransitionEnum { STATE_L, STATE_M }
|
||||||
|
""");
|
||||||
|
|
||||||
|
HeuristicCallGraphEngine engine = new HeuristicCallGraphEngine(context);
|
||||||
|
List<CallChain> chains = engine.findChains(
|
||||||
|
List.of(EntryPoint.builder()
|
||||||
|
.className("com.example.MyController")
|
||||||
|
.methodName("process")
|
||||||
|
.build()),
|
||||||
|
List.of(TriggerPoint.builder()
|
||||||
|
.className("com.example.StateMachine")
|
||||||
|
.methodName("fire")
|
||||||
|
.event("event")
|
||||||
|
.build()));
|
||||||
|
assertThat(chains).hasSize(1);
|
||||||
|
assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents())
|
||||||
|
.containsExactly("TransitionEnum.STATE_L");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldLinkCrossClassSupplierLambda(@TempDir Path tempDir) throws IOException {
|
||||||
|
Path root = tempDir.resolve("cross");
|
||||||
|
Files.createDirectories(root);
|
||||||
|
Files.writeString(root.resolve("JmsListener.java"), """
|
||||||
|
package com.example.web;
|
||||||
|
import com.example.service.StateMachineService;
|
||||||
|
import com.example.domain.OrderEvent;
|
||||||
|
public class JmsOrderListener {
|
||||||
|
private final StateMachineService service;
|
||||||
|
public JmsOrderListener(StateMachineService service) { this.service = service; }
|
||||||
|
public void receive() { service.sendWithProvider(() -> OrderEvent.CANCEL); }
|
||||||
|
}
|
||||||
|
""");
|
||||||
|
Files.writeString(root.resolve("Service.java"), """
|
||||||
|
package com.example.service;
|
||||||
|
import com.example.domain.OrderEvent;
|
||||||
|
import java.util.function.Supplier;
|
||||||
|
public class StateMachineService {
|
||||||
|
public <T extends OrderEvent> void sendWithProvider(Supplier<T> provider) {
|
||||||
|
OrderEvent event = provider.get();
|
||||||
|
fire(event);
|
||||||
|
}
|
||||||
|
private void fire(OrderEvent event) {}
|
||||||
|
}
|
||||||
|
""");
|
||||||
|
Files.writeString(root.resolve("OrderEvent.java"), """
|
||||||
|
package com.example.domain;
|
||||||
|
public enum OrderEvent { CANCEL, PROCESS }
|
||||||
|
""");
|
||||||
|
|
||||||
|
CodebaseContext context = new CodebaseContext();
|
||||||
|
context.scan(root);
|
||||||
|
HeuristicCallGraphEngine engine = new HeuristicCallGraphEngine(context);
|
||||||
|
List<CallChain> chains = engine.findChains(
|
||||||
|
List.of(EntryPoint.builder()
|
||||||
|
.className("com.example.web.JmsOrderListener")
|
||||||
|
.methodName("receive")
|
||||||
|
.build()),
|
||||||
|
List.of(TriggerPoint.builder()
|
||||||
|
.className("com.example.service.StateMachineService")
|
||||||
|
.methodName("fire")
|
||||||
|
.event("event")
|
||||||
|
.build()));
|
||||||
|
assertThat(chains).hasSize(1);
|
||||||
|
assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).containsExactly("OrderEvent.CANCEL");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldLinkRunnableExecuteSideEffectLambda(@TempDir Path tempDir) throws IOException {
|
||||||
|
RunnableProject project = RunnableProject.withExecutor(tempDir);
|
||||||
|
assertSinglePayEvent(chains(project, "com.example.service.AbstractOrderService", "sendEvent"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldReachFireSiteThroughRunnableRunHop(@TempDir Path tempDir) throws IOException {
|
||||||
|
RunnableProject project = RunnableProject.withExecutor(tempDir);
|
||||||
|
HeuristicCallGraphEngine engine = new HeuristicCallGraphEngine(project.context());
|
||||||
|
Map<String, List<CallEdge>> graph = engine.buildCallGraph();
|
||||||
|
|
||||||
|
assertThat(graph.get("com.example.executor.TaskExecutor.execute"))
|
||||||
|
.extracting(CallEdge::getTargetMethod)
|
||||||
|
.anyMatch(target -> target.contains("Runnable.run"));
|
||||||
|
|
||||||
|
List<CallChain> chains = engine.findChains(
|
||||||
|
List.of(EntryPoint.builder()
|
||||||
|
.className("com.example.web.OrderController")
|
||||||
|
.methodName("process")
|
||||||
|
.build()),
|
||||||
|
List.of(TriggerPoint.builder()
|
||||||
|
.className("com.example.service.AbstractOrderService")
|
||||||
|
.methodName("sendEvent")
|
||||||
|
.event("event")
|
||||||
|
.build()));
|
||||||
|
assertThat(chains).hasSize(1);
|
||||||
|
assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents())
|
||||||
|
.containsExactly("OrderCommand.PAY");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void assertSinglePayEvent(List<CallChain> chains) {
|
||||||
|
assertThat(chains).hasSize(1);
|
||||||
|
assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).containsExactly("OrderCommand.PAY");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<CallChain> chains(RunnableProject project, String triggerClass, String triggerMethod) {
|
||||||
|
HeuristicCallGraphEngine engine = new HeuristicCallGraphEngine(project.context());
|
||||||
|
return engine.findChains(
|
||||||
|
List.of(EntryPoint.builder()
|
||||||
|
.className("com.example.web.OrderController")
|
||||||
|
.methodName("process")
|
||||||
|
.build()),
|
||||||
|
List.of(TriggerPoint.builder()
|
||||||
|
.className(triggerClass)
|
||||||
|
.methodName(triggerMethod)
|
||||||
|
.event("event")
|
||||||
|
.build()));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String runnableRunHop(Map<String, List<CallEdge>> graph) {
|
||||||
|
List<CallEdge> edges = graph.get("com.example.executor.TaskExecutor.execute");
|
||||||
|
assertThat(edges).isNotNull();
|
||||||
|
return edges.stream()
|
||||||
|
.map(CallEdge::getTargetMethod)
|
||||||
|
.filter(hop -> hop.endsWith("Runnable.run"))
|
||||||
|
.findFirst()
|
||||||
|
.orElseThrow();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static VariableTracer tracer(CodebaseContext context) {
|
||||||
|
VariableTracer tracer = new VariableTracer(context, context.getConstantResolver());
|
||||||
|
TypeResolver typeResolver = new TypeResolver(context);
|
||||||
|
ConstantExtractor constantExtractor = new ConstantExtractor(context, context.getConstantResolver(), null);
|
||||||
|
tracer.setTypeResolver(typeResolver);
|
||||||
|
tracer.setConstantExtractor(constantExtractor);
|
||||||
|
return tracer;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static CodebaseContext scan(Path tempDir, String source) throws IOException {
|
||||||
|
Files.writeString(tempDir.resolve("App.java"), source);
|
||||||
|
CodebaseContext context = new CodebaseContext();
|
||||||
|
context.setResolveBindings(true);
|
||||||
|
context.scan(tempDir);
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Expression findInitializer(
|
||||||
|
CodebaseContext context, String typeFqn, String methodName, String variableName) {
|
||||||
|
TypeDeclaration type = context.getTypeDeclaration(typeFqn);
|
||||||
|
MethodDeclaration method = context.findMethodDeclaration(type, methodName, true);
|
||||||
|
for (Object statementObj : method.getBody().statements()) {
|
||||||
|
if (statementObj instanceof VariableDeclarationStatement statement) {
|
||||||
|
for (Object fragmentObj : statement.fragments()) {
|
||||||
|
if (fragmentObj instanceof VariableDeclarationFragment fragment
|
||||||
|
&& variableName.equals(fragment.getName().getIdentifier())) {
|
||||||
|
return fragment.getInitializer();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new AssertionError("Initializer not found: " + variableName);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static MethodInvocation findMethodInvocationInitializer(
|
||||||
|
CodebaseContext context, String typeFqn, String methodName, String variableName) {
|
||||||
|
Expression initializer = findInitializer(context, typeFqn, methodName, variableName);
|
||||||
|
assertThat(initializer).isInstanceOf(MethodInvocation.class);
|
||||||
|
return (MethodInvocation) initializer;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static MethodInvocation findSideEffectCall(CodebaseContext context, String typeFqn, String methodName) {
|
||||||
|
TypeDeclaration type = context.getTypeDeclaration(typeFqn);
|
||||||
|
MethodDeclaration method = context.findMethodDeclaration(type, methodName, true);
|
||||||
|
for (Object statementObj : method.getBody().statements()) {
|
||||||
|
if (statementObj instanceof ExpressionStatement statement
|
||||||
|
&& statement.getExpression() instanceof MethodInvocation invocation) {
|
||||||
|
return invocation;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new AssertionError("Side-effect call not found in " + methodName);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static MethodInvocation findReturnMethodInvocation(CodebaseContext context, String typeFqn, String methodName) {
|
||||||
|
TypeDeclaration type = context.getTypeDeclaration(typeFqn);
|
||||||
|
MethodDeclaration method = context.findMethodDeclaration(type, methodName, true);
|
||||||
|
for (Object statementObj : method.getBody().statements()) {
|
||||||
|
if (statementObj instanceof org.eclipse.jdt.core.dom.ReturnStatement returnStatement
|
||||||
|
&& returnStatement.getExpression() instanceof MethodInvocation invocation) {
|
||||||
|
return invocation;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new AssertionError("Return MethodInvocation not found in " + methodName);
|
||||||
|
}
|
||||||
|
|
||||||
|
private record RunnableProject(CodebaseContext context) {
|
||||||
|
|
||||||
|
static RunnableProject supplierOnly(Path tempDir) throws IOException {
|
||||||
|
Path root = tempDir.resolve("supplier");
|
||||||
|
Files.createDirectories(root);
|
||||||
|
writeSharedSources(root, false);
|
||||||
|
return new RunnableProject(scanRoot(root));
|
||||||
|
}
|
||||||
|
|
||||||
|
static RunnableProject withExecutor(Path tempDir) throws IOException {
|
||||||
|
Path root = tempDir.resolve("runnable");
|
||||||
|
Files.createDirectories(root);
|
||||||
|
writeSharedSources(root, true);
|
||||||
|
return new RunnableProject(scanRoot(root));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static CodebaseContext scanRoot(Path root) throws IOException {
|
||||||
|
CodebaseContext context = new CodebaseContext();
|
||||||
|
context.setResolveBindings(true);
|
||||||
|
context.scan(root);
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void writeSharedSources(Path root, boolean withExecutor) throws IOException {
|
||||||
|
Files.writeString(root.resolve("OrderCommand.java"), """
|
||||||
|
package com.example.domain;
|
||||||
|
public enum OrderCommand { PAY, SHIP, META }
|
||||||
|
""");
|
||||||
|
if (withExecutor) {
|
||||||
|
Files.writeString(root.resolve("TaskExecutor.java"), """
|
||||||
|
package com.example.executor;
|
||||||
|
public class TaskExecutor {
|
||||||
|
public void execute(Runnable task) { task.run(); }
|
||||||
|
}
|
||||||
|
""");
|
||||||
|
Files.writeString(root.resolve("OrderController.java"), """
|
||||||
|
package com.example.web;
|
||||||
|
import com.example.executor.TaskExecutor;
|
||||||
|
import com.example.service.AbstractOrderService;
|
||||||
|
public class OrderController {
|
||||||
|
private final TaskExecutor executor;
|
||||||
|
private final AbstractOrderService service;
|
||||||
|
public OrderController(TaskExecutor executor, AbstractOrderService service) {
|
||||||
|
this.executor = executor;
|
||||||
|
this.service = service;
|
||||||
|
}
|
||||||
|
public void process() {
|
||||||
|
executor.execute(() -> service.fireEvent());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""");
|
||||||
|
Files.writeString(root.resolve("AbstractOrderService.java"), """
|
||||||
|
package com.example.service;
|
||||||
|
import com.example.domain.OrderCommand;
|
||||||
|
public abstract class AbstractOrderService {
|
||||||
|
protected com.example.machine.StateMachine machine;
|
||||||
|
public void fireEvent() { sendEvent(OrderCommand.PAY); }
|
||||||
|
protected void sendEvent(OrderCommand event) { machine.fire(event); }
|
||||||
|
}
|
||||||
|
""");
|
||||||
|
Files.writeString(root.resolve("StateMachine.java"), """
|
||||||
|
package com.example.machine;
|
||||||
|
import com.example.domain.OrderCommand;
|
||||||
|
public class StateMachine {
|
||||||
|
public void fire(OrderCommand event) {}
|
||||||
|
}
|
||||||
|
""");
|
||||||
|
} else {
|
||||||
|
Files.writeString(root.resolve("OrderController.java"), """
|
||||||
|
package com.example.web;
|
||||||
|
import com.example.service.OrderService;
|
||||||
|
import com.example.domain.OrderCommand;
|
||||||
|
public class OrderController {
|
||||||
|
private final OrderService service;
|
||||||
|
public OrderController(OrderService service) { this.service = service; }
|
||||||
|
public void process() {
|
||||||
|
service.fireEvent(() -> OrderCommand.PAY);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""");
|
||||||
|
Files.writeString(root.resolve("OrderService.java"), """
|
||||||
|
package com.example.service;
|
||||||
|
import com.example.domain.OrderCommand;
|
||||||
|
import com.example.machine.StateMachine;
|
||||||
|
import java.util.function.Supplier;
|
||||||
|
public class OrderService {
|
||||||
|
private StateMachine machine;
|
||||||
|
public void fireEvent(Supplier<OrderCommand> provider) {
|
||||||
|
machine.fire(provider.get());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""");
|
||||||
|
Files.writeString(root.resolve("StateMachine.java"), """
|
||||||
|
package com.example.machine;
|
||||||
|
import com.example.domain.OrderCommand;
|
||||||
|
public class StateMachine {
|
||||||
|
public void fire(OrderCommand event) {}
|
||||||
|
}
|
||||||
|
""");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user