diff --git a/docs/metaData/Processes.adoc b/docs/metaData/Processes.adoc index 7985fb73..789bd6d6 100644 --- a/docs/metaData/Processes.adoc +++ b/docs/metaData/Processes.adoc @@ -38,6 +38,13 @@ See {link-permissionRules} for details. *** 1) by a single call to `.withStepList(List)`, which internally adds each step into the `steps` map. *** 2) by multiple calls to `.addStep(QStepMetaData)`, which adds a step to both the `stepList` and `steps` map. ** If a process also needs optional steps (for a <<_custom_process_flow>>), they should be added by a call to `.addOptionalStep(QStepMetaData)`, which only places them in the `steps` map. +* `stepFlow` - *enum, default LINEAR* - specifies the the flow-control logic between steps. Possible values are: +** `LINEAR` - steps are executed in-order, through the `stepList`. +A backend step _can_ customize the `nextStepName` or re-order the `stepList`, if needed. +In a frontend step, a user may be given the option to go _back_ to a previous step as well. +** `STATE_MACHINE` - steps are executed as a Fine State Machine, starting with the first step in `stepList`, +but then proceeding based on the `nextStepName` specified by the previous step. +Thus allowing much more flexible flows. * `schedule` - *<>* - set up the process to run automatically on the specified schedule. See below for details. * `minInputRecords` - *Integer* - #not used...# @@ -67,6 +74,11 @@ For processes with a user-interface, they must define one or more "screens" in t * `formFields` - *List of String* - list of field names used by the screen as form-inputs. * `viewFields` - *List of String* - list of field names used by the screen as visible outputs. * `recordListFields` - *List of String* - list of field names used by the screen in a record listing. +* `format` - *Optional String* - directive for a frontend to use specialized formatting for the display of the process. +** Consult frontend documentation for supported values and their meanings. +* `backStepName` - *Optional String* - For processes using `LINEAR` flow, if this value is given, +then the frontend should offer a control that the user can take (e.g., a button) to move back to an +earlier step in the process. ==== QFrontendComponentMetaData @@ -90,10 +102,13 @@ Expects a process value named `html`. Expects process values named `downloadFileName` and `serverFilePath`. ** `GOOGLE_DRIVE_SELECT_FOLDER` - Special form that presents a UI from Google Drive, where the user can select a folder (e.g., as a target for uploading files in a subsequent backend step). ** `BULK_EDIT_FORM` - For use by the standard QQQ Bulk Edit process. +** `BULK_LOAD_FILE_MAPPING_FORM`, `BULK_LOAD_VALUE_MAPPING_FORM`, or `BULK_LOAD_PROFILE_FORM` - For use by the standard QQQ Bulk Load process. ** `VALIDATION_REVIEW_SCREEN` - For use by the QQQ Streamed ETL With Frontend process family of processes. Displays a component prompting the user to run full validation or to skip it, or, if full validation has been ran, then showing the results of that validation. ** `PROCESS_SUMMARY_RESULTS` - For use by the QQQ Streamed ETL With Frontend process family of processes. Displays the summary results of running the process. +** `WIDGET` - Render a QQQ Widget. +Requires that `widgetName` be given as a value for the component. ** `RECORD_LIST` - _Deprecated. Showed a grid with a list of records as populated by the process._ * `values` - *Map of String → Serializable* - Key=value pairs, with different expectations based on the component's `type`. @@ -116,6 +131,27 @@ It can be used, however, for example, to cause a `defaultValue` to be applied to It can also be used to cause the process to throw an error, if a field is marked as `isRequired`, but a value is not present. ** `recordListMetaData` - *RecordListMetaData object* - _Not used at this time._ +==== QStateMachineStep + +Processes that use `flow = STATE_MACHINE` should use process steps of type `QStateMachineStep`. + +A common pattern seen in state-machine processes, is that they will present a frontend-step to a user, +then always run a given backend-step in response to that screen which the user submitted. +Inside that backend-step, custom application logic will determine the next state to go to, +which is typically another frontend-step (which would then submit data to its corresponding backend-step, +and continue the FSM). + +To help facilitate this pattern, factory methods exist on `QStateMachineStep`, +for constructing the commonly-expected types of state-machine steps: + +* `frontendThenBackend(name, frontendStep, backendStep)` - for the frontend-then-backend pattern described above. +* `backendOnly(name, backendStep)` - for a state that only has a backend step. +This might be useful as a “reset” step, to run before restarting a state-loop. +* `frontendOnly(name, frontendStep)` - for a state that only has a frontend step, +which would always be followed by another state, which must be specified as the `defaultNextStepName` +on the `QStateMachineStep`. + + ==== BasepullConfiguration A "Basepull" process is a common pattern where an application needs to perform some action on all new (or updated) records from a particular data source. @@ -218,12 +254,10 @@ But for some cases, doing page-level transactions can reduce long-transactions a * `withSchedule(QScheduleMetaData schedule)` - Add a <> to the process. [#_custom_process_flow] -==== Custom Process Flow -As referenced in the definition of the <<_QProcessMetaData_Properties,QProcessMetaData Properties>>, by default, a process -will execute each of its steps in-order, as defined in the `stepList` property. -However, a Backend Step can customize this flow #todo - write more clearly here... - -There are generally 2 method to call (in a `BackendStep`) to do a dynamic flow: +==== How to customize a Linear process flow +As referenced in the definition of the <<_QProcessMetaData_Properties,QProcessMetaData Properties>>, by default, +(with `flow = LINEAR`) a process will execute each of its steps in-order, as defined in the `stepList` property. +However, a Backend Step can customize this flow as follows: * `RunBackendStepOutput.setOverrideLastStepName(String stepName)` ** QQQ's `RunProcessAction` keeps track of which step it "last" ran, e.g., to tell it which one to run next. @@ -239,7 +273,7 @@ does need to be found in the new `stepNameList` - otherwise, the framework will for figuring out where to go next. [source,java] -.Example of a defining process that can use a flexible flow: +.Example of a defining process that can use a customized linear flow: ---- // for a case like this, it would be recommended to define all step names in constants: public final static String STEP_START = "start"; @@ -324,4 +358,21 @@ public static class StartStep implements BackendStep } ---- +[#_process_back] +==== How to allow a process to go back + +The simplest option to allow a process to present a "Back" button to users, +thus allowing them to move backward through a process +(e.g., from a review screen back to an earlier input screen), is to set the property `backStepName` +on a `QFrontendStepMetaData`. + +If the step that is executed after the user hits "Back" is a backend step, then within that +step, `runBackendStepInput.getIsStepBack()` will return `true` (but ONLY within that first step after +the user hits "Back"). It may be necessary within individual processes to be aware that the user +has chosen to go back, to reset certain values in the process's state. + +Alternatively, if a frontend step's "Back" behavior needs to be dynamic (e.g., sometimes not available, +or sometimes targeting different steps in the process), then in a backend step that runs before the +frontend step, a call to `runBackendStepOutput.getProcessState().setBackStepName()` can be made, +to customize the value which would otherwise come from the `QFrontendStepMetaData`. diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/processes/RunProcessAction.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/processes/RunProcessAction.java index 481201e3..a595b4e9 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/processes/RunProcessAction.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/processes/RunProcessAction.java @@ -122,6 +122,12 @@ public class RunProcessAction UUIDAndTypeStateKey stateKey = new UUIDAndTypeStateKey(UUID.fromString(runProcessInput.getProcessUUID()), StateType.PROCESS_STATUS); ProcessState processState = primeProcessState(runProcessInput, stateKey, process); + ///////////////////////////////////////////////////////////////////////////////////////////////////////////// + // these should always be clear when we're starting a run - so make sure they haven't leaked from previous // + ///////////////////////////////////////////////////////////////////////////////////////////////////////////// + processState.clearNextStepName(); + processState.clearBackStepName(); + ///////////////////////////////////////////////////////// // if process is 'basepull' style, keep track of 'now' // ///////////////////////////////////////////////////////// @@ -188,14 +194,35 @@ public class RunProcessAction private void runLinearStepLoop(QProcessMetaData process, ProcessState processState, UUIDAndTypeStateKey stateKey, RunProcessInput runProcessInput, RunProcessOutput runProcessOutput) throws Exception { String lastStepName = runProcessInput.getStartAfterStep(); + String startAtStep = runProcessInput.getStartAtStep(); while(true) { /////////////////////////////////////////////////////////////////////////////////////////////////////// // always refresh the step list - as any step that runs can modify it (in the process state). // // this is why we don't do a loop over the step list - as we'd get ConcurrentModificationExceptions. // + // deal with if we were told, from the input, to start After a step, or start At a step. // /////////////////////////////////////////////////////////////////////////////////////////////////////// - List stepList = getAvailableStepList(processState, process, lastStepName); + List stepList; + if(startAtStep == null) + { + stepList = getAvailableStepList(processState, process, lastStepName, false); + } + else + { + stepList = getAvailableStepList(processState, process, startAtStep, true); + + /////////////////////////////////////////////////////////////////////////////////// + // clear this field - so after we run a step, we'll then loop in last-step mode. // + /////////////////////////////////////////////////////////////////////////////////// + startAtStep = null; + + /////////////////////////////////////////////////////////////////////////////////// + // if we're going to run a backend step now, let it see that this is a step-back // + /////////////////////////////////////////////////////////////////////////////////// + processState.setIsStepBack(true); + } + if(stepList.isEmpty()) { break; @@ -232,7 +259,18 @@ public class RunProcessAction ////////////////////////////////////////////////// throw (new QException("Unsure how to run a step of type: " + step.getClass().getName())); } + + //////////////////////////////////////////////////////////////////////////////////////// + // only let this value be set for the original back step - don't let it stick around. // + // if a process wants to keep track of this itself, it can, but in a different slot. // + //////////////////////////////////////////////////////////////////////////////////////// + processState.setIsStepBack(false); } + + /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // in case we broke from the loop above (e.g., by going directly into a frontend step), once again make sure to lower this flag. // + /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + processState.setIsStepBack(false); } @@ -264,6 +302,12 @@ public class RunProcessAction processFrontendStepFieldDefaultValues(processState, step); processFrontendComponents(processState, step); processState.setNextStepName(step.getName()); + + if(StringUtils.hasContent(step.getBackStepName()) && processState.getBackStepName().isEmpty()) + { + processState.setBackStepName(step.getBackStepName()); + } + return LoopTodo.BREAK; } case SKIP -> @@ -317,6 +361,7 @@ public class RunProcessAction // else run the given lastStepName // ///////////////////////////////////// processState.clearNextStepName(); + processState.clearBackStepName(); step = process.getStep(lastStepName); if(step == null) { @@ -398,6 +443,7 @@ public class RunProcessAction // its sub-steps, or, to fall out of the loop and end the process. // ////////////////////////////////////////////////////////////////////////////////////////////////////// processState.clearNextStepName(); + processState.clearBackStepName(); runStateMachineStep(nextStepName.get(), process, processState, stateKey, runProcessInput, runProcessOutput, stackDepth + 1); return; } @@ -621,8 +667,10 @@ public class RunProcessAction /******************************************************************************* ** Get the list of steps which are eligible to run. + ** + ** lastStep will be included in the list, or not, based on includeLastStep. *******************************************************************************/ - private List getAvailableStepList(ProcessState processState, QProcessMetaData process, String lastStep) throws QException + static List getAvailableStepList(ProcessState processState, QProcessMetaData process, String lastStep, boolean includeLastStep) throws QException { if(lastStep == null) { @@ -649,6 +697,10 @@ public class RunProcessAction if(stepName.equals(lastStep)) { foundLastStep = true; + if(includeLastStep) + { + validStepNames.add(stepName); + } } } return (stepNamesToSteps(process, validStepNames)); @@ -660,7 +712,7 @@ public class RunProcessAction /******************************************************************************* ** *******************************************************************************/ - private List stepNamesToSteps(QProcessMetaData process, List stepNames) throws QException + private static List stepNamesToSteps(QProcessMetaData process, List stepNames) throws QException { List result = new ArrayList<>(); diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/processes/ProcessState.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/processes/ProcessState.java index ad7a0827..c6d07011 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/processes/ProcessState.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/processes/ProcessState.java @@ -40,6 +40,8 @@ public class ProcessState implements Serializable private Map values = new HashMap<>(); private List stepList = new ArrayList<>(); private Optional nextStepName = Optional.empty(); + private Optional backStepName = Optional.empty(); + private boolean isStepBack = false; private ProcessMetaDataAdjustment processMetaDataAdjustment = null; @@ -122,6 +124,39 @@ public class ProcessState implements Serializable + /******************************************************************************* + ** Getter for backStepName + ** + *******************************************************************************/ + public Optional getBackStepName() + { + return backStepName; + } + + + + /******************************************************************************* + ** Setter for backStepName + ** + *******************************************************************************/ + public void setBackStepName(String backStepName) + { + this.backStepName = Optional.of(backStepName); + } + + + + /******************************************************************************* + ** clear out the value of backStepName (set the Optional to empty) + ** + *******************************************************************************/ + public void clearBackStepName() + { + this.backStepName = Optional.empty(); + } + + + /******************************************************************************* ** Getter for stepList ** @@ -176,4 +211,35 @@ public class ProcessState implements Serializable } + + /******************************************************************************* + ** Getter for isStepBack + *******************************************************************************/ + public boolean getIsStepBack() + { + return (this.isStepBack); + } + + + + /******************************************************************************* + ** Setter for isStepBack + *******************************************************************************/ + public void setIsStepBack(boolean isStepBack) + { + this.isStepBack = isStepBack; + } + + + + /******************************************************************************* + ** Fluent setter for isStepBack + *******************************************************************************/ + public ProcessState withIsStepBack(boolean isStepBack) + { + this.isStepBack = isStepBack; + return (this); + } + + } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/processes/RunBackendStepInput.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/processes/RunBackendStepInput.java index bfaad833..81ff1d77 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/processes/RunBackendStepInput.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/processes/RunBackendStepInput.java @@ -419,6 +419,17 @@ public class RunBackendStepInput extends AbstractActionInput + /******************************************************************************* + ** Accessor for processState's isStepBack attribute + ** + *******************************************************************************/ + public boolean getIsStepBack() + { + return processState.getIsStepBack(); + } + + + /******************************************************************************* ** Accessor for processState - protected, because we generally want to access ** its members through wrapper methods, we think diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/processes/RunProcessInput.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/processes/RunProcessInput.java index a099caaf..c9d500f9 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/processes/RunProcessInput.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/processes/RunProcessInput.java @@ -49,6 +49,7 @@ public class RunProcessInput extends AbstractActionInput private ProcessState processState; private FrontendStepBehavior frontendStepBehavior = FrontendStepBehavior.BREAK; private String startAfterStep; + private String startAtStep; private String processUUID; private AsyncJobCallback asyncJobCallback; @@ -451,4 +452,35 @@ public class RunProcessInput extends AbstractActionInput { return asyncJobCallback; } + + /******************************************************************************* + ** Getter for startAtStep + *******************************************************************************/ + public String getStartAtStep() + { + return (this.startAtStep); + } + + + + /******************************************************************************* + ** Setter for startAtStep + *******************************************************************************/ + public void setStartAtStep(String startAtStep) + { + this.startAtStep = startAtStep; + } + + + + /******************************************************************************* + ** Fluent setter for startAtStep + *******************************************************************************/ + public RunProcessInput withStartAtStep(String startAtStep) + { + this.startAtStep = startAtStep; + return (this); + } + + } \ No newline at end of file diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/processes/QFrontendStepMetaData.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/processes/QFrontendStepMetaData.java index 0c07043e..514e4fae 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/processes/QFrontendStepMetaData.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/processes/QFrontendStepMetaData.java @@ -48,6 +48,7 @@ public class QFrontendStepMetaData extends QStepMetaData private Map formFieldMap; private String format; + private String backStepName; private List helpContents; @@ -436,4 +437,35 @@ public class QFrontendStepMetaData extends QStepMetaData } + + /******************************************************************************* + ** Getter for backStepName + *******************************************************************************/ + public String getBackStepName() + { + return (this.backStepName); + } + + + + /******************************************************************************* + ** Setter for backStepName + *******************************************************************************/ + public void setBackStepName(String backStepName) + { + this.backStepName = backStepName; + } + + + + /******************************************************************************* + ** Fluent setter for backStepName + *******************************************************************************/ + public QFrontendStepMetaData withBackStepName(String backStepName) + { + this.backStepName = backStepName; + return (this); + } + + } diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/processes/RunProcessActionTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/processes/RunProcessActionTest.java index 1d989f96..bf7eeb0a 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/processes/RunProcessActionTest.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/processes/RunProcessActionTest.java @@ -23,10 +23,15 @@ package com.kingsrook.qqq.backend.core.actions.processes; import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; import com.kingsrook.qqq.backend.core.BaseTest; import com.kingsrook.qqq.backend.core.context.QContext; import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.model.actions.processes.ProcessState; import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessInput; import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessOutput; import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReferenceLambda; @@ -35,6 +40,8 @@ import com.kingsrook.qqq.backend.core.model.metadata.processes.QBackendStepMetaD import com.kingsrook.qqq.backend.core.model.metadata.processes.QFrontendStepMetaData; import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData; import com.kingsrook.qqq.backend.core.model.metadata.processes.QStateMachineStep; +import com.kingsrook.qqq.backend.core.model.metadata.processes.QStepMetaData; +import com.kingsrook.qqq.backend.core.utils.collections.MultiLevelMapHelper; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; @@ -73,14 +80,14 @@ class RunProcessActionTest extends BaseTest ///////////////////////////////////////////////////////////////// // two-steps - a, points at b; b has no next-step, so it exits // ///////////////////////////////////////////////////////////////// - .addStep(QStateMachineStep.backendOnly("a", new QBackendStepMetaData().withName("aBackend") + .withStep(QStateMachineStep.backendOnly("a", new QBackendStepMetaData().withName("aBackend") .withCode(new QCodeReferenceLambda((runBackendStepInput, runBackendStepOutput) -> { log.add("in StepA"); runBackendStepOutput.getProcessState().setNextStepName("b"); })))) - .addStep(QStateMachineStep.backendOnly("b", new QBackendStepMetaData().withName("bBackend") + .withStep(QStateMachineStep.backendOnly("b", new QBackendStepMetaData().withName("bBackend") .withCode(new QCodeReferenceLambda((runBackendStepInput, runBackendStepOutput) -> { log.add("in StepB"); @@ -109,8 +116,8 @@ class RunProcessActionTest extends BaseTest { QProcessMetaData process = new QProcessMetaData().withName("test") - .addStep(QStateMachineStep.frontendOnly("a", new QFrontendStepMetaData().withName("aFrontend")).withDefaultNextStepName("b")) - .addStep(QStateMachineStep.frontendOnly("b", new QFrontendStepMetaData().withName("bFrontend"))) + .withStep(QStateMachineStep.frontendOnly("a", new QFrontendStepMetaData().withName("aFrontend")).withDefaultNextStepName("b")) + .withStep(QStateMachineStep.frontendOnly("b", new QFrontendStepMetaData().withName("bFrontend"))) .withStepFlow(ProcessStepFlow.STATE_MACHINE); @@ -150,7 +157,7 @@ class RunProcessActionTest extends BaseTest // since it never goes to the frontend, it'll stack overflow // // (though we'll catch it ourselves before JVM does) // /////////////////////////////////////////////////////////////// - .addStep(QStateMachineStep.backendOnly("a", new QBackendStepMetaData().withName("aBackend") + .withStep(QStateMachineStep.backendOnly("a", new QBackendStepMetaData().withName("aBackend") .withCode(new QCodeReferenceLambda((runBackendStepInput, runBackendStepOutput) -> { log.add("in StepA"); @@ -193,14 +200,14 @@ class RunProcessActionTest extends BaseTest // since it never goes to the frontend, it'll stack overflow // // (though we'll catch it ourselves before JVM does) // /////////////////////////////////////////////////////////////// - .addStep(QStateMachineStep.backendOnly("a", new QBackendStepMetaData().withName("aBackend") + .withStep(QStateMachineStep.backendOnly("a", new QBackendStepMetaData().withName("aBackend") .withCode(new QCodeReferenceLambda((runBackendStepInput, runBackendStepOutput) -> { log.add("in StepA"); runBackendStepOutput.getProcessState().setNextStepName("b"); })))) - .addStep(QStateMachineStep.backendOnly("b", new QBackendStepMetaData().withName("bBackend") + .withStep(QStateMachineStep.backendOnly("b", new QBackendStepMetaData().withName("bBackend") .withCode(new QCodeReferenceLambda((runBackendStepInput, runBackendStepOutput) -> { log.add("in StepB"); @@ -238,7 +245,7 @@ class RunProcessActionTest extends BaseTest { QProcessMetaData process = new QProcessMetaData().withName("test") - .addStep(QStateMachineStep.frontendThenBackend("a", + .withStep(QStateMachineStep.frontendThenBackend("a", new QFrontendStepMetaData().withName("aFrontend"), new QBackendStepMetaData().withName("aBackend") .withCode(new QCodeReferenceLambda((runBackendStepInput, runBackendStepOutput) -> @@ -247,7 +254,7 @@ class RunProcessActionTest extends BaseTest runBackendStepOutput.getProcessState().setNextStepName("b"); })))) - .addStep(QStateMachineStep.frontendThenBackend("b", + .withStep(QStateMachineStep.frontendThenBackend("b", new QFrontendStepMetaData().withName("bFrontend"), new QBackendStepMetaData().withName("bBackend") .withCode(new QCodeReferenceLambda((runBackendStepInput, runBackendStepOutput) -> @@ -256,7 +263,7 @@ class RunProcessActionTest extends BaseTest runBackendStepOutput.getProcessState().setNextStepName("c"); })))) - .addStep(QStateMachineStep.frontendThenBackend("c", + .withStep(QStateMachineStep.frontendThenBackend("c", new QFrontendStepMetaData().withName("cFrontend"), new QBackendStepMetaData().withName("cBackend") .withCode(new QCodeReferenceLambda((runBackendStepInput, runBackendStepOutput) -> @@ -265,7 +272,7 @@ class RunProcessActionTest extends BaseTest runBackendStepOutput.getProcessState().setNextStepName("d"); })))) - .addStep(QStateMachineStep.frontendOnly("d", + .withStep(QStateMachineStep.frontendOnly("d", new QFrontendStepMetaData().withName("dFrontend"))) .withStepFlow(ProcessStepFlow.STATE_MACHINE); @@ -321,7 +328,132 @@ class RunProcessActionTest extends BaseTest runProcessOutput = new RunProcessAction().execute(input); assertEquals(List.of("in StepA", "in StepB", "in StepC"), log); assertThat(runProcessOutput.getProcessState().getNextStepName()).isEmpty(); + } + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testGoingBack() throws QException + { + AtomicInteger backCount = new AtomicInteger(0); + Map stepRunCounts = new HashMap<>(); + + BackendStep backendStep = (runBackendStepInput, runBackendStepOutput) -> + { + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // shared backend-step lambda, that will do the same thing for both - but using step name to count how many times each is executed. // + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + MultiLevelMapHelper.getOrPutAndIncrement(stepRunCounts, runBackendStepInput.getStepName()); + if(runBackendStepInput.getIsStepBack()) + { + backCount.incrementAndGet(); + } + }; + + /////////////////////////////////////////////////////////// + // normal flow here: a -> b -> c // + // but, b can go back to a, as in: a -> b -> a -> b -> c // + /////////////////////////////////////////////////////////// + QProcessMetaData process = new QProcessMetaData().withName("test") + .withStep(new QBackendStepMetaData() + .withName("a") + .withCode(new QCodeReferenceLambda<>(backendStep))) + .withStep(new QFrontendStepMetaData() + .withName("b") + .withBackStepName("a")) + .withStep(new QBackendStepMetaData() + .withName("c") + .withCode(new QCodeReferenceLambda<>(backendStep))) + .withStepFlow(ProcessStepFlow.LINEAR); + + QContext.getQInstance().addProcess(process); + + RunProcessInput input = new RunProcessInput(); + input.setProcessName("test"); + input.setFrontendStepBehavior(RunProcessInput.FrontendStepBehavior.BREAK); + + /////////////////////////////////////////////////////////// + // start the process - we should be sent to b (frontend) // + /////////////////////////////////////////////////////////// + RunProcessOutput runProcessOutput = new RunProcessAction().execute(input); + assertThat(runProcessOutput.getProcessState().getNextStepName()) + .isPresent().get() + .isEqualTo("b"); + + assertEquals(0, backCount.get()); + assertEquals(Map.of("a", 1), stepRunCounts); + + //////////////////////////////////////////////////////////////// + // resume after b, but in back-mode - should end up back at b // + //////////////////////////////////////////////////////////////// + input.setStartAfterStep(null); + input.setStartAtStep("a"); + runProcessOutput = new RunProcessAction().execute(input); + assertThat(runProcessOutput.getProcessState().getNextStepName()) + .isPresent().get() + .isEqualTo("b"); + + assertEquals(1, backCount.get()); + assertEquals(Map.of("a", 2), stepRunCounts); + + //////////////////////////////////////////////////////////////////////////// + // resume after b, in regular (forward) mode - should wrap up the process // + //////////////////////////////////////////////////////////////////////////// + input.setStartAfterStep("b"); + input.setStartAtStep(null); + runProcessOutput = new RunProcessAction().execute(input); + assertThat(runProcessOutput.getProcessState().getNextStepName()) + .isEmpty(); + + assertEquals(1, backCount.get()); + assertEquals(Map.of("a", 2, "c", 1), stepRunCounts); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testGetAvailableStepList() throws QException + { + QProcessMetaData process = new QProcessMetaData() + .withStep(new QBackendStepMetaData().withName("A")) + .withStep(new QBackendStepMetaData().withName("B")) + .withStep(new QBackendStepMetaData().withName("C")) + .withStep(new QBackendStepMetaData().withName("D")) + .withStep(new QBackendStepMetaData().withName("E")); + + ProcessState processState = new ProcessState(); + processState.setStepList(process.getStepList().stream().map(s -> s.getName()).toList()); + + assertStepListNames(List.of("A", "B", "C", "D", "E"), RunProcessAction.getAvailableStepList(processState, process, null, false)); + assertStepListNames(List.of("A", "B", "C", "D", "E"), RunProcessAction.getAvailableStepList(processState, process, null, true)); + + assertStepListNames(List.of("B", "C", "D", "E"), RunProcessAction.getAvailableStepList(processState, process, "A", false)); + assertStepListNames(List.of("A", "B", "C", "D", "E"), RunProcessAction.getAvailableStepList(processState, process, "A", true)); + + assertStepListNames(List.of("D", "E"), RunProcessAction.getAvailableStepList(processState, process, "C", false)); + assertStepListNames(List.of("C", "D", "E"), RunProcessAction.getAvailableStepList(processState, process, "C", true)); + + assertStepListNames(Collections.emptyList(), RunProcessAction.getAvailableStepList(processState, process, "E", false)); + assertStepListNames(List.of("E"), RunProcessAction.getAvailableStepList(processState, process, "E", true)); + + assertStepListNames(Collections.emptyList(), RunProcessAction.getAvailableStepList(processState, process, "Z", false)); + assertStepListNames(Collections.emptyList(), RunProcessAction.getAvailableStepList(processState, process, "Z", true)); + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + private void assertStepListNames(List expectedNames, List actualSteps) + { + assertEquals(expectedNames, actualSteps.stream().map(s -> s.getName()).toList()); } } \ No newline at end of file diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinProcessHandler.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinProcessHandler.java index ec1c5bbf..1c0bd7b7 100644 --- a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinProcessHandler.java +++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinProcessHandler.java @@ -89,6 +89,7 @@ import io.javalin.apibuilder.EndpointGroup; import io.javalin.http.Context; import io.javalin.http.UploadedFile; import org.apache.commons.lang.NotImplementedException; +import org.apache.commons.lang3.BooleanUtils; import org.eclipse.jetty.http.HttpStatus; import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair; import static io.javalin.apibuilder.ApiBuilder.get; @@ -321,7 +322,7 @@ public class QJavalinProcessHandler *******************************************************************************/ public static void processInit(Context context) { - doProcessInitOrStep(context, null, null, RunProcessInput.FrontendStepBehavior.BREAK); + doProcessInitOrStep(context, null, null, null, RunProcessInput.FrontendStepBehavior.BREAK); } @@ -335,7 +336,7 @@ public class QJavalinProcessHandler *******************************************************************************/ public static void processRun(Context context) { - doProcessInitOrStep(context, null, null, RunProcessInput.FrontendStepBehavior.SKIP); + doProcessInitOrStep(context, null, null, null, RunProcessInput.FrontendStepBehavior.SKIP); } @@ -343,7 +344,7 @@ public class QJavalinProcessHandler /******************************************************************************* ** *******************************************************************************/ - private static void doProcessInitOrStep(Context context, String processUUID, String startAfterStep, RunProcessInput.FrontendStepBehavior frontendStepBehavior) + private static void doProcessInitOrStep(Context context, String processUUID, String startAfterStep, String startAtStep, RunProcessInput.FrontendStepBehavior frontendStepBehavior) { Map resultForCaller = new HashMap<>(); Exception returningException = null; @@ -357,8 +358,23 @@ public class QJavalinProcessHandler resultForCaller.put("processUUID", processUUID); String processName = context.pathParam("processName"); - LOG.info(startAfterStep == null ? "Initiating process [" + processName + "] [" + processUUID + "]" - : "Resuming process [" + processName + "] [" + processUUID + "] after step [" + startAfterStep + "]"); + + if(startAfterStep == null && startAtStep == null) + { + LOG.info("Initiating process [" + processName + "] [" + processUUID + "]"); + } + else if(startAfterStep != null) + { + LOG.info("Resuming process [" + processName + "] [" + processUUID + "] after step [" + startAfterStep + "]"); + } + else if(startAtStep != null) + { + LOG.info("Resuming process [" + processName + "] [" + processUUID + "] at step [" + startAtStep + "]"); + } + else + { + LOG.warn("A logical impossibility was reached, regarding the nullity of startAfterStep and startAtStep, at least given how this code was originally written."); + } RunProcessInput runProcessInput = new RunProcessInput(); QJavalinImplementation.setupSession(context, runProcessInput); @@ -367,11 +383,13 @@ public class QJavalinProcessHandler runProcessInput.setFrontendStepBehavior(frontendStepBehavior); runProcessInput.setProcessUUID(processUUID); runProcessInput.setStartAfterStep(startAfterStep); + runProcessInput.setStartAtStep(startAtStep); populateRunProcessRequestWithValuesFromContext(context, runProcessInput); String reportName = ValueUtils.getValueAsString(runProcessInput.getValue("reportName")); QJavalinAccessLogger.logStart(startAfterStep == null ? "processInit" : "processStep", logPair("processName", processName), logPair("processUUID", processUUID), StringUtils.hasContent(startAfterStep) ? logPair("startAfterStep", startAfterStep) : null, + StringUtils.hasContent(startAtStep) ? logPair("startAtStep", startAfterStep) : null, StringUtils.hasContent(reportName) ? logPair("reportName", reportName) : null); ////////////////////////////////////////////////////////////////////////////////////////////////// @@ -460,6 +478,7 @@ public class QJavalinProcessHandler } resultForCaller.put("values", runProcessOutput.getValues()); runProcessOutput.getProcessState().getNextStepName().ifPresent(nextStep -> resultForCaller.put("nextStep", nextStep)); + runProcessOutput.getProcessState().getBackStepName().ifPresent(backStep -> resultForCaller.put("backStep", backStep)); ///////////////////////////////////////////////////////////////////////////////////////////////////////////// // todo - delete after all frontends look for processMetaDataAdjustment instead of updatedFrontendStepList // @@ -660,8 +679,19 @@ public class QJavalinProcessHandler public static void processStep(Context context) { String processUUID = context.pathParam("processUUID"); - String lastStep = context.pathParam("step"); - doProcessInitOrStep(context, processUUID, lastStep, RunProcessInput.FrontendStepBehavior.BREAK); + + String startAfterStep = null; + String startAtStep = null; + if(BooleanUtils.isTrue(ValueUtils.getValueAsBoolean(context.queryParam("isStepBack")))) + { + startAtStep = context.pathParam("step"); + } + else + { + startAfterStep = context.pathParam("step"); + } + + doProcessInitOrStep(context, processUUID, startAfterStep, startAtStep, RunProcessInput.FrontendStepBehavior.BREAK); } diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/io/ProcessInitOrStepInput.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/io/ProcessInitOrStepInput.java index c138b0bc..372b9aac 100644 --- a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/io/ProcessInitOrStepInput.java +++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/io/ProcessInitOrStepInput.java @@ -45,6 +45,7 @@ public class ProcessInitOrStepInput extends AbstractMiddlewareInput ///////////////////////////////////// private String processUUID; private String startAfterStep; + // todo - add (in next version?) startAtStep (for back) private RunProcessInput.FrontendStepBehavior frontendStepBehavior = RunProcessInput.FrontendStepBehavior.BREAK; diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/io/ProcessInitOrStepOrStatusOutputInterface.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/io/ProcessInitOrStepOrStatusOutputInterface.java index f7d0d4a5..aef6ab44 100644 --- a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/io/ProcessInitOrStepOrStatusOutputInterface.java +++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/io/ProcessInitOrStepOrStatusOutputInterface.java @@ -58,6 +58,8 @@ public interface ProcessInitOrStepOrStatusOutputInterface extends AbstractMiddle *******************************************************************************/ void setNextStep(String nextStep); + // todo - add (in next version?) backStep + /******************************************************************************* ** Setter for values *******************************************************************************/