CE-1955 - Add back to processes

This commit is contained in:
2024-11-27 14:59:57 -06:00
parent 9213b8987b
commit 8c6b4e6863
10 changed files with 437 additions and 28 deletions

View File

@ -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`.

View File

@ -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<>();

View File

@ -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);
}
}

View File

@ -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

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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());
}
}

View File

@ -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);
}

View File

@ -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;

View File

@ -58,6 +58,8 @@ public interface ProcessInitOrStepOrStatusOutputInterface extends AbstractMiddle
*******************************************************************************/
void setNextStep(String nextStep);
// todo - add (in next version?) backStep
/*******************************************************************************
** Setter for values
*******************************************************************************/