All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.zeebe.model.bpmn.impl.BpmnValidator Maven / Gradle / Ivy

/*
 * Copyright © 2017 camunda services GmbH ([email protected])
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package io.zeebe.model.bpmn.impl;

import static io.zeebe.util.buffer.BufferUtil.bufferAsString;

import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

import io.zeebe.model.bpmn.*;
import io.zeebe.model.bpmn.instance.*;
import io.zeebe.msgpack.el.CompiledJsonCondition;
import io.zeebe.msgpack.jsonpath.JsonPathQuery;
import io.zeebe.msgpack.mapping.Mapping;
import org.agrona.DirectBuffer;

public class BpmnValidator
{
    private static final String PROHIBITED_EXPRESSIONS_REGEX = "(\\.\\*)|(\\[.*,.*\\])";
    private static final Pattern PROHIBITED_EXPRESSIONS = Pattern.compile(PROHIBITED_EXPRESSIONS_REGEX);

    public ValidationResult validate(WorkflowDefinition definition)
    {
        final ValidationResultImpl validationResult = new ValidationResultImpl();

        final List executableWorkflows = definition.getWorkflows().stream()
                .filter(Workflow::isExecutable)
                .collect(Collectors.toList());

        if (executableWorkflows.isEmpty())
        {
            validationResult.addError(definition, "BPMN model must contain at least one executable process.");
        }

        for (Workflow executableWorkflow : executableWorkflows)
        {
            validateWorkflow(validationResult, executableWorkflow);
        }

        return validationResult;
    }

    private void validateWorkflow(ValidationResultImpl validationResult, Workflow workflow)
    {
        final DirectBuffer bpmnProcessId = workflow.getBpmnProcessId();
        if (bpmnProcessId == null || bpmnProcessId.capacity() == 0)
        {
            validationResult.addError(workflow, "BPMN process id is required.");
        }
        else if (bpmnProcessId.capacity() > ZeebeConstraints.ID_MAX_LENGTH)
        {
            validationResult.addError(workflow, String.format("BPMN process id must not be longer than %d.", ZeebeConstraints.ID_MAX_LENGTH));
        }

        if (workflow.getInitialStartEvent() == null)
        {
            validationResult.addError(workflow, "The process must contain at least one none start event.");
        }

        for (FlowElement flowElement : workflow.getFlowElements())
        {
            validateFlowElement(validationResult, flowElement);
        }
    }

    private void validateFlowElement(ValidationResultImpl validationResult, FlowElement flowElement)
    {
        final DirectBuffer id = flowElement.getIdAsBuffer();
        if (id == null || id.capacity() == 0)
        {
            validationResult.addError(flowElement, "Activity id is required.");
        }
        else if (id.capacity() > ZeebeConstraints.ID_MAX_LENGTH)
        {
            validationResult.addError(flowElement, String.format("Activity id must not be longer than %d.", ZeebeConstraints.ID_MAX_LENGTH));
        }

        if (flowElement instanceof FlowNode)
        {
            validateFlowNode(validationResult, (FlowNode) flowElement);
        }

        if (flowElement instanceof ServiceTask)
        {
            validateServiceTask(validationResult, (ServiceTask) flowElement);
        }
        else if (flowElement instanceof EndEvent)
        {
            validateEndEvent(validationResult, (EndEvent) flowElement);
        }
        else if (flowElement instanceof ExclusiveGateway)
        {
            validateExclusiveGateway(validationResult, (ExclusiveGateway) flowElement);
        }
    }

    private void validateFlowNode(ValidationResultImpl validationResult, FlowNode flowNode)
    {
        if (!(flowNode instanceof ExclusiveGateway))
        {
            if (flowNode.getOutgoingSequenceFlows().size() > 1)
            {
                validationResult.addError(flowNode, "The flow element must not have more than one outgoing sequence flow.");
            }
        }

        flowNode.getIncomingSequenceFlows().stream().filter(s -> s.getSourceNode() == null).forEach(s ->
        {
            validationResult.addError(s, "Cannot find source of sequence flow.");
        });

        flowNode.getOutgoingSequenceFlows().stream().filter(s -> s.getTargetNode() == null).forEach(s ->
        {
            validationResult.addError(s, "Cannot find target of sequence flow.");
        });
    }

    private void validateServiceTask(ValidationResultImpl validationResult, ServiceTask serviceTask)
    {
        final TaskDefinition taskDefinition = serviceTask.getTaskDefinition();
        if (taskDefinition == null)
        {
            validationResult.addError(serviceTask, String.format("A service task must contain a '%s' extension element.", BpmnConstants.ZEEBE_ELEMENT_TASK_DEFINITION));
        }
        else
        {
            validateTaskDefinition(validationResult, taskDefinition);
        }

        final TaskHeaders taskHeaders = serviceTask.getTaskHeaders();
        if (taskHeaders != null)
        {
            validateTaskHeaders(validationResult, taskHeaders);
        }

        final InputOutputMapping inputOutputMapping = serviceTask.getInputOutputMapping();
        if (inputOutputMapping != null)
        {
            validateInputOutputMapping(validationResult, inputOutputMapping);
        }
    }

    private void validateTaskDefinition(ValidationResultImpl validationResult, final TaskDefinition taskDefinition)
    {
        final DirectBuffer taskType = taskDefinition.getTypeAsBuffer();
        if (taskType == null || taskType.capacity() == 0)
        {
            validationResult.addError(taskDefinition, String.format("A task definition must contain a '%s' attribute which specifies the type of the task.", BpmnConstants.ZEEBE_ATTRIBUTE_TASK_TYPE));
        }

        final int retries = taskDefinition.getRetries();
        if (retries < 1)
        {
            validationResult.addError(taskDefinition, "The task retries must be greater than 0.");
        }
    }

    private void validateTaskHeaders(ValidationResultImpl validationResult, TaskHeaders taskHeader)
    {
        for (Entry header : taskHeader.asMap().entrySet())
        {
            if (header.getKey() == null)
            {
                validationResult.addError(taskHeader, String.format("A task header must contain a '%s' attribute.", BpmnConstants.ZEEBE_ATTRIBUTE_TASK_HEADER_KEY));
            }

            if (header.getValue() == null)
            {
                validationResult.addError(taskHeader, String.format("A task header must contain a '%s' attribute.", BpmnConstants.ZEEBE_ATTRIBUTE_TASK_HEADER_VALUE));
            }
        }
    }

    private void validateInputOutputMapping(ValidationResultImpl validationResult, InputOutputMapping inputOutputMapping)
    {
        validateMappingExpressions(validationResult, inputOutputMapping, inputOutputMapping.getInputMappingsAsMap());
        validateMappingExpressions(validationResult, inputOutputMapping, inputOutputMapping.getOutputMappingsAsMap());

        validateMappings(validationResult, inputOutputMapping, inputOutputMapping.getInputMappings());
        validateMappings(validationResult, inputOutputMapping, inputOutputMapping.getOutputMappings());
    }

    private void validateMappingExpressions(ValidationResultImpl validationResult, InputOutputMapping element, Map mappings)
    {
        for (Entry mapping : mappings.entrySet())
        {
            final String source = mapping.getKey();
            final String target = mapping.getValue();

            if (PROHIBITED_EXPRESSIONS.matcher(source).find())
            {
                validationResult.addError(element, String.format("Source mapping: JSON path '%s' contains prohibited expression (for example $.* or $.(foo|bar)).", source));
            }

            if (PROHIBITED_EXPRESSIONS.matcher(target).find())
            {
                validationResult.addError(element, String.format("Target mapping: JSON path '%s' contains prohibited expression (for example $.* or $.(foo|bar)).", target));
            }

            if (mappings.size() > 1 && target.equals(Mapping.JSON_ROOT_PATH))
            {
                validationResult.addError(element, "Target mapping: root mapping is not allowed because it would override other mapping.");
            }
        }
    }

    private void validateMappings(ValidationResultImpl validationResult, InputOutputMapping element, Mapping[] mappings)
    {
        for (Mapping mapping : mappings)
        {
            final JsonPathQuery query = mapping.getSource();
            if (!query.isValid())
            {
                validationResult.addError(element, String.format("JSON path query '%s' is not valid! Reason: %s", bufferAsString(query.getExpression()), query.getErrorReason()));
            }
        }
    }

    private void validateEndEvent(ValidationResultImpl validationResult, EndEvent endEvent)
    {
        if (!endEvent.getOutgoingSequenceFlows().isEmpty())
        {
            validationResult.addError(endEvent, "An end event must not have an outgoing sequence flow.");
        }
    }

    private void validateExclusiveGateway(ValidationResultImpl validationResult, ExclusiveGateway exclusiveGateway)
    {
        if (exclusiveGateway.getBpmnAspect() == BpmnAspect.EXCLUSIVE_SPLIT)
        {
            final SequenceFlow defaultFlow = exclusiveGateway.getDefaultFlow();
            if (defaultFlow != null)
            {
                if (defaultFlow.hasCondition())
                {
                    validationResult.addError(defaultFlow, "A default sequence flow must not have a condition.");
                }

                if (!exclusiveGateway.getOutgoingSequenceFlows().contains(defaultFlow))
                {
                    validationResult.addError(exclusiveGateway, "The default sequence flow must be an outgoing sequence flow of the exclusive gateway.");
                }
            }
            else
            {
                validationResult.addWarning(exclusiveGateway, "An exclusive gateway should have a default sequence flow without condition.");
            }

            for (SequenceFlow sequenceFlow : exclusiveGateway.getOutgoingSequenceFlowsWithConditions())
            {
                final CompiledJsonCondition condition = sequenceFlow.getCondition();
                if (!condition.isValid())
                {
                    validationResult.addError(sequenceFlow, String.format("The condition '%s' is not valid: %s", condition.getExpression(), condition.getErrorMessage()));
                }
            }

            for (SequenceFlow sequenceFlow : exclusiveGateway.getOutgoingSequenceFlows())
            {
                if (!sequenceFlow.hasCondition() && !sequenceFlow.equals(defaultFlow))
                {
                    validationResult.addError(sequenceFlow, "A sequence flow on an exclusive gateway must have a condition, if it is not the default flow.");
                }
            }
        }
        else
        {
            if (exclusiveGateway.getOutgoingSequenceFlows().size() > 1)
            {
                validationResult.addError(exclusiveGateway, "An exclusive gateway with more than one outgoing sequence flow must have conditions on the sequence flows.");
            }
        }
    }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy