All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.fivefaces.cloud.workflow.awsonprem.impl.StateMachineExecutorImpl Maven / Gradle / Ivy

There is a newer version: 1.0.0
Show newest version
package com.fivefaces.cloud.workflow.awsonprem.impl;

import com.amazonaws.services.stepfunctions.builder.StateMachine;
import com.amazonaws.services.stepfunctions.builder.states.*;
import com.fivefaces.cloud.workflow.awsonprem.ChoiceStateExecutor;
import com.fivefaces.cloud.workflow.awsonprem.PassStateInvoker;
import com.fivefaces.cloud.workflow.awsonprem.StateMachineExecutor;
import com.fivefaces.cloud.workflow.awsonprem.TaskStateInvoker;
import com.fivefaces.cloud.workflow.awsonprem.exception.StateMachineFailedException;
import com.fivefaces.cloud.workflow.awsonprem.model.Execution;
import com.fivefaces.cloud.workflow.awsonprem.model.ExecutionResult;
import com.fivefaces.cloud.workflow.awsonprem.model.ExecutionResultStatus;
import com.fivefaces.cloud.workflow.awsonprem.utils.JsonPathUtils;
import com.jayway.jsonpath.Configuration;
import com.jayway.jsonpath.JsonPath;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;

import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

import static com.fivefaces.cloud.workflow.awsonprem.StateMachineConstants.CAUSE;
import static com.fivefaces.cloud.workflow.awsonprem.StateMachineConstants.ERROR;
import static com.jayway.jsonpath.Option.SUPPRESS_EXCEPTIONS;

/*
Limitations:
1. Most Choice computations are not implemented until they are needed.
2. Parallel, and Wait state types are not implemented.
3. InputPath and OutputPath are not implemented.
4. Retry, Catch are not implemented.
Spec: https://states-language.net/
*/

@Service
@RequiredArgsConstructor
@Slf4j
public class StateMachineExecutorImpl implements StateMachineExecutor {

    private final TaskStateInvoker taskStateInvoker;
    private final PassStateInvoker passStateInvoker;
    private final ChoiceStateExecutor choiceStateExecutor;

    @Override
    public ExecutionResult execute(StateMachine stateMachine, Execution execution) {
        try {
            executeState(stateMachine.getStartAt(), stateMachine.getStates(), execution);
            ExecutionResult result = new ExecutionResult();
            result.setStatus(ExecutionResultStatus.SUCCEEDED);
            result.setOutput(execution.getInput().jsonString());
            if (execution.debug()) {
                execution.finishLog();
                result.setDebug(execution.getLogs());
            }
            return result;
        } catch (StateMachineFailedException ex) {
            ExecutionResult result = new ExecutionResult();
            result.setStatus(ExecutionResultStatus.FAILED);
            result.setOutput(execution.getInput().jsonString());
            if (execution.debug()) {
                execution.finishLog();
                result.setDebug(execution.getLogs());
                result.setErrorMessage(ex.getMessage());
                result.setFailingState(execution.getCurrentStateLog() != null ? execution.getCurrentStateLog().getName() : "INIT");
            }
            return result;
        }
    }

    private void executeState(String stateName, Map states, Execution execution) {
        State state = states.get(stateName);
        if (execution.debug()) {
            execution.addLog(stateName);
        }
        switch (state.getType()) {
            case State.TASK:
                executeState((TaskState) state, states, execution);
                break;
            case State.CHOICE:
                executeState((ChoiceState) state, states, execution);
                break;
            case State.PASS:
                executeState((PassState) state, states, execution);
                break;
            case State.MAP:
                executeState((MapState) state, states, execution);
                break;
            case State.FAIL:
                executeState((FailState) state, execution);
            case State.SUCCEED:
                break;
            default:
                throw new IllegalStateException("Could not understand state with type " + state.getType());
        }
    }

    private void executeState(TaskState state, Map states, Execution execution) {
        Object result = taskStateInvoker.execute(state, execution);
        putResult(result, state.getResultPath(), execution);
        transitionState(state.getTransition(), states, execution);
    }

    private void executeState(ChoiceState state, Map states, Execution execution) {
        transitionState(choiceStateExecutor.execute(state, execution), states, execution);
    }

    private void executeState(PassState state, Map states, Execution execution) {
        Object result = passStateInvoker.execute(state, execution);
        putResult(result, state.getResultPath(), execution);
        transitionState(state.getTransition(), states, execution);
    }

    private void executeState(MapState state, Map states, Execution execution) {
        Object itemsObj = execution.getInput().read(JsonPath.compile(state.getItemsPath()));
        if (!(itemsObj instanceof Collection)) {
            throw new StateMachineFailedException("ItemsPath does not result in an array");
        }
        Collection items = (Collection) itemsObj;
        List mappedItems = items.stream()
                .map(item -> {
                    Execution itemExecution = new Execution(execution.getJsonUtil());
                    itemExecution.setContext(execution.getContext());
                    itemExecution.setInput(JsonPath.parse(item,
                            new Configuration.ConfigurationBuilder().options(SUPPRESS_EXCEPTIONS).build()));
                    executeState(state.getIterator().getStartAt(), state.getIterator().getStates(), itemExecution);
                    execution.addSubProcessLogs(itemExecution.getLogs());
                    return itemExecution.getInput().json();
                })
                .collect(Collectors.toUnmodifiableList());
        putResult(mappedItems, state.getResultPath(), execution);
        transitionState(state.getTransition(), states, execution);
    }

    private void executeState(FailState state, Execution execution) {
        Map result = new HashMap<>() {{
            put(ERROR, state.getError());
            put(CAUSE, state.getCause());
        }};
        putResult(result, "$", execution);
        throw new StateMachineFailedException("Reached a fail status");
    }

    private void putResult(Object result, String resultPath, Execution execution) {
        if (result == null) {
            return;
        }
        if (StringUtils.isEmpty(resultPath)) {
            resultPath = "$";
        }
        if ("$".equals(resultPath)) {
            execution.setInput(JsonPath.parse(result,
                    new Configuration.ConfigurationBuilder().options(SUPPRESS_EXCEPTIONS).build()));
        } else {
            JsonPathUtils.forcePut(execution.getInput(), resultPath, result);
        }
    }

    private void transitionState(Transition transition, Map states, Execution execution) {
        if (transition instanceof NextStateTransition) {
            String nextStateName = ((NextStateTransition) transition).getNextStateName();
            executeState(nextStateName, states, execution);
        } else if (transition instanceof EndTransition) {
            log.info("Ending state machine execution due to EndTransition");
        } else {
            throw new IllegalStateException("Could not understand transition " + transition.getClass());
        }
    }
}