diff --git a/build.gradle b/build.gradle index fbadaa0b..97c47ce9 100644 --- a/build.gradle +++ b/build.gradle @@ -291,16 +291,6 @@ subprojects { } } - apply plugin: 'maven-publish' - - publishing { - publications { - maven(MavenPublication) { - from components.java - } - } - } - } boolean isRelease(Project project) { diff --git a/gradle/version.gradle b/gradle/version.gradle index 58fc5f8d..6d30d5dc 100644 --- a/gradle/version.gradle +++ b/gradle/version.gradle @@ -1,6 +1,6 @@ import java.util.regex.Pattern -version = "2.3.24-snapshot" +version = "2.3.24" logger.lifecycle("Project version: $version") String detectSemVersion() { diff --git a/xm-commons-flow/build.gradle b/xm-commons-flow/build.gradle index 70e502dc..d2aeb21c 100644 --- a/xm-commons-flow/build.gradle +++ b/xm-commons-flow/build.gradle @@ -11,6 +11,7 @@ dependencies { compile project(":xm-commons-config") compile project(":xm-commons-i18n") compile project(":xm-commons-lep") + compile project(":xm-commons-permission") implementation("com.github.java-json-tools:json-schema-validator:2.2.8") { exclude group: 'javax.mail', module: 'mailapi' diff --git a/xm-commons-flow/src/main/java/com/icthh/xm/commons/flow/api/Condition.java b/xm-commons-flow/src/main/java/com/icthh/xm/commons/flow/api/Condition.java index f914ba4e..ce02fe1f 100644 --- a/xm-commons-flow/src/main/java/com/icthh/xm/commons/flow/api/Condition.java +++ b/xm-commons-flow/src/main/java/com/icthh/xm/commons/flow/api/Condition.java @@ -2,6 +2,10 @@ import com.icthh.xm.commons.lep.api.BaseLepContext; -public interface Condition { +public interface Condition extends Action { + @Override + default Boolean execute(T lepContext) { + return test(lepContext); + } Boolean test(T lepContext); } diff --git a/xm-commons-flow/src/main/java/com/icthh/xm/commons/flow/api/FlowLepContextFields.java b/xm-commons-flow/src/main/java/com/icthh/xm/commons/flow/api/FlowLepContextFields.java index 3c51f2a5..505b2c40 100644 --- a/xm-commons-flow/src/main/java/com/icthh/xm/commons/flow/api/FlowLepContextFields.java +++ b/xm-commons-flow/src/main/java/com/icthh/xm/commons/flow/api/FlowLepContextFields.java @@ -1,6 +1,14 @@ package com.icthh.xm.commons.flow.api; +import com.icthh.xm.commons.flow.context.FlowLepAdditionalContext.FlowLepAdditionalContextField; +import com.icthh.xm.commons.flow.context.StepLepAdditionalContext.StepLepAdditionalContextField; +import com.icthh.xm.commons.flow.context.StepsLepAdditionalContext.StepsLepAdditionalContextField; import com.icthh.xm.commons.flow.context.TenantResourceLepAdditionalContext.TenantResourceLepAdditionalContextField; -public interface FlowLepContextFields extends TenantResourceLepAdditionalContextField { +public interface FlowLepContextFields extends + TenantResourceLepAdditionalContextField, + StepLepAdditionalContextField, + StepsLepAdditionalContextField, + FlowLepAdditionalContextField +{ } diff --git a/xm-commons-flow/src/main/java/com/icthh/xm/commons/flow/context/FlowLepAdditionalContext.java b/xm-commons-flow/src/main/java/com/icthh/xm/commons/flow/context/FlowLepAdditionalContext.java new file mode 100644 index 00000000..37b8e9c2 --- /dev/null +++ b/xm-commons-flow/src/main/java/com/icthh/xm/commons/flow/context/FlowLepAdditionalContext.java @@ -0,0 +1,80 @@ +package com.icthh.xm.commons.flow.context; + +import com.icthh.xm.commons.flow.engine.context.FlowExecutionContext; +import com.icthh.xm.commons.flow.context.FlowLepAdditionalContext.FlowContext; +import com.icthh.xm.commons.flow.service.FlowService; +import com.icthh.xm.commons.lep.TargetProceedingLep; +import com.icthh.xm.commons.lep.api.BaseLepContext; +import com.icthh.xm.commons.lep.api.LepAdditionalContext; +import com.icthh.xm.commons.lep.api.LepAdditionalContextField; +import com.icthh.xm.commons.lep.api.LepBaseKey; +import com.icthh.xm.commons.lep.api.LepEngine; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +import java.util.Optional; +import java.util.Set; + +import static com.icthh.xm.commons.flow.context.FlowLepAdditionalContext.FlowLepAdditionalContextField.FLOW; +import static com.icthh.xm.commons.flow.engine.StepExecutorService.ACTION; +import static com.icthh.xm.commons.flow.engine.StepExecutorService.CONDITION; +import static com.icthh.xm.commons.flow.engine.StepExecutorService.FLOW_STEP_GROUP; +import static com.icthh.xm.commons.flow.engine.StepExecutorService.STEP_CLASS_EXECUTOR; + +@Component +@RequiredArgsConstructor +public class FlowLepAdditionalContext implements LepAdditionalContext { + + private static final Set STEP_LEP = Set.of(ACTION, CONDITION, STEP_CLASS_EXECUTOR); + + private final FlowService flowService; + + @Override + public String additionalContextKey() { + return FLOW; + } + + @Override + public FlowContext additionalContextValue() { + return null; + } + + @Override + public Optional additionalContextValue(BaseLepContext lepContext, LepEngine lepEngine, TargetProceedingLep lepMethod) { + LepBaseKey lepBaseKey = lepMethod.getLepBaseKey(); + if (FLOW_STEP_GROUP.equals(lepBaseKey.getGroup()) && STEP_LEP.contains(lepBaseKey.getBaseKey())) { + FlowExecutionContext context = lepMethod.getParameter("context", FlowExecutionContext.class); + return Optional.of(new FlowContext(context.getInput(), flowService)); + } else { + return Optional.of(new FlowContext(null, flowService)); + } + } + + @Override + public Class fieldAccessorInterface() { + return FlowLepAdditionalContextField.class; + } + + public interface FlowLepAdditionalContextField extends LepAdditionalContextField { + String FLOW = "flow"; + default FlowContext getFlow() { + return (FlowContext)get(FLOW); + } + } + + public static class FlowContext { + public final Object input; + public final Object context; + private final FlowService flowService; + + public FlowContext(Object input, FlowService flowService) { + this.input = input; + this.context = input; + this.flowService = flowService; + } + + public FlowExecutionContext executeFlow(String flowKey, Object input) { + return flowService.runFlowInternal(flowKey, input); + } + } +} diff --git a/xm-commons-flow/src/main/java/com/icthh/xm/commons/flow/context/StepLepAdditionalContext.java b/xm-commons-flow/src/main/java/com/icthh/xm/commons/flow/context/StepLepAdditionalContext.java new file mode 100644 index 00000000..1a41a7c4 --- /dev/null +++ b/xm-commons-flow/src/main/java/com/icthh/xm/commons/flow/context/StepLepAdditionalContext.java @@ -0,0 +1,104 @@ +package com.icthh.xm.commons.flow.context; + +import com.icthh.xm.commons.flow.context.StepLepAdditionalContext.StepContext; +import com.icthh.xm.commons.flow.domain.Step; +import com.icthh.xm.commons.flow.engine.context.FlowExecutionContext; +import com.icthh.xm.commons.flow.service.CodeSnippetExecutor; +import com.icthh.xm.commons.lep.TargetProceedingLep; +import com.icthh.xm.commons.lep.api.BaseLepContext; +import com.icthh.xm.commons.lep.api.LepAdditionalContext; +import com.icthh.xm.commons.lep.api.LepAdditionalContextField; +import com.icthh.xm.commons.lep.api.LepBaseKey; +import com.icthh.xm.commons.lep.api.LepEngine; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; + +import static com.icthh.xm.commons.config.client.utils.Utils.nullSafeMap; +import static com.icthh.xm.commons.flow.context.StepLepAdditionalContext.StepLepAdditionalContextField.STEP; +import static com.icthh.xm.commons.flow.engine.StepExecutorService.ACTION; +import static com.icthh.xm.commons.flow.engine.StepExecutorService.CONDITION; +import static com.icthh.xm.commons.flow.engine.StepExecutorService.FLOW_STEP_GROUP; +import static com.icthh.xm.commons.flow.engine.StepExecutorService.STEP_CLASS_EXECUTOR; + +@Component +@RequiredArgsConstructor +public class StepLepAdditionalContext implements LepAdditionalContext { + + private static final Set STEP_LEP = Set.of(ACTION, CONDITION, STEP_CLASS_EXECUTOR); + + private final CodeSnippetExecutor snippetExecutor; + + @Override + public String additionalContextKey() { + return STEP; + } + + @Override + public StepContext additionalContextValue() { + return null; + } + + @Override + public Optional additionalContextValue(BaseLepContext lepContext, LepEngine lepEngine, TargetProceedingLep lepMethod) { + LepBaseKey lepBaseKey = lepMethod.getLepBaseKey(); + if (FLOW_STEP_GROUP.equals(lepBaseKey.getGroup()) && STEP_LEP.contains(lepBaseKey.getBaseKey())) { + FlowExecutionContext context = lepMethod.getParameter("context", FlowExecutionContext.class); + Step step = lepMethod.getParameter("step", Step.class); + return Optional.of(new StepContext(context, step, snippetExecutor)); + } else { + return Optional.empty(); + } + } + + @Override + public Class fieldAccessorInterface() { + return StepLepAdditionalContextField.class; + } + + public interface StepLepAdditionalContextField extends LepAdditionalContextField { + String STEP = "step"; + default StepContext getStep() { + return (StepContext)get(STEP); + } + } + + public static class StepContext { + public final Object input; + public final Object context; + public final Map parameters; + + public final Integer iteration; + public final Object iterationItem; + public final List iterationsInput; + public final List iterationsOutput; + + private final String flowKey; + private final String stepKey; + private final CodeSnippetExecutor snippetExecutor; + + public StepContext(FlowExecutionContext context, Step step, CodeSnippetExecutor snippetExecutor) { + var input = context.getStepInput().get(step.getKey()); + this.input = input; + this.context = input; + + this.iteration = context.getIteration(); + this.iterationItem = context.getIterationItem(); + this.iterationsInput = context.getIterationsInput(); + this.iterationsOutput = context.getIterationsOutput(); + + this.flowKey = context.getFlowKey(); + this.stepKey = step.getKey(); + this.parameters = Map.copyOf(nullSafeMap(step.getParameters())); + this.snippetExecutor = snippetExecutor; + } + + public Object runSnippet(String snippetKey, BaseLepContext lepContext) { + return snippetExecutor.runCodeSnippet(lepContext, List.of(flowKey, stepKey, snippetKey)); + } + } +} diff --git a/xm-commons-flow/src/main/java/com/icthh/xm/commons/flow/context/StepsLepAdditionalContext.java b/xm-commons-flow/src/main/java/com/icthh/xm/commons/flow/context/StepsLepAdditionalContext.java new file mode 100644 index 00000000..97ea5dbd --- /dev/null +++ b/xm-commons-flow/src/main/java/com/icthh/xm/commons/flow/context/StepsLepAdditionalContext.java @@ -0,0 +1,152 @@ +package com.icthh.xm.commons.flow.context; + +import com.icthh.xm.commons.flow.context.StepsLepAdditionalContext.StepsContext; +import com.icthh.xm.commons.flow.engine.context.FlowExecutionContext; +import com.icthh.xm.commons.lep.TargetProceedingLep; +import com.icthh.xm.commons.lep.api.BaseLepContext; +import com.icthh.xm.commons.lep.api.LepAdditionalContext; +import com.icthh.xm.commons.lep.api.LepAdditionalContextField; +import com.icthh.xm.commons.lep.api.LepBaseKey; +import com.icthh.xm.commons.lep.api.LepEngine; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Optional; +import java.util.Set; + +import static com.icthh.xm.commons.flow.context.StepsLepAdditionalContext.StepsLepAdditionalContextField.STEPS; +import static com.icthh.xm.commons.flow.engine.StepExecutorService.ACTION; +import static com.icthh.xm.commons.flow.engine.StepExecutorService.CONDITION; +import static com.icthh.xm.commons.flow.engine.StepExecutorService.FLOW_STEP_GROUP; +import static com.icthh.xm.commons.flow.engine.StepExecutorService.STEP_CLASS_EXECUTOR; + +@Component +@RequiredArgsConstructor +public class StepsLepAdditionalContext implements LepAdditionalContext { + + private static final Set STEP_LEP = Set.of(ACTION, CONDITION, STEP_CLASS_EXECUTOR); + + @Override + public String additionalContextKey() { + return STEPS; + } + + @Override + public StepsContext additionalContextValue() { + return null; + } + + @Override + public Optional additionalContextValue(BaseLepContext lepContext, LepEngine lepEngine, TargetProceedingLep lepMethod) { + LepBaseKey lepBaseKey = lepMethod.getLepBaseKey(); + if (FLOW_STEP_GROUP.equals(lepBaseKey.getGroup()) && STEP_LEP.contains(lepBaseKey.getBaseKey())) { + FlowExecutionContext context = lepMethod.getParameter("context", FlowExecutionContext.class); + return Optional.of(new StepsContext(context)); + } else { + return Optional.empty(); + } + } + + @Override + public Class fieldAccessorInterface() { + return StepsLepAdditionalContextField.class; + } + + public interface StepsLepAdditionalContextField extends LepAdditionalContextField { + String STEPS = "steps"; + default StepsContext getSteps() { + return (StepsContext)get(STEPS); + } + } + + public static class StepLog { + public Object input; + public Object output; + } + + public static class StepsContext implements Map { + public final Map delegate; + + public StepsContext(FlowExecutionContext context) { + Set keys = new HashSet<>(); + keys.addAll(context.getStepInput().keySet()); + keys.addAll(context.getStepOutput().keySet()); + + Map steps = new HashMap<>(); + keys.forEach(key -> { + StepLog stepLog = new StepLog(); + stepLog.input = context.getStepInput().get(key); + stepLog.output = context.getStepOutput().get(key); + steps.put(key, stepLog); + }); + + this.delegate = Map.copyOf(steps); + } + + @Override + public int size() { + return delegate.size(); + } + + @Override + public boolean isEmpty() { + return delegate.isEmpty(); + } + + @Override + public boolean containsKey(Object key) { + return delegate.containsKey(key); + } + + @Override + public boolean containsValue(Object value) { + return delegate.containsValue(value); + } + + @Override + public StepLog get(Object key) { + return delegate.get(key); + } + + @Override + public StepLog put(String key, StepLog value) { + throw new UnsupportedOperationException(); + } + + @Override + public StepLog remove(Object key) { + throw new UnsupportedOperationException(); + } + + @Override + public void putAll(Map m) { + throw new UnsupportedOperationException(); + } + + @Override + public void clear() { + delegate.clear(); + } + + @Override + public Set keySet() { + return delegate.keySet(); + } + + @Override + public Collection values() { + return delegate.values(); + } + + @Override + public Set> entrySet() { + return delegate.entrySet(); + } + + } + +} diff --git a/xm-commons-flow/src/main/java/com/icthh/xm/commons/flow/context/TenantResourceLepAdditionalContext.java b/xm-commons-flow/src/main/java/com/icthh/xm/commons/flow/context/TenantResourceLepAdditionalContext.java index 94adc1b6..6ece0b4b 100644 --- a/xm-commons-flow/src/main/java/com/icthh/xm/commons/flow/context/TenantResourceLepAdditionalContext.java +++ b/xm-commons-flow/src/main/java/com/icthh/xm/commons/flow/context/TenantResourceLepAdditionalContext.java @@ -8,7 +8,7 @@ import java.util.Map; -import static com.icthh.xm.commons.flow.context.TenantResourceLepAdditionalContext.TenantResourceLepAdditionalContextField.FIELD_NAME; +import static com.icthh.xm.commons.flow.context.TenantResourceLepAdditionalContext.TenantResourceLepAdditionalContextField.TENANT_RESOURCE; @Component @RequiredArgsConstructor @@ -18,7 +18,7 @@ public class TenantResourceLepAdditionalContext implements LepAdditionalContext< @Override public String additionalContextKey() { - return FIELD_NAME; + return TENANT_RESOURCE; } @Override @@ -32,9 +32,9 @@ public Class fieldAccessorInterface() { } public interface TenantResourceLepAdditionalContextField extends LepAdditionalContextField { - String FIELD_NAME = "resources"; + String TENANT_RESOURCE = "resources"; default Map>> getResources() { - return (Map>>)get(FIELD_NAME); + return (Map>>)get(TENANT_RESOURCE); } } } diff --git a/xm-commons-flow/src/main/java/com/icthh/xm/commons/flow/domain/Action.java b/xm-commons-flow/src/main/java/com/icthh/xm/commons/flow/domain/Action.java index 2e21f593..2b40668e 100644 --- a/xm-commons-flow/src/main/java/com/icthh/xm/commons/flow/domain/Action.java +++ b/xm-commons-flow/src/main/java/com/icthh/xm/commons/flow/domain/Action.java @@ -11,5 +11,11 @@ public class Action extends Step { private Boolean isIterable; private String iterableJsonPath; private Boolean skipIterableJsonPathError; - private List next; + private Boolean removeNullOutputForIterableResult; + private String next; + + @Override + public String getNext(Object context) { + return next; + } } diff --git a/xm-commons-flow/src/main/java/com/icthh/xm/commons/flow/domain/Condition.java b/xm-commons-flow/src/main/java/com/icthh/xm/commons/flow/domain/Condition.java index 1b29c953..ef5a747c 100644 --- a/xm-commons-flow/src/main/java/com/icthh/xm/commons/flow/domain/Condition.java +++ b/xm-commons-flow/src/main/java/com/icthh/xm/commons/flow/domain/Condition.java @@ -3,11 +3,14 @@ import lombok.Data; import lombok.EqualsAndHashCode; -import java.util.List; - @Data @EqualsAndHashCode(callSuper = true) public class Condition extends Step { - private List nextOnConditionTrue; - private List nextOnConditionFalse; + private String nextOnConditionTrue; + private String nextOnConditionFalse; + + @Override + public String getNext(Object context) { + return Boolean.TRUE.equals(context) ? nextOnConditionTrue : nextOnConditionFalse; + } } diff --git a/xm-commons-flow/src/main/java/com/icthh/xm/commons/flow/domain/Flow.java b/xm-commons-flow/src/main/java/com/icthh/xm/commons/flow/domain/Flow.java index c2b67a24..7c24399a 100644 --- a/xm-commons-flow/src/main/java/com/icthh/xm/commons/flow/domain/Flow.java +++ b/xm-commons-flow/src/main/java/com/icthh/xm/commons/flow/domain/Flow.java @@ -3,6 +3,7 @@ import com.icthh.xm.commons.config.client.api.refreshable.ConfigWithKey; import lombok.Data; +import javax.validation.constraints.NotBlank; import java.util.List; @Data @@ -10,6 +11,8 @@ public class Flow implements ConfigWithKey { private String key; private String version; private String description; + @NotBlank + private String startStep; private List steps; private Trigger trigger; } diff --git a/xm-commons-flow/src/main/java/com/icthh/xm/commons/flow/domain/Step.java b/xm-commons-flow/src/main/java/com/icthh/xm/commons/flow/domain/Step.java index b32661cb..cafc97f2 100644 --- a/xm-commons-flow/src/main/java/com/icthh/xm/commons/flow/domain/Step.java +++ b/xm-commons-flow/src/main/java/com/icthh/xm/commons/flow/domain/Step.java @@ -22,11 +22,12 @@ public abstract class Step { private String key; private String typeKey; - private List depends; private Map parameters; private Map snippets; private StepSpec.StepType type; + public abstract String getNext(Object context); + @Data public static class Snippet { private String content; diff --git a/xm-commons-flow/src/main/java/com/icthh/xm/commons/flow/engine/FlowExecutorService.java b/xm-commons-flow/src/main/java/com/icthh/xm/commons/flow/engine/FlowExecutorService.java new file mode 100644 index 00000000..c05cafa0 --- /dev/null +++ b/xm-commons-flow/src/main/java/com/icthh/xm/commons/flow/engine/FlowExecutorService.java @@ -0,0 +1,140 @@ +package com.icthh.xm.commons.flow.engine; + +import com.google.common.collect.Lists; +import com.icthh.xm.commons.flow.domain.Action; +import com.icthh.xm.commons.flow.domain.Condition; +import com.icthh.xm.commons.flow.domain.Flow; +import com.icthh.xm.commons.flow.domain.Step; +import com.icthh.xm.commons.flow.engine.context.FlowExecutionContext; +import com.jayway.jsonpath.JsonPath; +import com.jayway.jsonpath.JsonPathException; +import lombok.RequiredArgsConstructor; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.time.StopWatch; +import org.springframework.stereotype.Service; + +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +import static java.lang.Boolean.TRUE; +import static java.util.concurrent.TimeUnit.MICROSECONDS; +import static java.util.function.Function.identity; +import static java.util.stream.Collectors.toList; +import static java.util.stream.Collectors.toMap; +import static java.util.stream.IntStream.range; +import static org.apache.commons.lang3.StringUtils.isNotBlank; + +@Slf4j +@Service +@RequiredArgsConstructor +public class FlowExecutorService { + + private final StepExecutorService stepExecutorService; + + public FlowExecutionContext execute(Flow flow, Object input) { + FlowExecutionContext context = new FlowExecutionContext(flow.getKey(), input); + Map steps = flow.getSteps().stream().collect(toMap(Step::getKey, identity())); + String lastActionKey = null; + try { + + Step currentStep = steps.get(flow.getStartStep()); + while(currentStep != null) { + lastActionKey = currentStep instanceof Action ? currentStep.getKey() : lastActionKey; + input = runStep(currentStep, input, context); + currentStep = getNextStep(currentStep, input, steps); + } + + context.setOutput(context.getStepOutput().get(lastActionKey)); + return context; + } catch (Throwable e) { + log.error("Error execute flow with error {} | executionContext: {}", flow.getKey(), context, e); + throw e; + } + } + + private Object runStep(Step currentStep, Object input, FlowExecutionContext context) { + StopWatch stopWatch = StopWatch.createStarted(); + log.debug("Execute step: {} with input: {}", currentStep.getKey(), input); + + context.getStepInput().put(currentStep.getKey(), input); + Object result = executeStep(currentStep, input, context); + context.getStepOutput().put(currentStep.getKey(), result); + + log.debug("Step: {} executed with result: {}, {}ms", currentStep.getKey(), result, stopWatch.getTime(MICROSECONDS)); + return result; + } + + private Object executeStep(Step currentStep, Object input, FlowExecutionContext context) { + // TODO refactor this to some pattern that follow open close principle + if (currentStep instanceof Action) { + Action action = (Action) currentStep; + if (TRUE.equals(action.getIsIterable())) { + return executeIterableAction(action, input, context); + } + return stepExecutorService.executeAction(input, action, context); + } else if (currentStep instanceof Condition) { + return stepExecutorService.executeCondition(input, (Condition) currentStep, context); + } else { + throw new IllegalArgumentException("Unsupported step type: " + currentStep.getClass()); + } + } + + @SneakyThrows + private Object executeIterableAction(Action action, Object input, FlowExecutionContext context) { + List items = readActionArray(action, input); + context.resetIteration(); + context.setIterationsInput(items); + for (int i = 0; i < items.size(); i++) { + Object iterationItem = items.get(i); + context.setIteration(i); + context.setIterationItem(iterationItem); + + Object result = stepExecutorService.executeAction(iterationItem, action, context); + context.getIterationsOutput().add(result); + } + + List output = context.getIterationsOutput(); + if (TRUE.equals(action.getRemoveNullOutputForIterableResult())) { + output = output.stream().filter(Objects::nonNull).collect(toList()); + } + + context.resetIteration(); + return output; + } + + private static List readActionArray(Action action, Object input) { + try { + Object value = JsonPath.read(input, action.getIterableJsonPath()); + if (value instanceof Number) { + Number iterations = (Number) value; + return range(0, iterations.intValue()).boxed().collect(toList()); + } else if (value instanceof List) { + return (List) value; + } else if (value.getClass().isArray()) { + return Arrays.asList((Object[]) value); + } else { + Iterable iterable = (Iterable) value; + return Lists.newArrayList(iterable); + } + } catch (JsonPathException e) { + log.error("Error read from action {} by json path {}", action, action.getIterableJsonPath(), e); + if (TRUE.equals(action.getSkipIterableJsonPathError())) { + return List.of(); + } + throw e; + } + } + + private Step getNextStep(Step currentStep, Object result, Map steps) { + String nextStep = currentStep.getNext(result); + currentStep = nextStep != null ? steps.get(nextStep): null; + if (currentStep == null && isNotBlank(nextStep)) { + log.error("Step for key: {} not found", nextStep); + } + return currentStep; + } + +} diff --git a/xm-commons-flow/src/main/java/com/icthh/xm/commons/flow/engine/StepExecutorService.java b/xm-commons-flow/src/main/java/com/icthh/xm/commons/flow/engine/StepExecutorService.java new file mode 100644 index 00000000..9fd08fa8 --- /dev/null +++ b/xm-commons-flow/src/main/java/com/icthh/xm/commons/flow/engine/StepExecutorService.java @@ -0,0 +1,57 @@ +package com.icthh.xm.commons.flow.engine; + +import com.icthh.xm.commons.flow.domain.Action; +import com.icthh.xm.commons.flow.domain.Condition; +import com.icthh.xm.commons.flow.domain.Step; +import com.icthh.xm.commons.flow.engine.context.FlowExecutionContext; +import com.icthh.xm.commons.flow.service.resolver.StepKeyResolver; +import com.icthh.xm.commons.flow.spec.step.StepSpec; +import com.icthh.xm.commons.flow.spec.step.StepSpecService; +import com.icthh.xm.commons.lep.LogicExtensionPoint; +import com.icthh.xm.commons.lep.spring.LepService; +import lombok.RequiredArgsConstructor; +import org.apache.commons.lang3.NotImplementedException; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import static java.lang.Boolean.TRUE; + +@Component +@LepService(group = StepExecutorService.FLOW_STEP_GROUP) +@RequiredArgsConstructor +public class StepExecutorService { + + public static final String ACTION = "Action"; + public static final String CONDITION = "Condition"; + public static final String STEP_CLASS_EXECUTOR = "StepClassExecutor"; + public static final String FLOW_STEP_GROUP = "flow.step"; + + private final StepSpecService stepSpecService; + + private StepExecutorService self; + + @LogicExtensionPoint(value = ACTION, resolver = StepKeyResolver.class) + public Object executeAction(Object input, Action step, FlowExecutionContext context) { + return self.executeStepByClassImpl(stepSpecService.getStepSpec(step.getTypeKey()), input, step, context); + } + + @LogicExtensionPoint(value = CONDITION, resolver = StepKeyResolver.class) + public Boolean executeCondition(Object input, Condition step, FlowExecutionContext context) { + return TRUE.equals(self.executeStepByClassImpl(stepSpecService.getStepSpec(step.getTypeKey()), input, step, context)); + } + + @LogicExtensionPoint(value = STEP_CLASS_EXECUTOR) + public Object executeStepByClassImpl(StepSpec stepSpec, Object input, Step step, FlowExecutionContext context) { + if (StringUtils.isBlank(stepSpec.getImplementation())) { + throw new IllegalArgumentException("Step implementation is not defined for step: " + step.getTypeKey()); + } + throw new NotImplementedException("Error resolve step. Pls check support default groovy lep."); + } + + @Autowired + public void setSelf(StepExecutorService self) { + this.self = self; + } + +} diff --git a/xm-commons-flow/src/main/java/com/icthh/xm/commons/flow/engine/context/FlowExecutionContext.java b/xm-commons-flow/src/main/java/com/icthh/xm/commons/flow/engine/context/FlowExecutionContext.java new file mode 100644 index 00000000..eacf48e3 --- /dev/null +++ b/xm-commons-flow/src/main/java/com/icthh/xm/commons/flow/engine/context/FlowExecutionContext.java @@ -0,0 +1,30 @@ +package com.icthh.xm.commons.flow.engine.context; + +import lombok.Data; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@Data +public class FlowExecutionContext { + + private final String flowKey; + private final Object input; + private Object output; + private final Map stepInput = new HashMap<>(); + private final Map stepOutput = new HashMap<>(); + + private Integer iteration; + private Object iterationItem; + private List iterationsInput; + private List iterationsOutput; + + public void resetIteration() { + iteration = null; + iterationItem = null; + iterationsInput = new ArrayList<>(); + iterationsOutput = new ArrayList<>(); + } +} diff --git a/xm-commons-flow/src/main/java/com/icthh/xm/commons/flow/rest/FlowExecuteResource.java b/xm-commons-flow/src/main/java/com/icthh/xm/commons/flow/rest/FlowExecuteResource.java new file mode 100644 index 00000000..bc6831f9 --- /dev/null +++ b/xm-commons-flow/src/main/java/com/icthh/xm/commons/flow/rest/FlowExecuteResource.java @@ -0,0 +1,58 @@ +package com.icthh.xm.commons.flow.rest; + +import com.icthh.xm.commons.flow.service.FlowService; +import com.icthh.xm.commons.permission.annotation.PrivilegeDescription; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.util.Map; + +@RestController +@RequestMapping("/api/flow") +@RequiredArgsConstructor +public class FlowExecuteResource { + + private final FlowService flowService; + + @GetMapping("/{flowKey}/execute") + @PreAuthorize("hasPermission({'flowKey': #flowKey, 'queryParams': #queryParams}, 'FLOW.EXECUTE.GET')") + @PrivilegeDescription("Privilege to execute the flow by key using get method") + public Object executeFlowGet(@PathVariable("flowKey") String flowKey, @RequestParam Map queryParams) { + return executeFlow(flowKey, queryParams); + } + + @PutMapping("/{flowKey}/execute") + @PreAuthorize("hasPermission({'flowKey': #flowKey, 'body': #body, 'queryParams': #queryParams}, 'FLOW.EXECUTE.PUT')") + @PrivilegeDescription("Privilege to execute the flow by key using put method") + public Object executeFlowPut(@PathVariable String flowKey, @RequestBody Map body) { + return executeFlow(flowKey, body); + } + + @PostMapping(path = "/{flowKey}/execute", consumes = "application/json") + @PreAuthorize("hasPermission({'flowKey': #flowKey, 'body': #body, 'queryParams': #queryParams}, 'FLOW.EXECUTE.POST_JSON')") + @PrivilegeDescription("Privilege to execute the flow by key using post json method") + public Object executeFlowPostJson(@PathVariable String flowKey, @RequestBody Map body) { + return executeFlow(flowKey, body); + } + + @PostMapping(path = "/{flowKey}/execute", consumes = "application/x-www-form-urlencoded") + @PreAuthorize("hasPermission({'flowKey': #flowKey, 'formData': #formData}, 'FLOW.EXECUTE.POST_FORM')") + @PrivilegeDescription("Privilege to execute the flow by key using post url encoded method") + public Object executeFlowPostUrlEncoded(@PathVariable String flowKey, @RequestParam Map formData) { + return executeFlow(flowKey, formData); + } + + private Object executeFlow(String flowKey, Object body) { + return flowService.runFlow(flowKey, body); + } + +} diff --git a/xm-commons-flow/src/main/java/com/icthh/xm/commons/flow/rest/FlowResource.java b/xm-commons-flow/src/main/java/com/icthh/xm/commons/flow/rest/FlowResource.java index 39bd3d39..b31c4c68 100644 --- a/xm-commons-flow/src/main/java/com/icthh/xm/commons/flow/rest/FlowResource.java +++ b/xm-commons-flow/src/main/java/com/icthh/xm/commons/flow/rest/FlowResource.java @@ -2,7 +2,10 @@ import com.icthh.xm.commons.flow.domain.Flow; import com.icthh.xm.commons.flow.service.FlowService; +import com.icthh.xm.commons.permission.annotation.PrivilegeDescription; import lombok.RequiredArgsConstructor; +import org.springframework.security.access.prepost.PostAuthorize; +import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; @@ -22,26 +25,36 @@ public class FlowResource { private final FlowService flowService; @GetMapping("/{flowKey}") + @PostAuthorize("hasPermission({'returnObject': returnObject.body}, 'FLOW.GET_ITEM')") + @PrivilegeDescription("Privilege to get the flow by key") public Flow getFlow(@PathVariable("flowKey") String flowKey) { return flowService.getFlow(flowKey); } @GetMapping() + @PostAuthorize("hasPermission({'returnObject': returnObject.body}, 'FLOW.GET_LIST')") + @PrivilegeDescription("Privilege to get all flows") public List getFlows() { return flowService.getFlows(); } @PostMapping() + @PreAuthorize("hasPermission({'flow': #flow}, 'FLOW.CREATE')") + @PrivilegeDescription("Privilege to create a new flow") public void createFlow(@RequestBody Flow flow) { flowService.createFlow(flow); } @PutMapping() + @PreAuthorize("hasPermission({'flow': #flow}, 'FLOW.UPDATE')") + @PrivilegeDescription("Privilege to update the flow") public void updateFlow(@RequestBody Flow flow) { flowService.updateFlow(flow); } @DeleteMapping("/{flowKey}") + @PreAuthorize("hasPermission({'flowKey': #flowKey}, 'FLOW.DELETE')") + @PrivilegeDescription("Privilege to delete the flow") public void deleteFlow(@PathVariable("flowKey") String flowKey) { flowService.deleteFlow(flowKey); } diff --git a/xm-commons-flow/src/main/java/com/icthh/xm/commons/flow/rest/FlowSpecResource.java b/xm-commons-flow/src/main/java/com/icthh/xm/commons/flow/rest/FlowSpecResource.java index 1715d319..277385bf 100644 --- a/xm-commons-flow/src/main/java/com/icthh/xm/commons/flow/rest/FlowSpecResource.java +++ b/xm-commons-flow/src/main/java/com/icthh/xm/commons/flow/rest/FlowSpecResource.java @@ -3,11 +3,12 @@ import com.icthh.xm.commons.flow.spec.resource.TenantResourceType; import com.icthh.xm.commons.flow.spec.resource.TenantResourceTypeService; import com.icthh.xm.commons.flow.spec.step.StepSpec; -import com.icthh.xm.commons.flow.spec.step.StepSpec.StepType; import com.icthh.xm.commons.flow.spec.step.StepSpecService; import com.icthh.xm.commons.flow.spec.trigger.TriggerType; import com.icthh.xm.commons.flow.spec.trigger.TriggerTypeSpecService; +import com.icthh.xm.commons.permission.annotation.PrivilegeDescription; import lombok.RequiredArgsConstructor; +import org.springframework.security.access.prepost.PostAuthorize; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; @@ -25,16 +26,22 @@ public class FlowSpecResource { private final TriggerTypeSpecService triggerSpecService; @GetMapping("/steps") - public List getSteps(@RequestParam(name = "stepType", required = false) StepType stepType) { + @PostAuthorize("hasPermission({'returnObject': returnObject.body}, 'FLOW.STEP_SPEC.GET_LIST')") + @PrivilegeDescription("Privilege to get all step specs") + public List getSteps(@RequestParam(name = "stepType", required = false) StepSpec.StepType stepType) { return stepSpecService.getSteps(stepType); } @GetMapping("/resource-types") + @PostAuthorize("hasPermission({'returnObject': returnObject.body}, 'FLOW.RESOURCE_TYPE.GET_LIST')") + @PrivilegeDescription("Privilege to get all resource types") public List getResourceTypes() { return resourceTypeService.resourceTypes(); } @GetMapping("/trigger-types") + @PostAuthorize("hasPermission({'returnObject': returnObject.body}, 'FLOW.TRIGGER_TYPE.GET_LIST')") + @PrivilegeDescription("Privilege to get all trigger types") public List getTriggerTypes() { return triggerSpecService.triggerTypes(); } diff --git a/xm-commons-flow/src/main/java/com/icthh/xm/commons/flow/rest/TenantResourceResource.java b/xm-commons-flow/src/main/java/com/icthh/xm/commons/flow/rest/TenantResourceResource.java index 4346848f..f67f5341 100644 --- a/xm-commons-flow/src/main/java/com/icthh/xm/commons/flow/rest/TenantResourceResource.java +++ b/xm-commons-flow/src/main/java/com/icthh/xm/commons/flow/rest/TenantResourceResource.java @@ -2,7 +2,10 @@ import com.icthh.xm.commons.flow.domain.TenantResource; import com.icthh.xm.commons.flow.service.TenantResourceService; +import com.icthh.xm.commons.permission.annotation.PrivilegeDescription; import lombok.RequiredArgsConstructor; +import org.springframework.security.access.prepost.PostAuthorize; +import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; @@ -24,26 +27,36 @@ public class TenantResourceResource { private final TenantResourceService resourceService; @GetMapping("/{resourceKey}") + @PostAuthorize("hasPermission({'returnObject': returnObject.body}, 'FLOW.RESOURCE.GET_ITEM')") + @PrivilegeDescription("Privilege to get the resource by resourceKey") public TenantResource getResource(@PathVariable("resourceKey") String resourceKey) { return resourceService.getResource(resourceKey); } @GetMapping() + @PostAuthorize("hasPermission({'returnObject': returnObject.body}, 'FLOW.RESOURCE.GET_LIST')") + @PrivilegeDescription("Privilege to get all resources") public List getResources(@RequestParam(name = "resourceType", required = false) String resourceType) { return resourceService.getResources(resourceType); } @PostMapping() + @PreAuthorize("hasPermission({'resource': #resource}, 'FLOW.RESOURCE.CREATE')") + @PrivilegeDescription("Privilege to create a new resource") public void createResource(@RequestBody TenantResource resource) { resourceService.createResource(resource); } @PutMapping() + @PreAuthorize("hasPermission({'resource': #resource}, 'FLOW.RESOURCE.UPDATE')") + @PrivilegeDescription("Privilege to update the resource") public void updateResource(@RequestBody TenantResource resource) { resourceService.updateResource(resource); } @DeleteMapping("/{resourceKey}") + @PreAuthorize("hasPermission({'resourceKey': #resourceKey}, 'FLOW.RESOURCE.DELETE')") + @PrivilegeDescription("Privilege to delete the resource") public void deleteResource(@PathVariable("resourceKey") String resourceKey) { resourceService.deleteResource(resourceKey); } diff --git a/xm-commons-flow/src/main/java/com/icthh/xm/commons/flow/service/CodeSnippetExecutor.java b/xm-commons-flow/src/main/java/com/icthh/xm/commons/flow/service/CodeSnippetExecutor.java index 5f4cc43b..46cd57eb 100644 --- a/xm-commons-flow/src/main/java/com/icthh/xm/commons/flow/service/CodeSnippetExecutor.java +++ b/xm-commons-flow/src/main/java/com/icthh/xm/commons/flow/service/CodeSnippetExecutor.java @@ -3,6 +3,8 @@ import com.icthh.xm.commons.flow.service.resolver.SnippetListLepKeyResolver; import com.icthh.xm.commons.flow.service.resolver.SnippetVarargsLepKeyResolver; import com.icthh.xm.commons.lep.LogicExtensionPoint; +import com.icthh.xm.commons.lep.api.BaseLepContext; +import com.icthh.xm.commons.lep.api.UseAsLepContext; import com.icthh.xm.commons.lep.spring.LepService; import org.springframework.stereotype.Service; @@ -15,12 +17,12 @@ public class CodeSnippetExecutor { public static final String SNIPPET = "Snippet"; @LogicExtensionPoint(value = SNIPPET, resolver = SnippetVarargsLepKeyResolver.class) - public Object runCodeSnippet(Object input, String ...snippetKeys) { + public Object runCodeSnippet(@UseAsLepContext BaseLepContext lepContext, String ...snippetKeys) { return null; } @LogicExtensionPoint(value = SNIPPET, resolver = SnippetListLepKeyResolver.class) - public Object runCodeSnippet(Object input, List snippetKeys) { + public Object runCodeSnippet(@UseAsLepContext BaseLepContext lepContext, List snippetKeys) { return null; } } diff --git a/xm-commons-flow/src/main/java/com/icthh/xm/commons/flow/service/CodeSnippetService.java b/xm-commons-flow/src/main/java/com/icthh/xm/commons/flow/service/CodeSnippetService.java index 25809779..e3ca058c 100644 --- a/xm-commons-flow/src/main/java/com/icthh/xm/commons/flow/service/CodeSnippetService.java +++ b/xm-commons-flow/src/main/java/com/icthh/xm/commons/flow/service/CodeSnippetService.java @@ -60,7 +60,7 @@ private String buildPath(String flowKey, String stepKey, String fileKey, Snippet throw new BusinessException("error.illegal.code.snippet.file.name", "File name can't contain '/' character"); } String tenantKey = tenantContextHolder.getTenantKey().toUpperCase(); - String folderPath = "/config/tenants/" + tenantKey + "/" + appName + "/flow/snippets/"; + String folderPath = "/config/tenants/" + tenantKey + "/" + appName + "/lep/flow/snippets/"; return folderPath + fileName; } } diff --git a/xm-commons-flow/src/main/java/com/icthh/xm/commons/flow/service/FlowService.java b/xm-commons-flow/src/main/java/com/icthh/xm/commons/flow/service/FlowService.java index 2a7f0669..bbdd804b 100644 --- a/xm-commons-flow/src/main/java/com/icthh/xm/commons/flow/service/FlowService.java +++ b/xm-commons-flow/src/main/java/com/icthh/xm/commons/flow/service/FlowService.java @@ -3,8 +3,12 @@ import com.icthh.xm.commons.config.client.repository.TenantConfigRepository; import com.icthh.xm.commons.config.domain.Configuration; import com.icthh.xm.commons.exceptions.BusinessException; +import com.icthh.xm.commons.exceptions.EntityNotFoundException; import com.icthh.xm.commons.flow.domain.Flow; +import com.icthh.xm.commons.flow.engine.FlowExecutorService; +import com.icthh.xm.commons.flow.engine.context.FlowExecutionContext; import com.icthh.xm.commons.flow.service.FlowConfigService.FlowsConfig; +import com.icthh.xm.commons.flow.service.resolver.FlowKeyLepKeyResolver; import com.icthh.xm.commons.flow.service.resolver.FlowTypeLepKeyResolver; import com.icthh.xm.commons.flow.service.trigger.TriggerProcessor; import com.icthh.xm.commons.lep.LogicExtensionPoint; @@ -17,8 +21,10 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Optional; import static java.util.stream.Collectors.toList; +import static org.apache.commons.lang3.StringUtils.isNotBlank; @Slf4j @RequiredArgsConstructor @@ -31,6 +37,7 @@ public class FlowService { private final TenantConfigRepository tenantConfigRepository; private final CodeSnippetService codeSnippetService; private final TriggerProcessor triggerProcessor; + private final FlowExecutorService flowExecutor; private FlowService self; @Autowired @@ -51,12 +58,14 @@ public Flow getFlow(String flowKey) { @LogicExtensionPoint(value = "CreateFlow", resolver = FlowTypeLepKeyResolver.class) public void createFlow(Flow flow) { assertNotExits(flow); + assertStartStepExists(flow); self.saveFlowInternal(flow); } @LogicExtensionPoint(value = "UpdateFlow", resolver = FlowTypeLepKeyResolver.class) public void updateFlow(Flow flow) { assertExits(flow.getKey()); + assertStartStepExists(flow); self.saveFlowInternal(flow); } @@ -125,4 +134,44 @@ private List convertToConfiguration(Map upda .collect(toList()); } + private void assertStartStepExists(Flow flow) { + boolean isExists = isNotBlank(flow.getStartStep()) && flow.getSteps().stream().anyMatch(step -> + flow.getStartStep().equals(step.getKey()) + ); + + if (!isExists) { + throw new BusinessException("error.flow.start.step.not.found", "Start step with key " + flow.getStartStep() + " not found"); + } + } + + @LogicExtensionPoint(value = "RunFlow", resolver = FlowKeyLepKeyResolver.class) + public Object runFlow(String flowKey, Object input) { + return self.runFlowInternal(flowKey, input).getOutput(); + } + + @LogicExtensionPoint("RunFlow") + public FlowExecutionContext runFlowInternal(String flowKey, Object input) { + Flow flow = getRequiredFlow(flowKey); + FlowExecutionContext executionContext = self.executeFlow(flow, input); + if (log.isDebugEnabled()) { + log.debug("Flow execution context: {}", executionContext); + } + return executionContext; + } + + @LogicExtensionPoint(value = "ExecuteFlow", resolver = FlowTypeLepKeyResolver.class) + public FlowExecutionContext executeFlow(Flow flow, Object input) { + return self.executeFlowInternal(flow, input); + } + + @LogicExtensionPoint(value = "ExecuteFlow") + public FlowExecutionContext executeFlowInternal(Flow flow, Object input) { + return flowExecutor.execute(flow, input); + } + + private Flow getRequiredFlow(String flowKey) { + return Optional.ofNullable(flowConfigService.getFlow(flowKey)) + .orElseThrow(() -> new EntityNotFoundException("Flow with key " + flowKey + " not found")); + } + } diff --git a/xm-commons-flow/src/main/java/com/icthh/xm/commons/flow/service/resolver/FlowKeyLepKeyResolver.java b/xm-commons-flow/src/main/java/com/icthh/xm/commons/flow/service/resolver/FlowKeyLepKeyResolver.java new file mode 100644 index 00000000..f6c0b285 --- /dev/null +++ b/xm-commons-flow/src/main/java/com/icthh/xm/commons/flow/service/resolver/FlowKeyLepKeyResolver.java @@ -0,0 +1,15 @@ +package com.icthh.xm.commons.flow.service.resolver; + +import com.icthh.xm.lep.api.LepKeyResolver; +import com.icthh.xm.lep.api.LepMethod; +import org.springframework.stereotype.Component; + +import java.util.List; + +@Component +public class FlowKeyLepKeyResolver implements LepKeyResolver { + @Override + public List segments(LepMethod method) { + return List.of(method.getParameter("flowKey", String.class)); + } +} diff --git a/xm-commons-flow/src/main/java/com/icthh/xm/commons/flow/service/resolver/StepKeyResolver.java b/xm-commons-flow/src/main/java/com/icthh/xm/commons/flow/service/resolver/StepKeyResolver.java new file mode 100644 index 00000000..874f1fcc --- /dev/null +++ b/xm-commons-flow/src/main/java/com/icthh/xm/commons/flow/service/resolver/StepKeyResolver.java @@ -0,0 +1,17 @@ +package com.icthh.xm.commons.flow.service.resolver; + +import com.icthh.xm.commons.flow.domain.Step; +import com.icthh.xm.lep.api.LepKeyResolver; +import com.icthh.xm.lep.api.LepMethod; +import org.springframework.stereotype.Component; + +import java.util.List; + +@Component +public class StepKeyResolver implements LepKeyResolver { + @Override + public List segments(LepMethod method) { + String step = method.getParameter("step", Step.class).getTypeKey(); + return List.of(step); + } +} diff --git a/xm-commons-flow/src/main/java/com/icthh/xm/commons/flow/spec/step/StepSpecService.java b/xm-commons-flow/src/main/java/com/icthh/xm/commons/flow/spec/step/StepSpecService.java index d1cf1bab..5a458f79 100644 --- a/xm-commons-flow/src/main/java/com/icthh/xm/commons/flow/spec/step/StepSpecService.java +++ b/xm-commons-flow/src/main/java/com/icthh/xm/commons/flow/spec/step/StepSpecService.java @@ -10,6 +10,7 @@ import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.stream.Collectors; import static com.icthh.xm.commons.config.client.utils.Utils.nullSafeList; @@ -48,6 +49,11 @@ public JavaType configFileJavaType(TypeFactory factory) { } public StepSpec getStepSpec(String stepKey) { + return Optional.ofNullable(getStepsSpec().get(stepKey)) + .orElseThrow(() -> new IllegalArgumentException("Step not found: " + stepKey)); + } + + public StepSpec findStepSpec(String stepKey) { return getStepsSpec().get(stepKey); } diff --git a/xm-commons-flow/src/main/resources/lep/default/flow/step/StepClassExecutor.groovy b/xm-commons-flow/src/main/resources/lep/default/flow/step/StepClassExecutor.groovy new file mode 100644 index 00000000..3d49c3aa --- /dev/null +++ b/xm-commons-flow/src/main/resources/lep/default/flow/step/StepClassExecutor.groovy @@ -0,0 +1,18 @@ +import com.icthh.xm.commons.flow.spec.step.StepSpec + +import java.util.concurrent.ConcurrentHashMap + +def step = lepContext.lepServices.getInstance(StepResolver.class).resolve(lepContext, lepContext.inArgs.stepSpec) +return step.execute(lepContext) + +class StepResolver { + private final Map steps = new ConcurrentHashMap<>(); + + public def resolve(def lepContext, StepSpec spec) { + def step = steps.computeIfAbsent(spec.implementation) { + lepContext.lepServices.getInstance(Class.forName(spec.implementation)) + } + return step + } + +} diff --git a/xm-commons-flow/src/test/java/com/icthh/xm/commons/flow/rest/AbstractFlowIntTest.java b/xm-commons-flow/src/test/java/com/icthh/xm/commons/flow/rest/AbstractFlowIntTest.java index d00b901f..c5e50d55 100644 --- a/xm-commons-flow/src/test/java/com/icthh/xm/commons/flow/rest/AbstractFlowIntTest.java +++ b/xm-commons-flow/src/test/java/com/icthh/xm/commons/flow/rest/AbstractFlowIntTest.java @@ -1,14 +1,25 @@ package com.icthh.xm.commons.flow.rest; +import com.icthh.xm.commons.config.client.api.RefreshableConfiguration; import com.icthh.xm.commons.config.client.repository.TenantConfigRepository; +import com.icthh.xm.commons.config.domain.Configuration; +import com.icthh.xm.commons.flow.context.FlowLepAdditionalContext; +import com.icthh.xm.commons.flow.context.StepLepAdditionalContext; +import com.icthh.xm.commons.flow.context.StepsLepAdditionalContext; import com.icthh.xm.commons.flow.context.TenantResourceLepAdditionalContext; +import com.icthh.xm.commons.flow.engine.FlowExecutorService; +import com.icthh.xm.commons.flow.engine.StepExecutorService; +import com.icthh.xm.commons.flow.service.CodeSnippetExecutor; import com.icthh.xm.commons.flow.service.CodeSnippetService; import com.icthh.xm.commons.flow.service.FlowConfigService; import com.icthh.xm.commons.flow.service.FlowService; import com.icthh.xm.commons.flow.service.TenantResourceConfigService; import com.icthh.xm.commons.flow.service.TenantResourceService; import com.icthh.xm.commons.flow.service.YamlConverter; +import com.icthh.xm.commons.flow.service.resolver.FlowKeyLepKeyResolver; import com.icthh.xm.commons.flow.service.resolver.FlowTypeLepKeyResolver; +import com.icthh.xm.commons.flow.service.resolver.SnippetListLepKeyResolver; +import com.icthh.xm.commons.flow.service.resolver.StepKeyResolver; import com.icthh.xm.commons.flow.service.resolver.TenantResourceTypeLepKeyResolver; import com.icthh.xm.commons.flow.service.resolver.TriggerResolver; import com.icthh.xm.commons.flow.service.trigger.TriggerProcessor; @@ -23,18 +34,29 @@ import com.icthh.xm.commons.tenant.TenantContextHolder; import com.icthh.xm.commons.tenant.TenantContextUtils; import com.icthh.xm.commons.tenant.spring.config.TenantContextConfiguration; +import lombok.SneakyThrows; +import org.apache.commons.io.IOUtils; import org.junit.After; import org.junit.Before; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.core.io.ClassPathResource; import org.springframework.test.context.junit4.SpringRunner; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.web.context.WebApplicationContext; import org.springframework.web.servlet.config.annotation.EnableWebMvc; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doAnswer; + @RunWith(SpringRunner.class) @SpringBootTest( @@ -56,12 +78,21 @@ FlowResource.class, FlowService.class, CodeSnippetService.class, + CodeSnippetExecutor.class, + SnippetListLepKeyResolver.class, TriggerProcessor.class, FlowTypeLepKeyResolver.class, TriggerResolver.class, + FlowKeyLepKeyResolver.class, TriggerTypeSpecService.class, YamlConverter.class, - FlowConfigService.class + FlowConfigService.class, + FlowExecutorService.class, + StepKeyResolver.class, + StepExecutorService.class, + StepLepAdditionalContext.class, + StepsLepAdditionalContext.class, + FlowLepAdditionalContext.class }, properties = {"spring.application.name=testApp"} ) @@ -77,17 +108,51 @@ public abstract class AbstractFlowIntTest { TenantContextHolder tenantContextHolder; @MockBean TenantConfigRepository tenantConfigRepository; + @Autowired + List configurations; + + List updateConfigs; @Before public void setup() { this.mockMvc = MockMvcBuilders.webAppContextSetup(this.wac).build(); TenantContextUtils.setTenant(tenantContextHolder, "TEST"); lepManagementService.beginThreadContext(); + updateConfigs = new ArrayList<>(); } @After public void after() { + updateConfigs.forEach(c -> updateConfig(c, null)); lepManagementService.endThreadContext(); } + public void updateConfiguration(String path, String content) { + updateConfigs.add(path); + updateConfig(path, content); + } + + private void updateConfig(String path, String content) { + configurations.stream().filter(c -> c.isListeningConfiguration(path)).forEach(c -> c.onRefresh(path, content)); + } + + @SneakyThrows + public static String loadFile(String path) { + try (InputStream cfgInputStream = new ClassPathResource(path).getInputStream()) { + return IOUtils.toString(cfgInputStream, UTF_8); + } + } + + public void mockSendConfigToRefresh() { + doAnswer(invocation -> { + if (invocation.getArguments()[0] == null) { + return null; + } + List configurations = invocation.getArgument(0); + configurations.forEach(c -> updateConfiguration(c.getPath(), c.getContent())); + return null; + }).when(tenantConfigRepository).updateConfigurations(any()); + + } + } diff --git a/xm-commons-flow/src/test/java/com/icthh/xm/commons/flow/rest/FlowExecuteIntTest.java b/xm-commons-flow/src/test/java/com/icthh/xm/commons/flow/rest/FlowExecuteIntTest.java new file mode 100644 index 00000000..b8c02923 --- /dev/null +++ b/xm-commons-flow/src/test/java/com/icthh/xm/commons/flow/rest/FlowExecuteIntTest.java @@ -0,0 +1,197 @@ +package com.icthh.xm.commons.flow.rest; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; +import com.icthh.xm.commons.flow.domain.Flow; +import com.icthh.xm.commons.flow.engine.FlowExecutorService; +import com.icthh.xm.commons.flow.engine.context.FlowExecutionContext; +import com.icthh.xm.commons.flow.service.FlowConfigService; +import com.icthh.xm.commons.flow.service.FlowConfigService.FlowsConfig; +import com.icthh.xm.commons.flow.service.FlowService; +import lombok.SneakyThrows; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; + +import java.util.List; +import java.util.Map; + +import static org.junit.Assert.assertEquals; + +public class FlowExecuteIntTest extends AbstractFlowIntTest { + + @Autowired + TestLepService testLepService; + + @Autowired + FlowConfigService flowConfigService; + + @Autowired + FlowService flowService; + + @Autowired + FlowExecutorService flowExecutor; + + @Test + @SneakyThrows + public void testSimpleExecution() { + String simpleFlow = initFlow(); + + FlowsConfig flows = new ObjectMapper(new YAMLFactory()).readValue(simpleFlow, FlowsConfig.class); + Flow flow = flows.getFlows().get(0); + + FlowExecutionContext executionContext = flowExecutor.execute(flow, Map.of("b", -1)); + assertEquals(0, executionContext.getOutput()); + assertEquals( + mockContext( + "simple-flow", + Map.of("b", -1), + 0, + Map.of("step1", Map.of("b", -1), "step2", 0, "step3", false), + Map.of("step1", 0, "step2", false, "step3", false) + ), + executionContext + ); + + executionContext = flowExecutor.execute(flow, Map.of("b", 2)); + assertEquals(8, executionContext.getOutput()); + assertEquals( + mockContext( + "simple-flow", + Map.of("b", 2), + 8, + Map.of("step1", Map.of("b", 2), "step2", 3, "step3", false, "step4", true), + Map.of("step1", 3, "step2", false, "step3", true, "step4", 8) + ), + executionContext + ); + + executionContext = flowExecutor.execute(flow, Map.of("b", 5)); + assertEquals(2, executionContext.getOutput()); + assertEquals( + mockContext( + "simple-flow", + Map.of("b", 5), + 2, + Map.of("step1", Map.of("b", 5), "step2", 6, "step5", true), + Map.of("step1", 6, "step2", true, "step5", 2) + ), + executionContext + ); + + flowService.deleteFlow(flow.getKey()); + } + + @Test + public void testRunFlowFromLep() { + initFlow(); + + updateConfiguration("/config/tenants/TEST/testApp/lep/test/Test.groovy", "lepContext.flow.executeFlow('simple-flow', [b: 5])"); + + FlowExecutionContext result = (FlowExecutionContext) testLepService.test(); + assertEquals(2, result.getOutput()); + assertEquals( + mockContext( + "simple-flow", + Map.of("b", 5), + 2, + Map.of("step1", Map.of("b", 5), "step2", 6, "step5", true), + Map.of("step1", 6, "step2", true, "step5", 2) + ), + result + ); + + flowService.deleteFlow("simple-flow"); + } + + @Test + @SneakyThrows + public void testRunIterableTaskLep() { + String countWordsFlow = loadFile("test-flow-execute/count-words-flow.yml"); + mockSendConfigToRefresh(); + + updateConfiguration("/config/tenants/TEST/testApp/lep/flow/step/Action$$groovyAction.groovy", + "return lepContext.step.runSnippet('groovyAction', lepContext)"); + updateConfiguration("/config/tenants/TEST/testApp/flow/step-spec/steps.yml", loadFile("test-flow-execute/steps.yml")); + + FlowsConfig flows = new ObjectMapper(new YAMLFactory()).readValue(countWordsFlow, FlowsConfig.class); + Flow flow = flows.getFlows().get(0); + flowService.createFlow(flow); + + String input = "Yes, some text words to count"; + FlowExecutionContext result = flowExecutor.execute(flow, input); + FlowExecutionContext mockedContext = mockContext( + "count-long-words-flow", + input, + 18, + Map.of("sum_chars", List.of(4, 4, 5, 5), "split_words", input, "words_to_length", Map.of( + "a", Map.of("b", new String[]{"Yes", "some", "text", "words", "to", "count"}) + )), + Map.of("words_to_length", List.of(4, 4, 5, 5), "sum_chars", 18, "split_words", Map.of( + "a", Map.of("b", new String[]{"Yes", "some", "text", "words", "to", "count"}) + )) + ); + mockedContext.resetIteration(); + assertEquals( + remap(mockedContext), + remap(result) + ); + + flowService.deleteFlow("count-words-flow"); + } + + @SneakyThrows + private String initFlow() { + String simpleFlow = loadFile("test-flow-execute/simple-flow.yml"); + mockSendConfigToRefresh(); + // test action lep + updateConfiguration("/config/tenants/TEST/testApp/lep/flow/step/Action$$sum.groovy", + "return lepContext.flow.input.b + lepContext.step.parameters.a"); + // test condition lep and run snippet + updateConfiguration("/config/tenants/TEST/testApp/lep/flow/step/Condition$$groovyCondition.groovy", + "return lepContext.step.runSnippet('groovyCondition', lepContext)"); + // test class condition and get previous step output + updateConfiguration("/config/tenants/commons/lep/flow/conditions/NotIsZeroCondition.groovy", + loadFile("test-flow-execute/NotIsZeroCondition.groovy")); + // test class action and get previous step output + updateConfiguration("/config/tenants/commons/lep/flow/actions/DivideAction.groovy", + loadFile("test-flow-execute/DivideAction.groovy")); + // test run snippet + updateConfiguration("/config/tenants/TEST/testApp/lep/flow/step/Action$$groovyAction.groovy", + "return lepContext.step.runSnippet('groovyAction', lepContext)"); + // test get from step patams and flow input + updateConfiguration("/config/tenants/TEST/testApp/lep/flow/step/Action$$minus.groovy", + "return (int) (lepContext.step.parameters.a - lepContext.flow.input.b)"); + + updateConfiguration("/config/tenants/TEST/testApp/flow/step-spec/steps.yml", loadFile("test-flow-execute/steps.yml")); + + FlowsConfig flows = new ObjectMapper(new YAMLFactory()).readValue(simpleFlow, FlowsConfig.class); + Flow flow = flows.getFlows().get(0); + flowService.createFlow(flow); + return simpleFlow; + } + + private FlowExecutionContext mockContext( + String key, + Object input, + Object output, + Map stepInput, + Map stepOutput + ) { + var context = new FlowExecutionContext(key, input); + context.setOutput(output); + for (Map.Entry entry : stepInput.entrySet()) { + context.getStepInput().put(entry.getKey(), entry.getValue()); + } + for (Map.Entry entry : stepOutput.entrySet()) { + context.getStepOutput().put(entry.getKey(), entry.getValue()); + } + return context; + } + + @SneakyThrows + public Map remap(Object obj) { + ObjectMapper objectMapper = new ObjectMapper(); + return objectMapper.readValue(objectMapper.writeValueAsString(obj), Map.class); + } + +} diff --git a/xm-commons-flow/src/test/java/com/icthh/xm/commons/flow/rest/FlowResourceIntTest.java b/xm-commons-flow/src/test/java/com/icthh/xm/commons/flow/rest/FlowResourceIntTest.java index a021ec1a..f3b06fac 100644 --- a/xm-commons-flow/src/test/java/com/icthh/xm/commons/flow/rest/FlowResourceIntTest.java +++ b/xm-commons-flow/src/test/java/com/icthh/xm/commons/flow/rest/FlowResourceIntTest.java @@ -21,7 +21,6 @@ import java.util.List; import java.util.Map; -import static com.icthh.xm.commons.flow.steps.StepsRefreshableConfigurationUnitTest.loadFile; import static org.hamcrest.Matchers.hasSize; import static org.hamcrest.core.IsEqual.equalTo; import static org.junit.Assert.assertEquals; @@ -103,15 +102,15 @@ public void testFlowCrud() { assertEquals("foofoofoo", value.get(0).getContent()); assertEquals("/config/tenants/TEST/test/somespec.yml", value.get(1).getPath()); assertEquals("blablabla", value.get(1).getContent()); - assertEquals("/config/tenants/TEST/testApp/flow/snippets/Snippet$$my-flow$$step2$$mapping.js", value.get(2).getPath()); + assertEquals("/config/tenants/TEST/testApp/lep/flow/snippets/Snippet$$my-flow$$step2$$mapping.js", value.get(2).getPath()); assertEquals("return context.get('orders').map(order => { return { id: order.id, name: order.name }; })", value.get(2).getContent()); - assertEquals("/config/tenants/TEST/testApp/flow/snippets/Snippet$$my-flow$$step2$$precheck.js", value.get(3).getPath()); + assertEquals("/config/tenants/TEST/testApp/lep/flow/snippets/Snippet$$my-flow$$step2$$precheck.js", value.get(3).getPath()); assertEquals("if (context.get('orders').length > 0) { return true; } else { return false; }", value.get(3).getContent()); - assertEquals("/config/tenants/TEST/testApp/flow/snippets/Snippet$$my-flow$$step3$$mapping.js", value.get(4).getPath()); + assertEquals("/config/tenants/TEST/testApp/lep/flow/snippets/Snippet$$my-flow$$step3$$mapping.js", value.get(4).getPath()); assertEquals("return context.get('users').map(user => { return { id: user.id, name: user.name }; })", value.get(4).getContent()); - assertEquals("/config/tenants/TEST/testApp/flow/snippets/Snippet$$my-flow$$step3$$postcheck.js", value.get(5).getPath()); + assertEquals("/config/tenants/TEST/testApp/lep/flow/snippets/Snippet$$my-flow$$step3$$postcheck.js", value.get(5).getPath()); assertEquals("if (context.get('votes').length > 0) { return true; } else { return false; }", value.get(5).getContent()); - assertEquals("/config/tenants/TEST/testApp/flow/snippets/Snippet$$my-flow$$step3$$precheck.js", value.get(6).getPath()); + assertEquals("/config/tenants/TEST/testApp/lep/flow/snippets/Snippet$$my-flow$$step3$$precheck.js", value.get(6).getPath()); assertEquals("if (context.get('users').length > 0) { return true; } else { return false; }", value.get(6).getContent()); ResultActions flowGet = mockMvc.perform(get("/api/flow/my-flow")) @@ -184,7 +183,7 @@ public Flow mockFlow() { step1.setTypeKey("actionkey"); step1.setParameters(Map.of("query", "select * from orders")); step1.setType(StepSpec.StepType.ACTION); - step1.setNext(List.of("step2")); + step1.setNext("step2"); // Snippets for Step 2 Step.Snippet precheckSnippetStep2 = new Step.Snippet(); @@ -202,7 +201,7 @@ public Flow mockFlow() { step2.setParameters(Map.of("query", "select * from users")); step2.setSnippets(Map.of("precheck", precheckSnippetStep2, "mapping", mappingSnippetStep2)); step2.setType(StepSpec.StepType.ACTION); - step2.setNext(List.of("step3")); + step2.setNext("step3"); // Snippets for Step 3 Step.Snippet precheckSnippetStep3 = new Step.Snippet(); @@ -234,6 +233,7 @@ public Flow mockFlow() { Flow flow = new Flow(); flow.setKey("my-flow"); flow.setTrigger(trigger); + flow.setStartStep("step1"); flow.setSteps(List.of(step1, step2, step3)); return flow; diff --git a/xm-commons-flow/src/test/java/com/icthh/xm/commons/flow/rest/TestLepContext.java b/xm-commons-flow/src/test/java/com/icthh/xm/commons/flow/rest/TestLepContext.java index 1e24d907..de39ee23 100644 --- a/xm-commons-flow/src/test/java/com/icthh/xm/commons/flow/rest/TestLepContext.java +++ b/xm-commons-flow/src/test/java/com/icthh/xm/commons/flow/rest/TestLepContext.java @@ -1,6 +1,7 @@ package com.icthh.xm.commons.flow.rest; +import com.icthh.xm.commons.flow.api.FlowLepContextFields; import com.icthh.xm.commons.lep.api.BaseLepContext; -public class TestLepContext extends BaseLepContext { +public class TestLepContext extends BaseLepContext implements FlowLepContextFields { } diff --git a/xm-commons-flow/src/test/java/com/icthh/xm/commons/flow/steps/StepsRefreshableConfigurationUnitTest.java b/xm-commons-flow/src/test/java/com/icthh/xm/commons/flow/steps/StepsRefreshableConfigurationUnitTest.java index aa744a8f..454da7f3 100644 --- a/xm-commons-flow/src/test/java/com/icthh/xm/commons/flow/steps/StepsRefreshableConfigurationUnitTest.java +++ b/xm-commons-flow/src/test/java/com/icthh/xm/commons/flow/steps/StepsRefreshableConfigurationUnitTest.java @@ -25,7 +25,7 @@ public void testReadConfiguration() { when(mock.getTenantKey()).thenReturn("UNIT_TEST"); var service = new StepSpecService("test", mock); service.onRefresh("/config/tenants/UNIT_TEST/test/flow/step-spec/testreadspec.yml", loadFile("step-spec/testreadspec.yml")); - StepSpec actionSpec = service.getStepSpec("actionkey"); + StepSpec actionSpec = service.findStepSpec("actionkey"); assertThat(actionSpec, equalTo(mockAction())); } diff --git a/xm-commons-flow/src/test/resources/flow.yml b/xm-commons-flow/src/test/resources/flow.yml index de768c0e..a9d588ba 100644 --- a/xm-commons-flow/src/test/resources/flow.yml +++ b/xm-commons-flow/src/test/resources/flow.yml @@ -2,14 +2,14 @@ flows: - key: "my-flow" description: "Init description" + startStep: "step1" steps: - key: "step1" typeKey: "actionkey" parameters: query: "select * from orders" type: "ACTION" - next: - - "step2" + next: "step2" - key: "step2" typeKey: "actionkey" parameters: @@ -24,8 +24,7 @@ flows: \ false; }" extension: "js" type: "ACTION" - next: - - "step3" + next: "step3" - key: "step3" typeKey: "actionkey" parameters: diff --git a/xm-commons-flow/src/test/resources/test-flow-execute/DivideAction.groovy b/xm-commons-flow/src/test/resources/test-flow-execute/DivideAction.groovy new file mode 100644 index 00000000..04c56a37 --- /dev/null +++ b/xm-commons-flow/src/test/resources/test-flow-execute/DivideAction.groovy @@ -0,0 +1,12 @@ +package commons.lep.flow.actions + +import com.icthh.xm.commons.flow.api.Action +import com.icthh.xm.commons.flow.api.FlowLepContextFields +import com.icthh.xm.commons.lep.api.BaseLepContext + +class DivideAction implements Action { + @Override + Object execute(T lepContext) { + return (int) (lepContext.step.parameters.c / lepContext.steps[lepContext.step.parameters.stepKey].output) + } +} diff --git a/xm-commons-flow/src/test/resources/test-flow-execute/NotIsZeroCondition.groovy b/xm-commons-flow/src/test/resources/test-flow-execute/NotIsZeroCondition.groovy new file mode 100644 index 00000000..5294e6ed --- /dev/null +++ b/xm-commons-flow/src/test/resources/test-flow-execute/NotIsZeroCondition.groovy @@ -0,0 +1,12 @@ +package commons.lep.flow.conditions + +import com.icthh.xm.commons.flow.api.Condition +import com.icthh.xm.commons.flow.api.FlowLepContextFields +import com.icthh.xm.commons.lep.api.BaseLepContext + +class NotIsZeroCondition implements Condition { + @Override + Boolean test(T lepContext) { + return lepContext.steps[lepContext.step.parameters.stepKey].output != 0 + } +} diff --git a/xm-commons-flow/src/test/resources/test-flow-execute/count-words-flow.yml b/xm-commons-flow/src/test/resources/test-flow-execute/count-words-flow.yml new file mode 100644 index 00000000..74b88c8f --- /dev/null +++ b/xm-commons-flow/src/test/resources/test-flow-execute/count-words-flow.yml @@ -0,0 +1,36 @@ +--- +flows: +- key: "count-long-words-flow" + startStep: "split_words" + steps: + - key: "split_words" + typeKey: "groovyAction" + type: "ACTION" + snippets: + groovyAction: + content: "[a:[b:lepContext.flow.input.split('\\\\W+')]]" + extension: "groovy" + next: "words_to_length" + - key: "words_to_length" + typeKey: "groovyAction" + type: "ACTION" + isIterable: true + iterableJsonPath: "$.a.b" + removeNullOutputForIterableResult: true + snippets: + groovyAction: + content: "lepContext.step.iterationItem.length() >= 4 ? lepContext.step.iterationItem.length() : null" + extension: "groovy" + next: "sum_chars" + - key: "sum_chars" + typeKey: "groovyAction" + type: "ACTION" + snippets: + groovyAction: + content: "lepContext.step.input.sum()" + extension: "groovy" + trigger: + typeKey: "httpkey" + parameters: + method: "GET" + url: "/api/orders" diff --git a/xm-commons-flow/src/test/resources/test-flow-execute/simple-flow.yml b/xm-commons-flow/src/test/resources/test-flow-execute/simple-flow.yml new file mode 100644 index 00000000..8cd74279 --- /dev/null +++ b/xm-commons-flow/src/test/resources/test-flow-execute/simple-flow.yml @@ -0,0 +1,51 @@ +--- +flows: +- key: "simple-flow" + startStep: "step1" + steps: + - key: "step1" + typeKey: "sum" + parameters: + a: 1 + type: "ACTION" + next: "step2" + - key: "step2" + typeKey: "groovyCondition" + parameters: + c: 5 + snippets: + groovyCondition: + content: "lepContext.step.context > lepContext.step.parameters.c" + extension: "groovy" + type: "CONDITION" + nextOnConditionFalse: "step3" + nextOnConditionTrue: "step5" + - key: "step3" + typeKey: "notIsZero" + type: "CONDITION" + parameters: + stepKey: "step1" + nextOnConditionTrue: "step4" + - key: "step4" + typeKey: "divide" + parameters: + c: 24 + stepKey: "step1" + type: "ACTION" + - key: "step5" + typeKey: "minus" + parameters: + a: 7 + type: "ACTION" + - key: "step6" + typeKey: "groovyAction" + type: "ACTION" + snippets: + groovyAction: + content: "(int)(lepContext.steps.step1.output * lepContext.steps.step5.output)" + extension: "groovy" + trigger: + typeKey: "httpkey" + parameters: + method: "GET" + url: "/api/orders" diff --git a/xm-commons-flow/src/test/resources/test-flow-execute/steps.yml b/xm-commons-flow/src/test/resources/test-flow-execute/steps.yml new file mode 100644 index 00000000..1ba16ed8 --- /dev/null +++ b/xm-commons-flow/src/test/resources/test-flow-execute/steps.yml @@ -0,0 +1,14 @@ +- key: sum + type: ACTION +- key: jsCondition + type: CONDITION +- key: notIsZero + type: CONDITION + implementation: commons.lep.flow.conditions.NotIsZeroCondition +- key: divide + type: ACTION + implementation: commons.lep.flow.actions.DivideAction +- key: minus + type: ACTION +- key: jsAction + type: ACTION diff --git a/xm-commons-lep/src/main/java/com/icthh/xm/commons/lep/api/BaseLepContext.java b/xm-commons-lep/src/main/java/com/icthh/xm/commons/lep/api/BaseLepContext.java index 7d846691..127eec7b 100644 --- a/xm-commons-lep/src/main/java/com/icthh/xm/commons/lep/api/BaseLepContext.java +++ b/xm-commons-lep/src/main/java/com/icthh/xm/commons/lep/api/BaseLepContext.java @@ -31,6 +31,10 @@ public Object get(Object additionalContextKey) { return additionalContext.get(additionalContextKey); } + public Object get(String additionalContextKey) { + return additionalContext.get(additionalContextKey); + } + public Object propertyMissing(String prop) { return get(prop); } diff --git a/xm-commons-lep/src/main/java/com/icthh/xm/commons/lep/api/UseAsLepContext.java b/xm-commons-lep/src/main/java/com/icthh/xm/commons/lep/api/UseAsLepContext.java new file mode 100644 index 00000000..89e37d9a --- /dev/null +++ b/xm-commons-lep/src/main/java/com/icthh/xm/commons/lep/api/UseAsLepContext.java @@ -0,0 +1,11 @@ +package com.icthh.xm.commons.lep.api; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.PARAMETER) +public @interface UseAsLepContext { +} diff --git a/xm-commons-lep/src/main/java/com/icthh/xm/commons/lep/impl/LogicExtensionPointHandler.java b/xm-commons-lep/src/main/java/com/icthh/xm/commons/lep/impl/LogicExtensionPointHandler.java index 34e2d2d5..af50d121 100644 --- a/xm-commons-lep/src/main/java/com/icthh/xm/commons/lep/impl/LogicExtensionPointHandler.java +++ b/xm-commons-lep/src/main/java/com/icthh/xm/commons/lep/impl/LogicExtensionPointHandler.java @@ -75,10 +75,19 @@ public Object handleLepMethod(Class targetType, Object target, Method method, private Object invokeLepMethod(LepEngine lepEngine, Object target, LepMethod lepMethod, LepKey lepKey) { TargetProceedingLep targetProceedingLep = new TargetProceedingLep(target, lepMethod, lepKey); - BaseLepContext lepContext = lepContextService.createLepContext(lepEngine, targetProceedingLep); + BaseLepContext lepContext = buildLepContext(lepEngine, lepMethod, targetProceedingLep); return lepEngine.invoke(lepKey, targetProceedingLep, lepContext); } + private BaseLepContext buildLepContext(LepEngine lepEngine, LepMethod lepMethod, TargetProceedingLep targetProceedingLep) { + String lepContextMethodParameter = lepMethod.getMethodSignature().getLepContextMethodParameter(); + if (lepContextMethodParameter == null) { + return lepContextService.createLepContext(lepEngine, targetProceedingLep); + } else { + return lepMethod.getParameter(lepContextMethodParameter, BaseLepContext.class); + } + } + @SneakyThrows private Object invokeOriginalMethod(Object target, LepMethod lepMethod, LepKey lepKey) { Method method = lepMethod.getMethodSignature().getMethod(); diff --git a/xm-commons-lep/src/main/java/com/icthh/xm/commons/lep/impl/MethodSignatureImpl.java b/xm-commons-lep/src/main/java/com/icthh/xm/commons/lep/impl/MethodSignatureImpl.java index 43904825..41b918ea 100644 --- a/xm-commons-lep/src/main/java/com/icthh/xm/commons/lep/impl/MethodSignatureImpl.java +++ b/xm-commons-lep/src/main/java/com/icthh/xm/commons/lep/impl/MethodSignatureImpl.java @@ -1,5 +1,7 @@ package com.icthh.xm.commons.lep.impl; +import com.icthh.xm.commons.lep.api.BaseLepContext; +import com.icthh.xm.commons.lep.api.UseAsLepContext; import com.icthh.xm.lep.api.MethodSignature; import java.lang.reflect.Method; @@ -9,6 +11,7 @@ import java.util.List; import java.util.Map; +import static java.util.Arrays.stream; import static java.util.Collections.emptyList; import static java.util.Collections.unmodifiableList; import static java.util.Collections.unmodifiableMap; @@ -23,9 +26,11 @@ public class MethodSignatureImpl implements MethodSignature { private final String[] parameterNamesArray; private final Method method; private final String declaringClassName; + private final String lepContextMethodParameter; public MethodSignatureImpl(Method method, Class targetType) { this.method = method; + this.lepContextMethodParameter = calculateLepContextMethodParameter(method); this.parameterNames = calculateParametersNames(method); this.parameterIndexes = calculateParametersIndexes(this.parameterNames); this.parameterNamesArray = this.parameterNames.toArray(STRINGS_EMPTY_ARRAY); @@ -33,6 +38,19 @@ public MethodSignatureImpl(Method method, Class targetType) { this.declaringClassName = (declaringClass != null) ? declaringClass.getName() : null; } + private String calculateLepContextMethodParameter(Method method) { + Parameter[] parameters = method.getParameters(); + if (parameters != null) { + return stream(parameters) + .filter(p -> BaseLepContext.class.isAssignableFrom(p.getType())) + .filter(p -> p.getAnnotation(UseAsLepContext.class) != null) + .findAny() + .map(Parameter::getName) + .orElse(null); + } + return null; + } + private Map calculateParametersIndexes(List parameterNames) { Map parameterIndexes = new HashMap<>(); for (int i = 0; i < parameterNames.size(); i++) { @@ -109,5 +127,9 @@ public Method getMethod() { return method; } + @Override + public String getLepContextMethodParameter() { + return lepContextMethodParameter; + } } diff --git a/xm-commons-lep/src/main/java/com/icthh/xm/lep/api/MethodSignature.java b/xm-commons-lep/src/main/java/com/icthh/xm/lep/api/MethodSignature.java index 07400c73..1ba325a3 100644 --- a/xm-commons-lep/src/main/java/com/icthh/xm/lep/api/MethodSignature.java +++ b/xm-commons-lep/src/main/java/com/icthh/xm/lep/api/MethodSignature.java @@ -116,4 +116,5 @@ public interface MethodSignature { */ Method getMethod(); + String getLepContextMethodParameter(); }