mirror of
https://github.com/Kingsrook/qqq.git
synced 2025-07-17 20:50:44 +00:00
CE-1955 - Add back to processes
This commit is contained in:
@ -38,6 +38,13 @@ See {link-permissionRules} for details.
|
||||
*** 1) by a single call to `.withStepList(List<QStepMetaData>)`, 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` - *<<QScheduleMetaData>>* - 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 <<QScheduleMetaData>> 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`.
|
||||
|
||||
|
@ -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<QStepMetaData> stepList = getAvailableStepList(processState, process, lastStepName);
|
||||
List<QStepMetaData> 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<QStepMetaData> getAvailableStepList(ProcessState processState, QProcessMetaData process, String lastStep) throws QException
|
||||
static List<QStepMetaData> 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<QStepMetaData> stepNamesToSteps(QProcessMetaData process, List<String> stepNames) throws QException
|
||||
private static List<QStepMetaData> stepNamesToSteps(QProcessMetaData process, List<String> stepNames) throws QException
|
||||
{
|
||||
List<QStepMetaData> result = new ArrayList<>();
|
||||
|
||||
|
@ -40,6 +40,8 @@ public class ProcessState implements Serializable
|
||||
private Map<String, Serializable> values = new HashMap<>();
|
||||
private List<String> stepList = new ArrayList<>();
|
||||
private Optional<String> nextStepName = Optional.empty();
|
||||
private Optional<String> 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<String> 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);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -48,6 +48,7 @@ public class QFrontendStepMetaData extends QStepMetaData
|
||||
private Map<String, QFieldMetaData> formFieldMap;
|
||||
|
||||
private String format;
|
||||
private String backStepName;
|
||||
|
||||
private List<QHelpContent> 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);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -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<BackendStep>((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<BackendStep>((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<BackendStep>((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<BackendStep>((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<BackendStep>((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<BackendStep>((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<BackendStep>((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<BackendStep>((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<String, Integer> 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<String> expectedNames, List<QStepMetaData> actualSteps)
|
||||
{
|
||||
assertEquals(expectedNames, actualSteps.stream().map(s -> s.getName()).toList());
|
||||
}
|
||||
|
||||
}
|
@ -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<String, Object> 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);
|
||||
}
|
||||
|
||||
|
||||
|
@ -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;
|
||||
|
||||
|
@ -58,6 +58,8 @@ public interface ProcessInitOrStepOrStatusOutputInterface extends AbstractMiddle
|
||||
*******************************************************************************/
|
||||
void setNextStep(String nextStep);
|
||||
|
||||
// todo - add (in next version?) backStep
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for values
|
||||
*******************************************************************************/
|
||||
|
Reference in New Issue
Block a user