From eb8bf12047d8ed83ce6cfe84b4540491d7d89e12 Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Tue, 28 May 2024 16:50:26 -0500 Subject: [PATCH] CE-938 Adding cancel-process action, cancelStep meta-data --- .../processes/CancelProcessAction.java | 110 ++++++++++++++ .../processes/RunBackendStepAction.java | 13 +- .../core/instances/QInstanceValidator.java | 38 +++-- .../metadata/processes/QProcessMetaData.java | 34 +++++ .../processes/CancelProcessActionTest.java | 139 ++++++++++++++++++ .../instances/QInstanceValidatorTest.java | 27 +++- .../javalin/QJavalinProcessHandler.java | 28 ++++ .../javalin/QJavalinProcessHandlerTest.java | 42 ++++++ 8 files changed, 419 insertions(+), 12 deletions(-) create mode 100644 qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/processes/CancelProcessAction.java create mode 100644 qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/processes/CancelProcessActionTest.java diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/processes/CancelProcessAction.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/processes/CancelProcessAction.java new file mode 100644 index 00000000..1e70f88c --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/processes/CancelProcessAction.java @@ -0,0 +1,110 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2022. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.core.actions.processes; + + +import java.util.Optional; +import java.util.UUID; +import com.kingsrook.qqq.backend.core.actions.ActionHelper; +import com.kingsrook.qqq.backend.core.exceptions.QBadRequestException; +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.logging.QLogger; +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.processes.QProcessMetaData; +import com.kingsrook.qqq.backend.core.state.StateType; +import com.kingsrook.qqq.backend.core.state.UUIDAndTypeStateKey; +import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair; + + +/******************************************************************************* + ** Action handler for running the cancel step of a qqq process + * + *******************************************************************************/ +public class CancelProcessAction extends RunProcessAction +{ + private static final QLogger LOG = QLogger.getLogger(CancelProcessAction.class); + + + + /******************************************************************************* + ** + *******************************************************************************/ + public RunProcessOutput execute(RunProcessInput runProcessInput) throws QException + { + ActionHelper.validateSession(runProcessInput); + + QProcessMetaData process = runProcessInput.getInstance().getProcess(runProcessInput.getProcessName()); + if(process == null) + { + throw new QBadRequestException("Process [" + runProcessInput.getProcessName() + "] is not defined in this instance."); + } + + if(runProcessInput.getProcessUUID() == null) + { + throw (new QBadRequestException("Cannot cancel process - processUUID was not given.")); + } + + UUIDAndTypeStateKey stateKey = new UUIDAndTypeStateKey(UUID.fromString(runProcessInput.getProcessUUID()), StateType.PROCESS_STATUS); + Optional processState = getState(runProcessInput.getProcessUUID()); + if(processState.isEmpty()) + { + throw (new QBadRequestException("Cannot cancel process - State for process UUID [" + runProcessInput.getProcessUUID() + "] was not found.")); + } + + RunProcessOutput runProcessOutput = new RunProcessOutput(); + try + { + if(process.getCancelStep() != null) + { + LOG.info("Running cancel step for process", logPair("processName", process.getName())); + runBackendStep(runProcessInput, process, runProcessOutput, stateKey, process.getCancelStep(), process, processState.get()); + } + else + { + LOG.debug("Process does not have a custom cancel step to run.", logPair("processName", process.getName())); + } + } + catch(QException qe) + { + //////////////////////////////////////////////////////////// + // upon exception (e.g., one thrown by a step), throw it. // + //////////////////////////////////////////////////////////// + throw (qe); + } + catch(Exception e) + { + throw (new QException("Error cancelling process", e)); + } + finally + { + ////////////////////////////////////////////////////// + // always put the final state in the process result // + ////////////////////////////////////////////////////// + runProcessOutput.setProcessState(processState.get()); + } + + return (runProcessOutput); + } + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/processes/RunBackendStepAction.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/processes/RunBackendStepAction.java index fc23904d..306d365b 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/processes/RunBackendStepAction.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/processes/RunBackendStepAction.java @@ -26,6 +26,7 @@ import java.io.Serializable; import java.util.ArrayList; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.stream.Collectors; import com.kingsrook.qqq.backend.core.actions.ActionHelper; import com.kingsrook.qqq.backend.core.actions.tables.QueryAction; @@ -71,7 +72,17 @@ public class RunBackendStepAction QStepMetaData stepMetaData = process.getStep(runBackendStepInput.getStepName()); if(stepMetaData == null) { - throw new QException("Step [" + runBackendStepInput.getStepName() + "] is not defined in the process [" + process.getName() + "]"); + if(process.getCancelStep() != null && Objects.equals(process.getCancelStep().getName(), runBackendStepInput.getStepName())) + { + ///////////////////////////////////// + // special case for cancel step... // + ///////////////////////////////////// + stepMetaData = process.getCancelStep(); + } + else + { + throw new QException("Step [" + runBackendStepInput.getStepName() + "] is not defined in the process [" + process.getName() + "]"); + } } if(!(stepMetaData instanceof QBackendStepMetaData backendStepMetaData)) diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidator.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidator.java index 275a4a91..765e8e7c 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidator.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidator.java @@ -109,6 +109,7 @@ import com.kingsrook.qqq.backend.core.utils.StringUtils; import com.kingsrook.qqq.backend.core.utils.ValueUtils; import com.kingsrook.qqq.backend.core.utils.lambdas.UnsafeLambda; import org.quartz.CronExpression; +import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair; /******************************************************************************* @@ -151,6 +152,7 @@ public class QInstanceValidator // once, during the enrichment/validation work, so, capture it, and store it back in the instance. // ///////////////////////////////////////////////////////////////////////////////////////////////////// JoinGraph joinGraph = null; + long start = System.currentTimeMillis(); try { ///////////////////////////////////////////////////////////////////////////////////////////////// @@ -191,6 +193,9 @@ public class QInstanceValidator validateUniqueTopLevelNames(qInstance); runPlugins(QInstance.class, qInstance, qInstance); + + long end = System.currentTimeMillis(); + LOG.info("Validation (and enrichment) performance", logPair("millis", (end - start))); } catch(Exception e) { @@ -668,17 +673,20 @@ public class QInstanceValidator { if(assertCondition(CollectionUtils.nullSafeHasContents(exposedJoin.getJoinPath()), joinPrefix + "is missing a joinPath.")) { - joinConnectionsForTable = Objects.requireNonNullElseGet(joinConnectionsForTable, () -> joinGraph.getJoinConnections(table.getName())); - - boolean foundJoinConnection = false; - for(JoinGraph.JoinConnectionList joinConnectionList : joinConnectionsForTable) + if(joinGraph != null) { - if(joinConnectionList.matchesJoinPath(exposedJoin.getJoinPath())) + joinConnectionsForTable = Objects.requireNonNullElseGet(joinConnectionsForTable, () -> joinGraph.getJoinConnections(table.getName())); + + boolean foundJoinConnection = false; + for(JoinGraph.JoinConnectionList joinConnectionList : joinConnectionsForTable) { - foundJoinConnection = true; + if(joinConnectionList.matchesJoinPath(exposedJoin.getJoinPath())) + { + foundJoinConnection = true; + } } + assertCondition(foundJoinConnection, joinPrefix + "specified a joinPath [" + exposedJoin.getJoinPath() + "] which does not match a valid join connection in the instance."); } - assertCondition(foundJoinConnection, joinPrefix + "specified a joinPath [" + exposedJoin.getJoinPath() + "] which does not match a valid join connection in the instance."); assertCondition(!usedJoinPaths.contains(exposedJoin.getJoinPath()), tablePrefix + "has more than one join with the joinPath: " + exposedJoin.getJoinPath()); usedJoinPaths.add(exposedJoin.getJoinPath()); @@ -1479,7 +1487,7 @@ public class QInstanceValidator warn("Error loading expectedType for field [" + fieldMetaData.getName() + "] in process [" + processName + "]: " + e.getMessage()); } - validateSimpleCodeReference("Process " + processName + " code reference: ", codeReference, expectedClass); + validateSimpleCodeReference("Process " + processName + " code reference:", codeReference, expectedClass); } } } @@ -1487,6 +1495,14 @@ public class QInstanceValidator } } + if(process.getCancelStep() != null) + { + if(assertCondition(process.getCancelStep().getCode() != null, "Cancel step is missing a code reference, in process " + processName)) + { + validateSimpleCodeReference("Process " + processName + " cancel step code reference: ", process.getCancelStep().getCode(), BackendStep.class); + } + } + /////////////////////////////////////////////////////////////////////////////// // if the process has a schedule, make sure required schedule data populated // /////////////////////////////////////////////////////////////////////////////// @@ -1498,7 +1514,11 @@ public class QInstanceValidator if(process.getVariantBackend() != null) { - assertCondition(qInstance.getBackend(process.getVariantBackend()) != null, "Process " + processName + ", a variant backend was not found named " + process.getVariantBackend()); + if(qInstance.getBackends() != null) + { + assertCondition(qInstance.getBackend(process.getVariantBackend()) != null, "Process " + processName + ", a variant backend was not found named " + process.getVariantBackend()); + } + assertCondition(process.getVariantRunStrategy() != null, "A variant run strategy was not set for process " + processName + " (which does specify a variant backend)"); } else diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/processes/QProcessMetaData.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/processes/QProcessMetaData.java index 44c9259b..fc7c7687 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/processes/QProcessMetaData.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/processes/QProcessMetaData.java @@ -60,6 +60,8 @@ public class QProcessMetaData implements QAppChildMetaData, MetaDataWithPermissi private List stepList; // these are the steps that are ran, by-default, in the order they are ran in private Map steps; // this is the full map of possible steps + private QBackendStepMetaData cancelStep; + private QIcon icon; private QScheduleMetaData schedule; @@ -675,6 +677,7 @@ public class QProcessMetaData implements QAppChildMetaData, MetaDataWithPermissi } + /******************************************************************************* ** Getter for variantRunStrategy *******************************************************************************/ @@ -746,4 +749,35 @@ public class QProcessMetaData implements QAppChildMetaData, MetaDataWithPermissi return steps; } + + + /******************************************************************************* + ** Getter for cancelStep + *******************************************************************************/ + public QBackendStepMetaData getCancelStep() + { + return (this.cancelStep); + } + + + + /******************************************************************************* + ** Setter for cancelStep + *******************************************************************************/ + public void setCancelStep(QBackendStepMetaData cancelStep) + { + this.cancelStep = cancelStep; + } + + + + /******************************************************************************* + ** Fluent setter for cancelStep + *******************************************************************************/ + public QProcessMetaData withCancelStep(QBackendStepMetaData cancelStep) + { + this.cancelStep = cancelStep; + return (this); + } + } diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/processes/CancelProcessActionTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/processes/CancelProcessActionTest.java new file mode 100644 index 00000000..a26523a3 --- /dev/null +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/processes/CancelProcessActionTest.java @@ -0,0 +1,139 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2024. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.core.actions.processes; + + +import java.util.UUID; +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.logging.QCollectingLogger; +import com.kingsrook.qqq.backend.core.logging.QLogger; +import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepInput; +import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepOutput; +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.QCodeReference; +import com.kingsrook.qqq.backend.core.model.metadata.processes.QBackendStepMetaData; +import com.kingsrook.qqq.backend.core.utils.TestUtils; +import org.junit.jupiter.api.Test; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertEquals; + + +/******************************************************************************* + ** Unit test for CancelProcessAction + *******************************************************************************/ +public class CancelProcessActionTest extends BaseTest +{ + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testBadInputs() + { + RunProcessInput input = new RunProcessInput(); + assertThatThrownBy(() -> new CancelProcessAction().execute(input)) + .hasMessageContaining("Process [null] is not defined"); + + input.setProcessName("foobar"); + assertThatThrownBy(() -> new CancelProcessAction().execute(input)) + .hasMessageContaining("Process [foobar] is not defined"); + + input.setProcessName(TestUtils.PROCESS_NAME_GREET_PEOPLE_INTERACTIVE); + assertThatThrownBy(() -> new CancelProcessAction().execute(input)) + .hasMessageContaining("processUUID was not given"); + + input.setProcessUUID(UUID.randomUUID().toString()); + assertThatThrownBy(() -> new CancelProcessAction().execute(input)) + .hasMessageContaining("State for process UUID") + .hasMessageContaining("was not found"); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void test() throws QException + { + try + { + /////////////////////////////////////////////////////////////// + // start up the process - having it break upon frontend step // + /////////////////////////////////////////////////////////////// + RunProcessInput input = new RunProcessInput(); + input.setProcessName(TestUtils.PROCESS_NAME_GREET_PEOPLE_INTERACTIVE); + input.setFrontendStepBehavior(RunProcessInput.FrontendStepBehavior.BREAK); + + RunProcessOutput runProcessOutput = new RunProcessAction().execute(input); + input.setProcessUUID(runProcessOutput.getProcessUUID()); + + ///////////////////////////////////////////////////////////////////////////////// + // try to run the cancel action, but, with no cancel step, it should exit noop // + ///////////////////////////////////////////////////////////////////////////////// + QCollectingLogger collectingLogger = QLogger.activateCollectingLoggerForClass(CancelProcessAction.class); + new CancelProcessAction().execute(input); + assertThat(collectingLogger.getCollectedMessages()) + .anyMatch(m -> m.getMessage().contains("does not have a custom cancel step")); + collectingLogger.clear(); + + /////////////////////////////////////// + // add a cancel step to this process // + /////////////////////////////////////// + QContext.getQInstance().getProcess(TestUtils.PROCESS_NAME_GREET_PEOPLE_INTERACTIVE) + .setCancelStep(new QBackendStepMetaData().withCode(new QCodeReference(CancelStep.class))); + + new CancelProcessAction().execute(input); + assertThat(collectingLogger.getCollectedMessages()) + .noneMatch(m -> m.getMessage().contains("does not have a custom cancel step")) + .anyMatch(m -> m.getMessage().contains("Running cancel step")); + assertEquals(1, CancelStep.callCount); + } + finally + { + QLogger.deactivateCollectingLoggerForClass(CancelProcessAction.class); + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static class CancelStep implements BackendStep + { + static int callCount = 0; + + + + @Override + public void run(RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput) throws QException + { + callCount++; + } + } + +} \ No newline at end of file diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidatorTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidatorTest.java index 1c6947a3..e7b25bf4 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidatorTest.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidatorTest.java @@ -38,6 +38,7 @@ import com.kingsrook.qqq.backend.core.actions.dashboard.PersonsByCreateDateBarCh import com.kingsrook.qqq.backend.core.actions.dashboard.widgets.AbstractWidgetRenderer; import com.kingsrook.qqq.backend.core.actions.dashboard.widgets.ParentWidgetRenderer; import com.kingsrook.qqq.backend.core.actions.processes.BackendStep; +import com.kingsrook.qqq.backend.core.actions.processes.CancelProcessActionTest; import com.kingsrook.qqq.backend.core.context.QContext; import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.exceptions.QInstanceValidationException; @@ -69,6 +70,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.layout.QIcon; import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValue; import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSource; import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSourceType; +import com.kingsrook.qqq.backend.core.model.metadata.processes.QBackendStepMetaData; import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData; import com.kingsrook.qqq.backend.core.model.metadata.queues.SQSQueueProviderMetaData; import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportDataSource; @@ -154,7 +156,7 @@ public class QInstanceValidatorTest extends BaseTest @Test public void test_validateEmptyBackends() { - assertValidationFailureReasons((qInstance) -> qInstance.setBackends(new HashMap<>()), + assertValidationFailureReasonsAllowingExtraReasons((qInstance) -> qInstance.setBackends(new HashMap<>()), "At least 1 backend must be defined"); } @@ -393,6 +395,26 @@ public class QInstanceValidatorTest extends BaseTest + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void test_validateProcessCancelSteps() + { + assertValidationFailureReasons((qInstance) -> qInstance.getProcess(TestUtils.PROCESS_NAME_GREET_PEOPLE).withCancelStep(new QBackendStepMetaData()), + "Cancel step is missing a code reference"); + + assertValidationFailureReasons((qInstance) -> qInstance.getProcess(TestUtils.PROCESS_NAME_GREET_PEOPLE).withCancelStep(new QBackendStepMetaData().withCode(new QCodeReference())), + "missing a code reference name", "missing a code type"); + + assertValidationFailureReasons((qInstance) -> qInstance.getProcess(TestUtils.PROCESS_NAME_GREET_PEOPLE).withCancelStep(new QBackendStepMetaData().withCode(new QCodeReference(ValidAuthCustomizer.class))), + "CodeReference is not of the expected type"); + + assertValidationSuccess((qInstance) -> qInstance.getProcess(TestUtils.PROCESS_NAME_GREET_PEOPLE).withCancelStep(new QBackendStepMetaData().withCode(new QCodeReference(CancelProcessActionTest.CancelStep.class)))); + } + + + /******************************************************************************* ** *******************************************************************************/ @@ -537,7 +559,8 @@ public class QInstanceValidatorTest extends BaseTest //////////////////////////////////////////////////// // make sure if remove all plugins, we don't fail // //////////////////////////////////////////////////// - assertValidationSuccess((qInstance) -> {}); + assertValidationSuccess((qInstance) -> { + }); } } 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 957b5a21..5faa8c08 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 @@ -45,6 +45,7 @@ import com.kingsrook.qqq.backend.core.actions.async.AsyncJobState; import com.kingsrook.qqq.backend.core.actions.async.AsyncJobStatus; import com.kingsrook.qqq.backend.core.actions.async.JobGoingAsyncException; import com.kingsrook.qqq.backend.core.actions.permissions.PermissionsHelper; +import com.kingsrook.qqq.backend.core.actions.processes.CancelProcessAction; import com.kingsrook.qqq.backend.core.actions.processes.QProcessCallback; import com.kingsrook.qqq.backend.core.actions.processes.RunProcessAction; import com.kingsrook.qqq.backend.core.actions.reporting.GenerateReportAction; @@ -130,6 +131,7 @@ public class QJavalinProcessHandler post("/step/{step}", QJavalinProcessHandler::processStep); get("/status/{jobUUID}", QJavalinProcessHandler::processStatus); get("/records", QJavalinProcessHandler::processRecords); + get("/cancel", QJavalinProcessHandler::processCancel); }); get("/possibleValues/{fieldName}", QJavalinProcessHandler::possibleValues); @@ -768,6 +770,32 @@ public class QJavalinProcessHandler + /******************************************************************************* + ** + *******************************************************************************/ + private static void processCancel(Context context) + { + try + { + RunProcessInput runProcessInput = new RunProcessInput(); + QJavalinImplementation.setupSession(context, runProcessInput); + + runProcessInput.setProcessName(context.pathParam("processName")); + runProcessInput.setProcessUUID(context.pathParam("processUUID")); + + new CancelProcessAction().execute(runProcessInput); + + Map resultForCaller = new HashMap<>(); + context.result(JsonUtils.toJson(resultForCaller)); + } + catch(Exception e) + { + QJavalinImplementation.handleException(context, e); + } + } + + + /******************************************************************************* ** *******************************************************************************/ diff --git a/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/backend/javalin/QJavalinProcessHandlerTest.java b/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/backend/javalin/QJavalinProcessHandlerTest.java index 6a7ec840..74aa407c 100644 --- a/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/backend/javalin/QJavalinProcessHandlerTest.java +++ b/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/backend/javalin/QJavalinProcessHandlerTest.java @@ -25,6 +25,7 @@ package com.kingsrook.qqq.backend.javalin; import java.net.URLEncoder; import java.nio.charset.Charset; import java.util.List; +import java.util.UUID; import com.kingsrook.qqq.backend.core.actions.async.AsyncJobState; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria; @@ -604,4 +605,45 @@ class QJavalinProcessHandlerTest extends QJavalinTestBase assertEquals(1, jsonObject.getJSONArray("options").getJSONObject(0).getInt("id")); assertEquals("Darin Kelkhoff (1)", jsonObject.getJSONArray("options").getJSONObject(0).getString("label")); } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void test_processCancel() + { + ///////////////////////// + // 400s for bad inputs // + ///////////////////////// + { + HttpResponse response = Unirest.get(BASE_URL + "/processes/noSuchProcess/" + UUID.randomUUID() + "/cancel").asString(); + assertEquals(400, response.getStatus()); + assertThat(response.getBody()).contains("Process [noSuchProcess] is not defined in this instance"); + } + { + HttpResponse response = Unirest.get(BASE_URL + "/processes/" + TestUtils.PROCESS_NAME_SIMPLE_SLEEP + "/" + UUID.randomUUID() + "/cancel").asString(); + assertEquals(400, response.getStatus()); + assertThat(response.getBody()).matches(".*State for process UUID.*not found.*"); + } + + /////////////////////////////////// + // start a process, get its uuid // + /////////////////////////////////// + String processBasePath = BASE_URL + "/processes/" + TestUtils.PROCESS_NAME_SLEEP_INTERACTIVE; + HttpResponse response = Unirest.post(processBasePath + "/init?" + TestUtils.SleeperStep.FIELD_SLEEP_MILLIS + "=" + MORE_THAN_TIMEOUT) + .header("Content-Type", "application/json").asString(); + + JSONObject jsonObject = assertProcessStepCompleteResponse(response); + String processUUID = jsonObject.getString("processUUID"); + assertNotNull(processUUID, "Process UUID should not be null."); + + ///////////////////////////////////////// + // now cancel that, and expect success // + ///////////////////////////////////////// + response = Unirest.get(BASE_URL + "/processes/" + TestUtils.PROCESS_NAME_SLEEP_INTERACTIVE + "/" + processUUID + "/cancel").asString(); + assertEquals(200, response.getStatus()); + } + }