From 299764b9f1d4e4eab1bc98f62dcab997b23255de Mon Sep 17 00:00:00 2001 From: Francisco Javier Tirado Sarti <65240126+fjtirado@users.noreply.github.com> Date: Thu, 31 Oct 2024 17:32:36 +0100 Subject: [PATCH] [Fix #3733] Collecting more than one error message (#3756) * [Fix #3733] Collecting more than one error message * [Fix #3733] Including process validation check * [Fix #3733] Adding transition check --- .../validation/RuleFlowProcessValidator.java | 10 +++- .../AbstractWorkflowOperationIdFactory.java | 21 ++++--- .../workflow/parser/ParserContext.java | 12 +++- .../parser/ServerlessWorkflowParser.java | 22 +++++-- .../parser/handlers/ActionNodeUtils.java | 17 ++++-- .../handlers/ActionResourceFactory.java | 38 ++++++++----- .../handlers/CompositeContextNodeHandler.java | 4 +- .../parser/handlers/StateHandler.java | 36 +++++++++--- .../parser/handlers/SwitchHandler.java | 2 +- .../handlers/validation/SwitchValidator.java | 20 +++---- .../validation/WorkflowValidator.java | 6 +- .../parser/types/ScriptTypeHandler.java | 4 +- .../parser/types/SysOutTypeHandler.java | 16 +++++- .../validation/SwitchValidatorTest.java | 54 +++++++++++------- .../StaticFluentWorkflowApplicationTest.java | 5 +- .../StaticWorkflowApplicationTest.java | 13 +++++ .../src/test/resources/wrong.sw.json | 57 +++++++++++++++++++ .../workflow/executor/StaticRPCRegister.java | 4 +- 18 files changed, 253 insertions(+), 88 deletions(-) create mode 100644 kogito-serverless-workflow/kogito-serverless-workflow-executor-core/src/test/resources/wrong.sw.json diff --git a/jbpm/jbpm-flow/src/main/java/org/jbpm/ruleflow/core/validation/RuleFlowProcessValidator.java b/jbpm/jbpm-flow/src/main/java/org/jbpm/ruleflow/core/validation/RuleFlowProcessValidator.java index 5319367af5d..ae89938bbae 100755 --- a/jbpm/jbpm-flow/src/main/java/org/jbpm/ruleflow/core/validation/RuleFlowProcessValidator.java +++ b/jbpm/jbpm-flow/src/main/java/org/jbpm/ruleflow/core/validation/RuleFlowProcessValidator.java @@ -115,9 +115,7 @@ public static RuleFlowProcessValidator getInstance() { return INSTANCE; } - public ProcessValidationError[] validateProcess(final RuleFlowProcess process) { - final List errors = new ArrayList<>(); - + public List validateProcess(final RuleFlowProcess process, List errors) { if (process.getName() == null) { errors.add(new ProcessValidationErrorImpl(process, "Process has no name.")); @@ -151,6 +149,12 @@ public ProcessValidationError[] validateProcess(final RuleFlowProcess process) { errors, process); + return errors; + + } + + public ProcessValidationError[] validateProcess(final RuleFlowProcess process) { + final List errors = validateProcess(process, new ArrayList<>()); return errors.toArray(new ProcessValidationError[errors.size()]); } diff --git a/kogito-serverless-workflow/kogito-serverless-workflow-builder/src/main/java/org/kie/kogito/serverless/workflow/operationid/AbstractWorkflowOperationIdFactory.java b/kogito-serverless-workflow/kogito-serverless-workflow-builder/src/main/java/org/kie/kogito/serverless/workflow/operationid/AbstractWorkflowOperationIdFactory.java index 5f4278c92d6..6a71a72b583 100644 --- a/kogito-serverless-workflow/kogito-serverless-workflow-builder/src/main/java/org/kie/kogito/serverless/workflow/operationid/AbstractWorkflowOperationIdFactory.java +++ b/kogito-serverless-workflow/kogito-serverless-workflow-builder/src/main/java/org/kie/kogito/serverless/workflow/operationid/AbstractWorkflowOperationIdFactory.java @@ -44,7 +44,7 @@ public abstract class AbstractWorkflowOperationIdFactory implements WorkflowOper @Override public WorkflowOperationId from(Workflow workflow, FunctionDefinition function, Optional context) { - ActionResource actionResource = ActionResourceFactory.getActionResource(function); + ActionResource actionResource = ActionResourceFactory.getActionResource(function, context); Optional convertedUri = convertURI(workflow, context, actionResource.getUri()); final String fileName; final String uri; @@ -56,14 +56,19 @@ public WorkflowOperationId from(Workflow workflow, FunctionDefinition function, fileName = getFileName(workflow, function, context, uri, actionResource.getOperation(), actionResource.getService()); } if (fileName == null || fileName.isBlank()) { - throw new IllegalArgumentException( - format("Empty file name for function '%s', please review uri '%s' or consider using a different strategy defined in the kogito.sw.operationIdStrategy property", - function.getName(), uri)); + String msg = format("Empty file name for function '%s', please review uri '%s' or consider using a different strategy defined in the kogito.sw.operationIdStrategy property", + function.getName(), uri); + context.ifPresentOrElse(c -> c.addValidationError(msg), () -> { + throw new IllegalArgumentException(msg); + }); } String packageName = onlyChars(removeExt(fileName.toLowerCase())); if (packageName.isBlank()) { - throw new IllegalArgumentException( - format("Empty package for file '%s'. A file name should contain at least one letter which is not part of the extension", fileName)); + String msg = + format("Empty package for file '%s'. A file name should contain at least one letter which is not part of the extension", fileName); + context.ifPresentOrElse(c -> c.addValidationError(msg), () -> { + throw new IllegalArgumentException(msg); + }); } return new WorkflowOperationId(uri, actionResource.getOperation(), actionResource.getService(), fileName, packageName); } @@ -80,7 +85,9 @@ private JsonNode getUriDefinitions(Workflow workflow, Optional co try { definitions = uri == null ? NullNode.instance : ObjectMapperFactory.get().readTree(readBytes(uri, workflow, context)); } catch (IOException e) { - throw new UncheckedIOException(e); + context.ifPresentOrElse(c -> c.addValidationError(e.getMessage()), () -> { + throw new UncheckedIOException(e); + }); } uriDefinitions.setDefinitions(definitions); } diff --git a/kogito-serverless-workflow/kogito-serverless-workflow-builder/src/main/java/org/kie/kogito/serverless/workflow/parser/ParserContext.java b/kogito-serverless-workflow/kogito-serverless-workflow-builder/src/main/java/org/kie/kogito/serverless/workflow/parser/ParserContext.java index 16dee79f093..6b86c08c753 100644 --- a/kogito-serverless-workflow/kogito-serverless-workflow-builder/src/main/java/org/kie/kogito/serverless/workflow/parser/ParserContext.java +++ b/kogito-serverless-workflow/kogito-serverless-workflow-builder/src/main/java/org/kie/kogito/serverless/workflow/parser/ParserContext.java @@ -43,8 +43,9 @@ public class ParserContext { private final NodeIdGenerator idGenerator; private final WorkflowOperationIdFactory operationIdFactory; private final KogitoBuildContext context; - private final Collection generatedFiles; + private final Collection generatedFiles = new ArrayList<>(); private final AsyncInfoResolver asyncInfoResolver; + private final Collection validationErrors = new ArrayList<>(); public static final String ASYNC_CONVERTER_KEY = "asyncInfoConverter"; @@ -60,7 +61,6 @@ public ParserContext(NodeIdGenerator idGenerator, RuleFlowProcessFactory factory this.context = context; this.operationIdFactory = operationIdFactory; this.asyncInfoResolver = asyncInfoResolver; - this.generatedFiles = new ArrayList<>(); } public void add(StateHandler stateHandler) { @@ -114,4 +114,12 @@ public void setCompensation() { public KogitoBuildContext getContext() { return context; } + + public void addValidationError(String message) { + validationErrors.add(message); + } + + public Collection validationErrors() { + return validationErrors; + } } diff --git a/kogito-serverless-workflow/kogito-serverless-workflow-builder/src/main/java/org/kie/kogito/serverless/workflow/parser/ServerlessWorkflowParser.java b/kogito-serverless-workflow/kogito-serverless-workflow-builder/src/main/java/org/kie/kogito/serverless/workflow/parser/ServerlessWorkflowParser.java index 8fd92f82f35..49e16462cc3 100644 --- a/kogito-serverless-workflow/kogito-serverless-workflow-builder/src/main/java/org/kie/kogito/serverless/workflow/parser/ServerlessWorkflowParser.java +++ b/kogito-serverless-workflow/kogito-serverless-workflow-builder/src/main/java/org/kie/kogito/serverless/workflow/parser/ServerlessWorkflowParser.java @@ -20,24 +20,28 @@ import java.io.IOException; import java.io.Reader; -import java.io.UncheckedIOException; import java.net.URI; import java.net.URL; import java.nio.file.Path; +import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.Optional; import java.util.stream.Collectors; import org.jbpm.process.core.datatype.impl.type.ObjectDataType; +import org.jbpm.process.core.validation.ProcessValidationError; +import org.jbpm.process.core.validation.impl.ProcessValidationErrorImpl; import org.jbpm.ruleflow.core.Metadata; import org.jbpm.ruleflow.core.RuleFlowProcessFactory; +import org.jbpm.ruleflow.core.validation.RuleFlowProcessValidator; import org.jbpm.workflow.core.WorkflowModelValidator; import org.kie.kogito.codegen.api.GeneratedInfo; import org.kie.kogito.codegen.api.context.KogitoBuildContext; import org.kie.kogito.internal.process.runtime.KogitoWorkflowProcess; import org.kie.kogito.internal.utils.ConversionUtils; import org.kie.kogito.jackson.utils.ObjectMapperFactory; +import org.kie.kogito.process.validation.ValidationException; import org.kie.kogito.serverless.workflow.SWFConstants; import org.kie.kogito.serverless.workflow.extensions.OutputSchema; import org.kie.kogito.serverless.workflow.operationid.WorkflowOperationIdFactoryProvider; @@ -121,7 +125,7 @@ private ServerlessWorkflowParser(Workflow workflow, KogitoBuildContext context) } private GeneratedInfo parseProcess() { - WorkflowValidator.validateStart(workflow); + RuleFlowProcessFactory factory = RuleFlowProcessFactory.createProcess(workflow.getId(), !workflow.isKeepActive()) .name(workflow.getName() == null ? DEFAULT_NAME : workflow.getName()) .version(workflow.getVersion() == null ? DEFAULT_VERSION : workflow.getVersion()) @@ -134,6 +138,7 @@ private GeneratedInfo parseProcess() { .type(KogitoWorkflowProcess.SW_TYPE); ParserContext parserContext = new ParserContext(idGenerator, factory, context, WorkflowOperationIdFactoryProvider.getFactory(context.getApplicationProperty(WorkflowOperationIdFactoryProvider.PROPERTY_NAME))); + WorkflowValidator.validateStart(workflow, parserContext); modelValidator(parserContext, Optional.ofNullable(workflow.getDataInputSchema())).ifPresent(factory::inputValidator); modelValidator(parserContext, ServerlessWorkflowUtils.getExtension(workflow, OutputSchema.class).map(OutputSchema::getOutputSchema)).ifPresent(factory::outputValidator); loadConstants(factory, parserContext); @@ -145,6 +150,7 @@ private GeneratedInfo parseProcess() { handlers.forEach(StateHandler::handleState); handlers.forEach(StateHandler::handleTransitions); handlers.forEach(StateHandler::handleConnections); + if (parserContext.isCompensation()) { factory.metaData(Metadata.COMPENSATION, true); factory.metaData(Metadata.COMPENSATE_WHEN_ABORTED, true); @@ -170,8 +176,13 @@ private GeneratedInfo parseProcess() { if (!annotations.isEmpty()) { factory.metaData(Metadata.ANNOTATIONS, annotations); } - - return new GeneratedInfo<>(factory.validate().getProcess(), parserContext.generatedFiles()); + factory.link(); + List errors = RuleFlowProcessValidator.getInstance().validateProcess(factory.getProcess(), new ArrayList<>()); + parserContext.validationErrors().forEach(m -> errors.add(new ProcessValidationErrorImpl(factory.getProcess(), m))); + if (!errors.isEmpty()) { + throw new ValidationException(factory.getProcess().getId(), errors); + } + return new GeneratedInfo<>(factory.getProcess(), parserContext.generatedFiles()); } private Optional modelValidator(ParserContext parserContext, Optional schema) { @@ -194,7 +205,8 @@ private void loadConstants(RuleFlowProcessFactory factory, ParserContext parserC try { constants.setConstantsDef(ObjectMapperFactory.get().readValue(readBytes(constants.getRefValue(), workflow, parserContext), JsonNode.class)); } catch (IOException e) { - throw new UncheckedIOException("Invalid file " + constants.getRefValue(), e); + parserContext.addValidationError("Invalid file " + constants.getRefValue() + e); + return; } } factory.metaData(Metadata.CONSTANTS, constants.getConstantsDef()); diff --git a/kogito-serverless-workflow/kogito-serverless-workflow-builder/src/main/java/org/kie/kogito/serverless/workflow/parser/handlers/ActionNodeUtils.java b/kogito-serverless-workflow/kogito-serverless-workflow-builder/src/main/java/org/kie/kogito/serverless/workflow/parser/handlers/ActionNodeUtils.java index 5ecad8e9429..fc0e7468067 100644 --- a/kogito-serverless-workflow/kogito-serverless-workflow-builder/src/main/java/org/kie/kogito/serverless/workflow/parser/handlers/ActionNodeUtils.java +++ b/kogito-serverless-workflow/kogito-serverless-workflow-builder/src/main/java/org/kie/kogito/serverless/workflow/parser/handlers/ActionNodeUtils.java @@ -34,16 +34,21 @@ public class ActionNodeUtils { return embeddedSubProcess.actionNode(context.newId()).name(functionDef.getName()); } - public static void checkArgs(FunctionRef functionRef, String... requiredArgs) { + public static boolean checkArgs(ParserContext context, FunctionRef functionRef, String... requiredArgs) { JsonNode args = functionRef.getArguments(); + boolean isOk = true; if (args == null) { - throw new IllegalArgumentException("Arguments cannot be null for function " + functionRef.getRefName()); - } - for (String arg : requiredArgs) { - if (!args.has(arg)) { - throw new IllegalArgumentException("Missing mandatory " + arg + " argument for function " + functionRef.getRefName()); + context.addValidationError("Arguments cannot be null for function " + functionRef.getRefName()); + isOk = false; + } else { + for (String arg : requiredArgs) { + if (!args.has(arg)) { + context.addValidationError("Missing mandatory " + arg + " argument for function " + functionRef.getRefName()); + isOk = false; + } } } + return isOk; } private ActionNodeUtils() { diff --git a/kogito-serverless-workflow/kogito-serverless-workflow-builder/src/main/java/org/kie/kogito/serverless/workflow/parser/handlers/ActionResourceFactory.java b/kogito-serverless-workflow/kogito-serverless-workflow-builder/src/main/java/org/kie/kogito/serverless/workflow/parser/handlers/ActionResourceFactory.java index ee3fbdb47a6..6f62d751b44 100644 --- a/kogito-serverless-workflow/kogito-serverless-workflow-builder/src/main/java/org/kie/kogito/serverless/workflow/parser/handlers/ActionResourceFactory.java +++ b/kogito-serverless-workflow/kogito-serverless-workflow-builder/src/main/java/org/kie/kogito/serverless/workflow/parser/handlers/ActionResourceFactory.java @@ -19,7 +19,10 @@ package org.kie.kogito.serverless.workflow.parser.handlers; import java.util.Map; -import java.util.function.Function; +import java.util.Optional; +import java.util.function.BiFunction; + +import org.kie.kogito.serverless.workflow.parser.ParserContext; import io.serverlessworkflow.api.functions.FunctionDefinition; import io.serverlessworkflow.api.functions.FunctionDefinition.Type; @@ -28,38 +31,47 @@ public class ActionResourceFactory { - private static final Map> map = + private static final Map, ActionResource>> map = Map.of(FunctionDefinition.Type.REST, ActionResourceFactory::justOperation, FunctionDefinition.Type.ASYNCAPI, ActionResourceFactory::justOperation, FunctionDefinition.Type.RPC, ActionResourceFactory::withService); - private static ActionResource justOperation(String operationStr) { - String[] tokens = getTokens(operationStr, 2); + private static ActionResource justOperation(String operationStr, Optional context) { + String[] tokens = getTokens(operationStr, 2, context); return new ActionResource(tokens[0], tokens[1], null); } - private static ActionResource withService(String operationStr) { - String[] tokens = getTokens(operationStr, 3); + private static ActionResource withService(String operationStr, Optional context) { + String[] tokens = getTokens(operationStr, 3, context); return new ActionResource(tokens[0], tokens[2], tokens[1]); } - private static String[] getTokens(String operationStr, int expectedTokens) { + private static String[] getTokens(String operationStr, int expectedTokens, Optional context) { String[] tokens = operationStr.split(OPERATION_SEPARATOR); if (tokens.length != expectedTokens) { - throw new IllegalArgumentException(String.format("%s should have just %d %s", operationStr, expectedTokens - 1, OPERATION_SEPARATOR)); + String msg = String.format("%s should have just %d %s", operationStr, expectedTokens - 1, OPERATION_SEPARATOR); + context.ifPresentOrElse(c -> c.addValidationError(msg), () -> { + throw new IllegalArgumentException(msg); + }); } return tokens; } - public static ActionResource getActionResource(FunctionDefinition function) { - Function factory = map.get(function.getType()); + public static ActionResource getActionResource(FunctionDefinition function, Optional context) { + BiFunction, ActionResource> factory = map.get(function.getType()); if (factory == null) { - throw new UnsupportedOperationException(function.getType() + " does not support action resources"); + String msg = function.getType() + " does not support action resources"; + context.ifPresentOrElse(c -> c.addValidationError(msg), () -> { + throw new UnsupportedOperationException(msg); + }); } String operation = function.getOperation(); if (operation == null) { - throw new IllegalArgumentException("operation string must not be null for function " + function.getName()); + String msg = "operation string must not be null for function " + function.getName(); + context.ifPresentOrElse(c -> c.addValidationError(msg), () -> { + throw new IllegalArgumentException(msg); + }); } - return factory.apply(operation); + return factory.apply(operation, context); } private ActionResourceFactory() { diff --git a/kogito-serverless-workflow/kogito-serverless-workflow-builder/src/main/java/org/kie/kogito/serverless/workflow/parser/handlers/CompositeContextNodeHandler.java b/kogito-serverless-workflow/kogito-serverless-workflow-builder/src/main/java/org/kie/kogito/serverless/workflow/parser/handlers/CompositeContextNodeHandler.java index 755f00e21aa..49deb2d8e29 100644 --- a/kogito-serverless-workflow/kogito-serverless-workflow-builder/src/main/java/org/kie/kogito/serverless/workflow/parser/handlers/CompositeContextNodeHandler.java +++ b/kogito-serverless-workflow/kogito-serverless-workflow-builder/src/main/java/org/kie/kogito/serverless/workflow/parser/handlers/CompositeContextNodeHandler.java @@ -165,7 +165,7 @@ private MakeNodeResult processActionFilter(RuleFlowNodeContainerFactory em return filterAndMergeNode(embeddedSubProcess, collectVar, fromExpr, resultExpr, toExpr, useData, shouldMerge, (factory, inputVar, outputVar) -> addActionMetadata(getActionNode(factory, action.getSubFlowRef(), inputVar, outputVar), action)); } else { - throw new IllegalArgumentException("Action node " + action.getName() + " of state " + state.getName() + " does not have function or event defined"); + return faultyNodeResult(embeddedSubProcess, "Action node " + action.getName() + " of state " + state.getName() + " does not have function or event defined"); } } @@ -199,7 +199,7 @@ private TimerNodeFactory createTimerNode(RuleFlowNodeContainerFactory f .findFirst() .map(functionDef -> fromFunctionDefinition(embeddedSubProcess, functionDef, functionRef, varInfo)) .or(() -> fromPredefinedFunction(embeddedSubProcess, functionRef, varInfo)) - .orElseThrow(() -> new IllegalArgumentException("Cannot find function " + functionName)); + .orElseGet(() -> faultyNode(embeddedSubProcess, "Cannot find function " + functionName)); } private Stream getFunctionDefStream() { diff --git a/kogito-serverless-workflow/kogito-serverless-workflow-builder/src/main/java/org/kie/kogito/serverless/workflow/parser/handlers/StateHandler.java b/kogito-serverless-workflow/kogito-serverless-workflow-builder/src/main/java/org/kie/kogito/serverless/workflow/parser/handlers/StateHandler.java index 5c9caf6f451..e0dbbf5ee99 100644 --- a/kogito-serverless-workflow/kogito-serverless-workflow-builder/src/main/java/org/kie/kogito/serverless/workflow/parser/handlers/StateHandler.java +++ b/kogito-serverless-workflow/kogito-serverless-workflow-builder/src/main/java/org/kie/kogito/serverless/workflow/parser/handlers/StateHandler.java @@ -21,6 +21,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; +import java.util.Collections; import java.util.List; import java.util.ListIterator; import java.util.Map; @@ -148,7 +149,7 @@ public void handleEnd() { private void handleCompensation(RuleFlowNodeContainerFactory factory) { StateHandler compensation = parserContext.getStateHandler(state.getCompensatedBy()); if (compensation == null) { - throw new IllegalArgumentException("State " + getState().getName() + " refers to a compensation " + state.getCompensatedBy() + " which cannot be found"); + parserContext.addValidationError("State " + getState().getName() + " refers to a compensation " + state.getCompensatedBy() + " which cannot be found"); } parserContext.setCompensation(); WorkflowElementIdentifier eventCompensationId = parserContext.newId(); @@ -164,7 +165,7 @@ private void handleCompensation(RuleFlowNodeContainerFactory factory) { compensation = parserContext.getStateHandler(compensation); while (compensation != null) { if (!compensation.usedForCompensation()) { - throw new IllegalArgumentException( + parserContext.addValidationError( "Compensation state can only have transition to other compensation state. State " + compensation.getState().getName() + " is not used for compensation"); } lastNodeId = handleCompensation(embeddedSubProcess, compensation); @@ -177,7 +178,7 @@ private void handleCompensation(RuleFlowNodeContainerFactory factory) { private WorkflowElementIdentifier handleCompensation(RuleFlowNodeContainerFactory embeddedSubProcess, StateHandler compensation) { if (compensation.getState().getCompensatedBy() != null) { - throw new IllegalArgumentException("Serverless workflow specification forbids nested compensations, hence state " + compensation.getState().getName() + " is not valid"); + parserContext.addValidationError("Serverless workflow specification forbids nested compensations, hence state " + compensation.getState().getName() + " is not valid"); } compensation.handleState(embeddedSubProcess); Transition transition = compensation.getState().getTransition(); @@ -256,11 +257,13 @@ private boolean hasCode(ErrorDefinition errorDef) { protected final Collection getErrorDefinitions(Error error) { Errors errors = workflow.getErrors(); if (errors == null) { - throw new IllegalArgumentException("workflow should contain errors property"); + parserContext.addValidationError("workflow should contain errors property"); + return Collections.emptyList(); } List errorDefs = errors.getErrorDefs(); if (errorDefs == null) { - throw new IllegalArgumentException("workflow errors property must contain errorDefs property"); + parserContext.addValidationError("workflow errors property must contain errorDefs property"); + return Collections.emptyList(); } if (error.getErrorRef() != null) { @@ -268,15 +271,16 @@ protected final Collection getErrorDefinitions(Error error) { } else if (error.getErrorRefs() != null) { return getErrorsDefinitions(errorDefs, error.getErrorRefs()); } else { - throw new IllegalArgumentException("state errors should contain either errorRef or errorRefs property"); + parserContext.addValidationError("state errors should contain either errorRef or errorRefs property"); + return Collections.emptyList(); } } private Collection getErrorsDefinitions(List errorDefs, List errorRefs) { Collection result = new ArrayList<>(); for (String errorRef : errorRefs) { - result.add(errorDefs.stream().filter(errorDef -> errorDef.getName().equals(errorRef) && hasCode(errorDef)).findAny() - .orElseThrow(() -> new IllegalArgumentException("Cannot find any error definition for errorRef" + errorRef))); + errorDefs.stream().filter(errorDef -> errorDef.getName().equals(errorRef) && hasCode(errorDef)).findAny().ifPresentOrElse(result::add, + () -> parserContext.addValidationError("Cannot find any error definition for errorRef" + errorRef)); } return result; } @@ -382,7 +386,12 @@ protected final void handleTransition(RuleFlowNodeContainerFactory factory targetState.connectSource(actionNode); } } else { - callback.ifPresent(HandleTransitionCallBack::onEmptyTarget); + callback.ifPresentOrElse(HandleTransitionCallBack::onEmptyTarget, + () -> { + if (transition != null) { + parserContext.addValidationError(String.format("There is no state for transition %s originated in %s", transition.getNextState(), state.getName())); + } + }); } } @@ -582,4 +591,13 @@ default void onEmptyTarget() { protected static WorkflowElementIdentifier concatId(WorkflowElementIdentifier start, WorkflowElementIdentifier end) { return WorkflowElementIdentifierFactory.fromExternalFormat(start.toSanitizeString() + "_" + end.toSanitizeString()); } + + public MakeNodeResult faultyNodeResult(RuleFlowNodeContainerFactory embeddedSubProcess, String message) { + return new MakeNodeResult(faultyNode(embeddedSubProcess, message)); + } + + public NodeFactory faultyNode(RuleFlowNodeContainerFactory embeddedSubProcess, String message) { + parserContext.addValidationError(message); + return embeddedSubProcess.actionNode(parserContext.newId()); + } } diff --git a/kogito-serverless-workflow/kogito-serverless-workflow-builder/src/main/java/org/kie/kogito/serverless/workflow/parser/handlers/SwitchHandler.java b/kogito-serverless-workflow/kogito-serverless-workflow-builder/src/main/java/org/kie/kogito/serverless/workflow/parser/handlers/SwitchHandler.java index 581bf0d410f..bbf9536938e 100644 --- a/kogito-serverless-workflow/kogito-serverless-workflow-builder/src/main/java/org/kie/kogito/serverless/workflow/parser/handlers/SwitchHandler.java +++ b/kogito-serverless-workflow/kogito-serverless-workflow-builder/src/main/java/org/kie/kogito/serverless/workflow/parser/handlers/SwitchHandler.java @@ -58,7 +58,7 @@ public boolean usedForCompensation() { @Override public MakeNodeResult makeNode(RuleFlowNodeContainerFactory factory) { - validateConditions(state, workflow); + validateConditions(state, workflow, parserContext); SplitFactory splitNode = factory.splitNode(parserContext.newId()); splitNode = isDataBased() ? exclusiveSplitNode(splitNode) : eventBasedExclusiveSplitNode(splitNode); return new MakeNodeResult(splitNode.name(state.getName())); diff --git a/kogito-serverless-workflow/kogito-serverless-workflow-builder/src/main/java/org/kie/kogito/serverless/workflow/parser/handlers/validation/SwitchValidator.java b/kogito-serverless-workflow/kogito-serverless-workflow-builder/src/main/java/org/kie/kogito/serverless/workflow/parser/handlers/validation/SwitchValidator.java index 09cd5c26773..11f07410eb2 100644 --- a/kogito-serverless-workflow/kogito-serverless-workflow-builder/src/main/java/org/kie/kogito/serverless/workflow/parser/handlers/validation/SwitchValidator.java +++ b/kogito-serverless-workflow/kogito-serverless-workflow-builder/src/main/java/org/kie/kogito/serverless/workflow/parser/handlers/validation/SwitchValidator.java @@ -50,12 +50,11 @@ public class SwitchValidator { private SwitchValidator() { } - public static void validateConditions(SwitchState state, Workflow workflow) { + public static void validateConditions(SwitchState state, Workflow workflow, ParserContext context) { if (state.getDataConditions().isEmpty() && state.getEventConditions().isEmpty()) { - throw new IllegalArgumentException(String.format(CONDITIONS_NOT_FOUND_ERROR, state.getName(), workflow.getName())); - } - if (!state.getDataConditions().isEmpty() && !state.getEventConditions().isEmpty()) { - throw new IllegalArgumentException(String.format(DATA_CONDITIONS_AND_EVENT_CONDITIONS_FOUND_ERROR, state.getName(), workflow.getName())); + context.addValidationError(String.format(CONDITIONS_NOT_FOUND_ERROR, state.getName(), workflow.getName())); + } else if (!state.getDataConditions().isEmpty() && !state.getEventConditions().isEmpty()) { + context.addValidationError(String.format(DATA_CONDITIONS_AND_EVENT_CONDITIONS_FOUND_ERROR, state.getName(), workflow.getName())); } } @@ -67,18 +66,17 @@ public static void validateDefaultCondition(DefaultConditionDefinition defaultCo if (transition != null) { String nextState = transition.getNextState(); if (nextState == null || nextState.isEmpty()) { - throw new IllegalArgumentException(String.format(NEXT_STATE_REQUIRED_FOR_DEFAULT_CONDITION_ERROR, state.getName(), workflow.getName())); - } - if (parserContext.getStateHandler(nextState) == null) { - throw new IllegalArgumentException(String.format(NEXT_STATE_NOT_FOUND_FOR_DEFAULT_CONDITION_ERROR, nextState, state.getName(), workflow.getName())); + parserContext.addValidationError(String.format(NEXT_STATE_REQUIRED_FOR_DEFAULT_CONDITION_ERROR, state.getName(), workflow.getName())); + } else if (parserContext.getStateHandler(nextState) == null) { + parserContext.addValidationError(String.format(NEXT_STATE_NOT_FOUND_FOR_DEFAULT_CONDITION_ERROR, nextState, state.getName(), workflow.getName())); } } else if (defaultCondition.getEnd() == null) { - throw new IllegalArgumentException(String.format(TRANSITION_OR_END_MUST_BE_CONFIGURED_FOR_DEFAULT_CONDITION_ERROR, state.getName(), workflow.getName())); + parserContext.addValidationError(String.format(TRANSITION_OR_END_MUST_BE_CONFIGURED_FOR_DEFAULT_CONDITION_ERROR, state.getName(), workflow.getName())); } if (!state.getEventConditions().isEmpty()) { String eventTimeout = resolveEventTimeout(state, workflow); if (eventTimeout == null) { - throw new IllegalArgumentException(String.format(EVENT_TIMEOUT_REQUIRED_ERROR, state.getName(), workflow.getName())); + parserContext.addValidationError(String.format(EVENT_TIMEOUT_REQUIRED_ERROR, state.getName(), workflow.getName())); } } } diff --git a/kogito-serverless-workflow/kogito-serverless-workflow-builder/src/main/java/org/kie/kogito/serverless/workflow/parser/handlers/validation/WorkflowValidator.java b/kogito-serverless-workflow/kogito-serverless-workflow-builder/src/main/java/org/kie/kogito/serverless/workflow/parser/handlers/validation/WorkflowValidator.java index a0c5a15c031..82de55e0af7 100644 --- a/kogito-serverless-workflow/kogito-serverless-workflow-builder/src/main/java/org/kie/kogito/serverless/workflow/parser/handlers/validation/WorkflowValidator.java +++ b/kogito-serverless-workflow/kogito-serverless-workflow-builder/src/main/java/org/kie/kogito/serverless/workflow/parser/handlers/validation/WorkflowValidator.java @@ -18,6 +18,8 @@ */ package org.kie.kogito.serverless.workflow.parser.handlers.validation; +import org.kie.kogito.serverless.workflow.parser.ParserContext; + import io.serverlessworkflow.api.Workflow; import static org.kie.kogito.internal.utils.ConversionUtils.isEmpty; @@ -27,9 +29,9 @@ public class WorkflowValidator { private WorkflowValidator() { } - public static void validateStart(Workflow workflow) { + public static void validateStart(Workflow workflow, ParserContext context) { if (workflow.getStart() == null || isEmpty(workflow.getStart().getStateName())) { - throw new IllegalArgumentException("Workflow does not define a starting state"); + context.addValidationError("Workflow does not define a starting state"); } } } diff --git a/kogito-serverless-workflow/kogito-serverless-workflow-builder/src/main/java/org/kie/kogito/serverless/workflow/parser/types/ScriptTypeHandler.java b/kogito-serverless-workflow/kogito-serverless-workflow-builder/src/main/java/org/kie/kogito/serverless/workflow/parser/types/ScriptTypeHandler.java index c8a8cb74ab8..b9301b5b2bd 100644 --- a/kogito-serverless-workflow/kogito-serverless-workflow-builder/src/main/java/org/kie/kogito/serverless/workflow/parser/types/ScriptTypeHandler.java +++ b/kogito-serverless-workflow/kogito-serverless-workflow-builder/src/main/java/org/kie/kogito/serverless/workflow/parser/types/ScriptTypeHandler.java @@ -48,7 +48,9 @@ public String type() { public NodeFactory getActionNode(Workflow workflow, ParserContext context, RuleFlowNodeContainerFactory embeddedSubProcess, FunctionDefinition functionDef, FunctionRef functionRef, VariableInfo varInfo) { - checkArgs(functionRef, SCRIPT); + if (!checkArgs(context, functionRef, SCRIPT)) { + return embeddedSubProcess.actionNode(context.newId()); + } String lang = trimCustomOperation(functionDef); if (PYTHON.equalsIgnoreCase(lang)) { return addFunctionArgs(workflow, diff --git a/kogito-serverless-workflow/kogito-serverless-workflow-builder/src/main/java/org/kie/kogito/serverless/workflow/parser/types/SysOutTypeHandler.java b/kogito-serverless-workflow/kogito-serverless-workflow-builder/src/main/java/org/kie/kogito/serverless/workflow/parser/types/SysOutTypeHandler.java index 9f8d597d736..3c0ec2936a1 100644 --- a/kogito-serverless-workflow/kogito-serverless-workflow-builder/src/main/java/org/kie/kogito/serverless/workflow/parser/types/SysOutTypeHandler.java +++ b/kogito-serverless-workflow/kogito-serverless-workflow-builder/src/main/java/org/kie/kogito/serverless/workflow/parser/types/SysOutTypeHandler.java @@ -20,7 +20,9 @@ import org.jbpm.ruleflow.core.RuleFlowNodeContainerFactory; import org.jbpm.ruleflow.core.factory.ActionNodeFactory; +import org.jbpm.ruleflow.core.factory.NodeFactory; import org.kie.kogito.serverless.workflow.parser.FunctionTypeHandlerFactory; +import org.kie.kogito.serverless.workflow.parser.ParserContext; import org.kie.kogito.serverless.workflow.parser.VariableInfo; import org.kie.kogito.serverless.workflow.suppliers.SysoutActionSupplier; @@ -35,13 +37,25 @@ public class SysOutTypeHandler extends ActionTypeHandler { public static final String SYSOUT_TYPE = "sysout"; public static final String SYSOUT_TYPE_PARAM = "message"; + @Override + public NodeFactory getActionNode(Workflow workflow, + ParserContext context, + RuleFlowNodeContainerFactory embeddedSubProcess, + FunctionDefinition functionDef, + FunctionRef functionRef, + VariableInfo varInfo) { + if (!checkArgs(context, functionRef, SYSOUT_TYPE_PARAM)) { + return embeddedSubProcess.actionNode(context.newId()); + } + return super.getActionNode(workflow, context, embeddedSubProcess, functionDef, functionRef, varInfo); + } + @Override protected > ActionNodeFactory fillAction(Workflow workflow, ActionNodeFactory node, FunctionDefinition functionDef, FunctionRef functionRef, VariableInfo varInfo) { - checkArgs(functionRef, SYSOUT_TYPE_PARAM); return node.action(new SysoutActionSupplier(workflow.getExpressionLang(), functionRef.getArguments().get(SYSOUT_TYPE_PARAM).asText(), varInfo.getInputVar(), FunctionTypeHandlerFactory.trimCustomOperation(functionDef))); } diff --git a/kogito-serverless-workflow/kogito-serverless-workflow-builder/src/test/java/org/kie/kogito/serverless/workflow/parser/handlers/validation/SwitchValidatorTest.java b/kogito-serverless-workflow/kogito-serverless-workflow-builder/src/test/java/org/kie/kogito/serverless/workflow/parser/handlers/validation/SwitchValidatorTest.java index aed7bb78acd..1d2221122f2 100644 --- a/kogito-serverless-workflow/kogito-serverless-workflow-builder/src/test/java/org/kie/kogito/serverless/workflow/parser/handlers/validation/SwitchValidatorTest.java +++ b/kogito-serverless-workflow/kogito-serverless-workflow-builder/src/test/java/org/kie/kogito/serverless/workflow/parser/handlers/validation/SwitchValidatorTest.java @@ -25,6 +25,7 @@ import org.junit.jupiter.api.Test; import org.kie.kogito.serverless.workflow.parser.ParserContext; import org.kie.kogito.serverless.workflow.parser.handlers.StateHandler; +import org.mockito.ArgumentCaptor; import io.serverlessworkflow.api.Workflow; import io.serverlessworkflow.api.defaultdef.DefaultConditionDefinition; @@ -34,7 +35,7 @@ import io.serverlessworkflow.api.switchconditions.EventCondition; import io.serverlessworkflow.api.transitions.Transition; -import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.assertj.core.api.Assertions.assertThat; import static org.kie.kogito.serverless.workflow.parser.handlers.validation.SwitchValidator.CONDITIONS_NOT_FOUND_ERROR; import static org.kie.kogito.serverless.workflow.parser.handlers.validation.SwitchValidator.DATA_CONDITIONS_AND_EVENT_CONDITIONS_FOUND_ERROR; import static org.kie.kogito.serverless.workflow.parser.handlers.validation.SwitchValidator.EVENT_TIMEOUT_REQUIRED_ERROR; @@ -43,6 +44,7 @@ import static org.kie.kogito.serverless.workflow.parser.handlers.validation.SwitchValidator.TRANSITION_OR_END_MUST_BE_CONFIGURED_FOR_DEFAULT_CONDITION_ERROR; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; class SwitchValidatorTest { @@ -63,26 +65,30 @@ void setUp() { @Test void validateConditionsNoConditionsFoundError() { - assertThatThrownBy(() -> SwitchValidator.validateConditions(switchState, workflow)) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage(String.format(CONDITIONS_NOT_FOUND_ERROR, SWITCH_STATE_NAME, WORKFLOW_NAME)); + SwitchValidator.validateConditions(switchState, workflow, parserContext); + ArgumentCaptor argument = ArgumentCaptor.forClass(String.class); + verify(parserContext).addValidationError(argument.capture()); + assertThat(argument.getValue()).isEqualTo(String.format(CONDITIONS_NOT_FOUND_ERROR, SWITCH_STATE_NAME, WORKFLOW_NAME)); } @Test void validateConditionsBothConditionsFoundError() { switchState.getDataConditions().add(mock(DataCondition.class)); switchState.getEventConditions().add(mock(EventCondition.class)); - assertThatThrownBy(() -> SwitchValidator.validateConditions(switchState, workflow)) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage(String.format(DATA_CONDITIONS_AND_EVENT_CONDITIONS_FOUND_ERROR, SWITCH_STATE_NAME, WORKFLOW_NAME)); + SwitchValidator.validateConditions(switchState, workflow, parserContext); + ArgumentCaptor argument = ArgumentCaptor.forClass(String.class); + verify(parserContext).addValidationError(argument.capture()); + assertThat(argument.getValue()).isEqualTo(String.format(DATA_CONDITIONS_AND_EVENT_CONDITIONS_FOUND_ERROR, SWITCH_STATE_NAME, WORKFLOW_NAME)); } @Test void validateDefaultConditionTransitionWithoutNextError() { DefaultConditionDefinition defaultCondition = mockDefaultConditionWithTransition(); - assertThatThrownBy(() -> SwitchValidator.validateDefaultCondition(defaultCondition, switchState, workflow, parserContext)) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage(String.format(NEXT_STATE_REQUIRED_FOR_DEFAULT_CONDITION_ERROR, SWITCH_STATE_NAME, WORKFLOW_NAME)); + + SwitchValidator.validateDefaultCondition(defaultCondition, switchState, workflow, parserContext); + ArgumentCaptor argument = ArgumentCaptor.forClass(String.class); + verify(parserContext).addValidationError(argument.capture()); + assertThat(argument.getValue()).isEqualTo(String.format(NEXT_STATE_REQUIRED_FOR_DEFAULT_CONDITION_ERROR, SWITCH_STATE_NAME, WORKFLOW_NAME)); } @Test @@ -90,17 +96,19 @@ void validateDefaultConditionTransitionNextStateNotFoundError() { DefaultConditionDefinition defaultCondition = mockDefaultConditionWithTransition(); Transition transition = defaultCondition.getTransition(); doReturn(NEXT_STATE).when(transition).getNextState(); - assertThatThrownBy(() -> SwitchValidator.validateDefaultCondition(defaultCondition, switchState, workflow, parserContext)) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage(String.format(NEXT_STATE_NOT_FOUND_FOR_DEFAULT_CONDITION_ERROR, NEXT_STATE, SWITCH_STATE_NAME, WORKFLOW_NAME)); + SwitchValidator.validateDefaultCondition(defaultCondition, switchState, workflow, parserContext); + ArgumentCaptor argument = ArgumentCaptor.forClass(String.class); + verify(parserContext).addValidationError(argument.capture()); + assertThat(argument.getValue()).isEqualTo(String.format((String.format(NEXT_STATE_NOT_FOUND_FOR_DEFAULT_CONDITION_ERROR, NEXT_STATE, SWITCH_STATE_NAME, WORKFLOW_NAME)))); } @Test void validateDefaultConditionWithoutTransitionAndEndIsNullError() { DefaultConditionDefinition defaultCondition = mock(DefaultConditionDefinition.class); - assertThatThrownBy(() -> SwitchValidator.validateDefaultCondition(defaultCondition, switchState, workflow, parserContext)) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage(String.format(TRANSITION_OR_END_MUST_BE_CONFIGURED_FOR_DEFAULT_CONDITION_ERROR, SWITCH_STATE_NAME, WORKFLOW_NAME)); + SwitchValidator.validateDefaultCondition(defaultCondition, switchState, workflow, parserContext); + ArgumentCaptor argument = ArgumentCaptor.forClass(String.class); + verify(parserContext).addValidationError(argument.capture()); + assertThat(argument.getValue()).isEqualTo(String.format(TRANSITION_OR_END_MUST_BE_CONFIGURED_FOR_DEFAULT_CONDITION_ERROR, SWITCH_STATE_NAME, WORKFLOW_NAME)); } @Test @@ -111,9 +119,10 @@ void validateDefaultConditionWithEventConditionsTransitionButTimeoutNotSetError( doReturn(NEXT_STATE).when(transition).getNextState(); StateHandler stateHandler = mock(StateHandler.class); doReturn(stateHandler).when(parserContext).getStateHandler(NEXT_STATE); - assertThatThrownBy(() -> SwitchValidator.validateDefaultCondition(defaultCondition, switchState, workflow, parserContext)) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage(String.format(EVENT_TIMEOUT_REQUIRED_ERROR, SWITCH_STATE_NAME, WORKFLOW_NAME)); + SwitchValidator.validateDefaultCondition(defaultCondition, switchState, workflow, parserContext); + ArgumentCaptor argument = ArgumentCaptor.forClass(String.class); + verify(parserContext).addValidationError(argument.capture()); + assertThat(argument.getValue()).isEqualTo(EVENT_TIMEOUT_REQUIRED_ERROR, SWITCH_STATE_NAME, WORKFLOW_NAME); } @Test @@ -122,9 +131,10 @@ void validateDefaultConditionWithEventConditionsEndButTimeoutNotSetError() { DefaultConditionDefinition defaultCondition = mock(DefaultConditionDefinition.class); End end = mock(End.class); doReturn(end).when(defaultCondition).getEnd(); - assertThatThrownBy(() -> SwitchValidator.validateDefaultCondition(defaultCondition, switchState, workflow, parserContext)) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage(String.format(EVENT_TIMEOUT_REQUIRED_ERROR, SWITCH_STATE_NAME, WORKFLOW_NAME)); + SwitchValidator.validateDefaultCondition(defaultCondition, switchState, workflow, parserContext); + ArgumentCaptor argument = ArgumentCaptor.forClass(String.class); + verify(parserContext).addValidationError(argument.capture()); + assertThat(argument.getValue()).isEqualTo(String.format(EVENT_TIMEOUT_REQUIRED_ERROR, SWITCH_STATE_NAME, WORKFLOW_NAME)); } private SwitchState mockSwitchState() { diff --git a/kogito-serverless-workflow/kogito-serverless-workflow-executor-core/src/test/java/org/kie/kogito/serverless/workflow/executor/StaticFluentWorkflowApplicationTest.java b/kogito-serverless-workflow/kogito-serverless-workflow-executor-core/src/test/java/org/kie/kogito/serverless/workflow/executor/StaticFluentWorkflowApplicationTest.java index 1cc91bde1a2..5cf827f291c 100644 --- a/kogito-serverless-workflow/kogito-serverless-workflow-executor-core/src/test/java/org/kie/kogito/serverless/workflow/executor/StaticFluentWorkflowApplicationTest.java +++ b/kogito-serverless-workflow/kogito-serverless-workflow-executor-core/src/test/java/org/kie/kogito/serverless/workflow/executor/StaticFluentWorkflowApplicationTest.java @@ -29,6 +29,7 @@ import org.kie.api.event.process.ProcessCompletedEvent; import org.kie.kogito.internal.process.event.DefaultKogitoProcessEventListener; import org.kie.kogito.process.Process; +import org.kie.kogito.process.validation.ValidationException; import org.kie.kogito.serverless.workflow.actions.SysoutAction; import org.kie.kogito.serverless.workflow.actions.WorkflowLogLevel; import org.kie.kogito.serverless.workflow.fluent.FunctionBuilder; @@ -260,7 +261,7 @@ void testMissingMessageException() { try (StaticWorkflowApplication application = StaticWorkflowApplication.create()) { Workflow workflow = workflow("Testing logs").function(FunctionBuilder.def(funcName, Type.CUSTOM, SysOutTypeHandler.SYSOUT_TYPE)).start(operation().action( call(funcName))).end().build(); - assertThatExceptionOfType(IllegalArgumentException.class).isThrownBy(() -> application.process(workflow)).withMessageContaining("message"); + assertThatExceptionOfType(ValidationException.class).isThrownBy(() -> application.process(workflow)).withMessageContaining("message"); } } @@ -270,7 +271,7 @@ void testNoArgsMessageException() { try (StaticWorkflowApplication application = StaticWorkflowApplication.create()) { Workflow workflow = workflow("Testing logs").function(FunctionBuilder.def(funcName, Type.CUSTOM, SysOutTypeHandler.SYSOUT_TYPE)).start(operation().action( call(funcName, null))).end().build(); - assertThatExceptionOfType(IllegalArgumentException.class).isThrownBy(() -> application.process(workflow)).withMessageContaining("Arguments cannot be null"); + assertThatExceptionOfType(ValidationException.class).isThrownBy(() -> application.process(workflow)).withMessageContaining("Arguments cannot be null"); } } diff --git a/kogito-serverless-workflow/kogito-serverless-workflow-executor-core/src/test/java/org/kie/kogito/serverless/workflow/executor/StaticWorkflowApplicationTest.java b/kogito-serverless-workflow/kogito-serverless-workflow-executor-core/src/test/java/org/kie/kogito/serverless/workflow/executor/StaticWorkflowApplicationTest.java index 4309f18156c..bed9c81d532 100644 --- a/kogito-serverless-workflow/kogito-serverless-workflow-executor-core/src/test/java/org/kie/kogito/serverless/workflow/executor/StaticWorkflowApplicationTest.java +++ b/kogito-serverless-workflow/kogito-serverless-workflow-executor-core/src/test/java/org/kie/kogito/serverless/workflow/executor/StaticWorkflowApplicationTest.java @@ -32,6 +32,7 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; +import org.kie.kogito.process.validation.ValidationException; import org.kie.kogito.serverless.workflow.actions.SysoutAction; import org.kie.kogito.serverless.workflow.utils.ServerlessWorkflowUtils; import org.kie.kogito.serverless.workflow.utils.WorkflowFormat; @@ -50,6 +51,7 @@ import ch.qos.logback.core.read.ListAppender; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.catchThrowableOfType; import static org.kie.kogito.serverless.workflow.utils.ServerlessWorkflowUtils.getWorkflow; import static org.kie.kogito.serverless.workflow.utils.ServerlessWorkflowUtils.writeWorkflow; @@ -107,6 +109,17 @@ void interpolationFile(String fileName, WorkflowFormat format) throws IOExceptio } } + @Test + void testValidationError() throws IOException { + try (Reader reader = new InputStreamReader(Thread.currentThread().getContextClassLoader().getResourceAsStream("wrong.sw.json")); + StaticWorkflowApplication application = StaticWorkflowApplication.create()) { + Workflow workflow = getWorkflow(reader, WorkflowFormat.JSON); + ValidationException validationException = catchThrowableOfType(() -> application.process(workflow), ValidationException.class); + assertThat(validationException.getErrors()).hasSizeGreaterThanOrEqualTo(4); + assertThat(validationException).hasMessageContaining("error").hasMessageContaining("function").hasMessageContaining("connect").hasMessageContaining("transition"); + } + } + private static Stream interpolationParameters() { return Stream.of(Arguments.of("interpolation.sw.json", WorkflowFormat.JSON), Arguments.of("interpolation.sw.yml", WorkflowFormat.YAML)); diff --git a/kogito-serverless-workflow/kogito-serverless-workflow-executor-core/src/test/resources/wrong.sw.json b/kogito-serverless-workflow/kogito-serverless-workflow-executor-core/src/test/resources/wrong.sw.json new file mode 100644 index 00000000000..41c48f309a9 --- /dev/null +++ b/kogito-serverless-workflow/kogito-serverless-workflow-executor-core/src/test/resources/wrong.sw.json @@ -0,0 +1,57 @@ +{ + "id": "hello", + "version": "1.0", + "name": "Hello Workflow", + "description": "Inject Hello World", + "start": "Hello", + "functions": [ + { + "name": "getIP", + "type": "custom", + "operation": "rest:get:https://ipinfo.io/json" + }, + { + "name": "pushData", + "type": "custom", + "operation": "rest:post:https://httpbin.org/post" + }, + { + "name": "logInfo", + "type": "custom", + "operation": "sysout:INFO" + } + ], + "errors":[], + "states": [ + { + "name": "Hello", + "type": "inject", + "data": { + "greeting": "Hello World", + "mantra": "Serverless Workflow is awesome!" + }, + "end": true + }, + { + "name": "Get public IP", + "type": "operation", + "actions": [ + { + "functionRef": { + "refName": "getIPO" + }, + "actionDataFilter": { + "toStateData": ".ip_info" + } + } + ], + "onErrors": [ + { + "errorRef": "notAvailable", + "transition": "logError" + } + ], + "transition": "push_host_data" + } + ] +} \ No newline at end of file diff --git a/kogito-serverless-workflow/kogito-serverless-workflow-executor-grpc/src/main/java/org/kie/kogito/serverless/workflow/executor/StaticRPCRegister.java b/kogito-serverless-workflow/kogito-serverless-workflow-executor-grpc/src/main/java/org/kie/kogito/serverless/workflow/executor/StaticRPCRegister.java index 0fed7511b53..372f86edcc3 100644 --- a/kogito-serverless-workflow/kogito-serverless-workflow-executor-grpc/src/main/java/org/kie/kogito/serverless/workflow/executor/StaticRPCRegister.java +++ b/kogito-serverless-workflow/kogito-serverless-workflow-executor-grpc/src/main/java/org/kie/kogito/serverless/workflow/executor/StaticRPCRegister.java @@ -18,6 +18,8 @@ */ package org.kie.kogito.serverless.workflow.executor; +import java.util.Optional; + import org.kie.kogito.serverless.workflow.parser.handlers.ActionResource; import org.kie.kogito.serverless.workflow.parser.handlers.ActionResourceFactory; import org.kie.kogito.serverless.workflow.utils.RPCWorkflowUtils; @@ -36,7 +38,7 @@ public void register(StaticWorkflowApplication application, Workflow workflow) { } private void registerHandler(StaticWorkflowApplication application, FunctionDefinition function) { - ActionResource actionResource = ActionResourceFactory.getActionResource(function); + ActionResource actionResource = ActionResourceFactory.getActionResource(function, Optional.empty()); application.registerHandler(new StaticRPCWorkItemHandler(RPCWorkflowUtils.getRPCClassName(actionResource.getService()))); } }