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

org.jbpm.bpmn2.xml.ProcessHandler Maven / Gradle / Ivy

There is a newer version: 10.0.0
Show newest version
/*
 * Copyright 2021 Red Hat, Inc. and/or its affiliates.
 *
 * 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 org.jbpm.bpmn2.xml;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import org.jbpm.bpmn2.core.Association;
import org.jbpm.bpmn2.core.Collaboration;
import org.jbpm.bpmn2.core.CorrelationKey;
import org.jbpm.bpmn2.core.CorrelationProperty;
import org.jbpm.bpmn2.core.CorrelationSubscription;
import org.jbpm.bpmn2.core.DataStore;
import org.jbpm.bpmn2.core.Definitions;
import org.jbpm.bpmn2.core.Error;
import org.jbpm.bpmn2.core.Escalation;
import org.jbpm.bpmn2.core.Expression;
import org.jbpm.bpmn2.core.Interface;
import org.jbpm.bpmn2.core.IntermediateLink;
import org.jbpm.bpmn2.core.ItemDefinition;
import org.jbpm.bpmn2.core.Lane;
import org.jbpm.bpmn2.core.Message;
import org.jbpm.bpmn2.core.SequenceFlow;
import org.jbpm.bpmn2.core.Signal;
import org.jbpm.compiler.xml.Handler;
import org.jbpm.compiler.xml.Parser;
import org.jbpm.compiler.xml.ProcessBuildData;
import org.jbpm.compiler.xml.core.BaseAbstractHandler;
import org.jbpm.process.core.ContextContainer;
import org.jbpm.process.core.context.exception.ActionExceptionHandler;
import org.jbpm.process.core.context.exception.CompensationHandler;
import org.jbpm.process.core.context.exception.CompensationScope;
import org.jbpm.process.core.context.exception.ExceptionScope;
import org.jbpm.process.core.context.swimlane.Swimlane;
import org.jbpm.process.core.context.variable.VariableScope;
import org.jbpm.process.core.correlation.CorrelationManager;
import org.jbpm.process.core.event.EventFilter;
import org.jbpm.process.core.event.EventTypeFilter;
import org.jbpm.process.core.event.MVELMessageExpressionEvaluator;
import org.jbpm.process.core.timer.Timer;
import org.jbpm.process.instance.impl.Action;
import org.jbpm.process.instance.impl.actions.CancelNodeInstanceAction;
import org.jbpm.process.instance.impl.actions.ProcessInstanceCompensationAction;
import org.jbpm.process.instance.impl.actions.SignalProcessInstanceAction;
import org.jbpm.ruleflow.core.Metadata;
import org.jbpm.ruleflow.core.RuleFlowProcess;
import org.jbpm.ruleflow.core.validation.RuleFlowProcessValidator;
import org.jbpm.workflow.core.Connection;
import org.jbpm.workflow.core.Constraint;
import org.jbpm.workflow.core.DroolsAction;
import org.jbpm.workflow.core.impl.ConnectionImpl;
import org.jbpm.workflow.core.impl.ConnectionRef;
import org.jbpm.workflow.core.impl.ConstraintImpl;
import org.jbpm.workflow.core.impl.DroolsConsequenceAction;
import org.jbpm.workflow.core.impl.ExtendedNodeImpl;
import org.jbpm.workflow.core.impl.NodeImpl;
import org.jbpm.workflow.core.node.ActionNode;
import org.jbpm.workflow.core.node.BoundaryEventNode;
import org.jbpm.workflow.core.node.CompositeContextNode;
import org.jbpm.workflow.core.node.CompositeNode;
import org.jbpm.workflow.core.node.ConstraintTrigger;
import org.jbpm.workflow.core.node.EndNode;
import org.jbpm.workflow.core.node.EventNode;
import org.jbpm.workflow.core.node.EventSubProcessNode;
import org.jbpm.workflow.core.node.EventTrigger;
import org.jbpm.workflow.core.node.FaultNode;
import org.jbpm.workflow.core.node.HumanTaskNode;
import org.jbpm.workflow.core.node.RuleSetNode;
import org.jbpm.workflow.core.node.Split;
import org.jbpm.workflow.core.node.StartNode;
import org.jbpm.workflow.core.node.StateBasedNode;
import org.jbpm.workflow.core.node.StateNode;
import org.jbpm.workflow.core.node.SubProcessNode;
import org.jbpm.workflow.core.node.Trigger;
import org.jbpm.workflow.core.node.WorkItemNode;
import org.kie.api.definition.process.Node;
import org.kie.api.definition.process.NodeContainer;
import org.kie.api.definition.process.Process;
import org.kie.kogito.internal.process.runtime.KogitoNode;
import org.kie.kogito.internal.process.runtime.KogitoWorkflowProcess;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xml.sax.Attributes;
import org.xml.sax.SAXException;

public class ProcessHandler extends BaseAbstractHandler implements Handler {

    private static final Logger logger = LoggerFactory.getLogger(ProcessHandler.class);

    public static final String CURRENT_PROCESS = "BPMN.Process";
    public static final String CONNECTIONS = "BPMN.Connections";
    public static final String LINKS = "BPMN.ThrowLinks";
    public static final String ASSOCIATIONS = "BPMN.Associations";
    public static final String ERRORS = "BPMN.Errors";
    public static final String ESCALATIONS = "BPMN.Escalations";

    @SuppressWarnings("unchecked")
    public ProcessHandler() {
        if ((this.validParents == null) && (this.validPeers == null)) {
            this.validParents = new HashSet();
            this.validParents.add(Definitions.class);

            this.validPeers = new HashSet();
            this.validPeers.add(null);
            this.validPeers.add(ItemDefinition.class);
            this.validPeers.add(Message.class);
            this.validPeers.add(Interface.class);
            this.validPeers.add(Escalation.class);
            this.validPeers.add(Error.class);
            this.validPeers.add(Signal.class);
            this.validPeers.add(DataStore.class);
            this.validPeers.add(RuleFlowProcess.class);

            this.allowNesting = false;
        }
    }

    @Override
    public Object start(final String uri, final String localName,
            final Attributes attrs, final Parser parser)
            throws SAXException {
        parser.startElementBuilder(localName, attrs);

        String id = attrs.getValue("id");
        String name = attrs.getValue("name");
        String visibility = attrs.getValue("processType");
        String packageName = attrs.getValue("http://www.jboss.org/drools", "packageName");
        String dynamic = attrs.getValue("http://www.jboss.org/drools", "adHoc");
        String version = attrs.getValue("http://www.jboss.org/drools", "version");

        RuleFlowProcess process = new RuleFlowProcess();
        process.setAutoComplete(true);
        process.setId(id);
        if (name == null) {
            name = id;
        }
        process.setName(name);
        process.setType(KogitoWorkflowProcess.BPMN_TYPE);
        if (packageName == null) {
            packageName = "org.drools.bpmn2";
        }
        process.setPackageName(packageName);
        if ("true".equals(dynamic)) {
            process.setDynamic(true);
            process.setAutoComplete(false);
        }
        if (version != null) {
            process.setVersion(version);
        }
        if (visibility == null || "".equals(visibility)) {
            visibility = KogitoWorkflowProcess.NONE_VISIBILITY;
        }
        process.setVisibility(visibility);
        ((ProcessBuildData) parser.getData()).setMetaData(CURRENT_PROCESS, process);
        ((ProcessBuildData) parser.getData()).addProcess(process);
        // register the definitions object as metadata of process.
        process.setMetaData("Definitions", parser.getParent());
        // register bpmn2 imports as meta data of process
        Object typedImports = ((ProcessBuildData) parser.getData()).getMetaData("Bpmn2Imports");
        if (typedImports != null) {
            process.setMetaData("Bpmn2Imports", typedImports);
        }
        // register item definitions as meta data of process
        Object itemDefinitions = ((ProcessBuildData) parser.getData()).getMetaData("ItemDefinitions");
        if (itemDefinitions != null) {
            process.setMetaData("ItemDefinitions", itemDefinitions);
        }

        // for unique id's of nodes, start with one to avoid returning wrong nodes for dynamic nodes
        parser.getMetaData().put("idGen", new AtomicInteger(1));
        parser.getMetaData().put("CurrentProcessDefinition", process);
        process.getCorrelationManager().setClassLoader(parser.getClassLoader());
        return process;
    }

    @Override
    @SuppressWarnings("unchecked")
    public Object end(final String uri, final String localName,
            final Parser parser) throws SAXException {
        parser.endElementBuilder();

        RuleFlowProcess process = (RuleFlowProcess) parser.getCurrent();
        List throwLinks = (List) process
                .getMetaData(LINKS);
        linkIntermediateLinks(process, throwLinks);

        List connections = (List) process.getMetaData(CONNECTIONS);
        linkConnections(process, connections);
        linkBoundaryEvents(process);

        // This must be done *after* linkConnections(process, connections)
        //  because it adds hidden connections for compensations
        List associations = (List) process.getMetaData(ASSOCIATIONS);
        linkAssociations((Definitions) process.getMetaData("Definitions"), process, associations);

        List lanes = (List) process.getMetaData(LaneHandler.LANES);
        assignLanes(process, lanes);
        postProcessNodes(process, process);
        postProcessCollaborations(process, parser);
        return process;
    }

    private void postProcessCollaborations(RuleFlowProcess process, Parser parser) {
        // now we wire correlation process subscriptions
        CorrelationManager correlationManager = process.getCorrelationManager();
        for (Message message : HandlerUtil.messages(parser).values()) {
            correlationManager.newMessage(message.getId(), message.getName(), message.getType());
        }

        // only the ones this process is member of
        List collaborations = HandlerUtil.collaborations(parser).values().stream().filter(c -> c.getProcessesRef().contains(process.getId())).collect(Collectors.toList());
        for (Collaboration collaboration : collaborations) {
            for (CorrelationKey key : collaboration.getCorrelationKeys()) {

                correlationManager.newCorrelation(key.getId(), key.getName());
                List properties = key.getPropertiesRef().stream().map(k -> HandlerUtil.correlationProperties(parser).get(k)).collect(Collectors.toList());
                for (CorrelationProperty correlationProperty : properties) {
                    correlationProperty.getMessageRefs().forEach(messageRef -> {

                        // for now only MVEL expressions
                        MVELMessageExpressionEvaluator evaluator = new MVELMessageExpressionEvaluator(correlationProperty.getRetrievalExpression(messageRef).getScript());
                        correlationManager.addMessagePropertyExpression(key.getId(), messageRef, correlationProperty.getId(), evaluator);
                    });
                }
            }
        }

        // we create the correlations
        for (CorrelationSubscription subscription : HandlerUtil.correlationSubscription(process).values()) {
            correlationManager.subscribeTo(subscription.getCorrelationKeyRef());
            for (Map.Entry binding : subscription.getPropertyExpressions().entrySet()) {
                MVELMessageExpressionEvaluator evaluator = new MVELMessageExpressionEvaluator(binding.getValue().getScript());
                correlationManager.addProcessSubscriptionPropertyExpression(subscription.getCorrelationKeyRef(), binding.getKey(), evaluator);
            }
        }
    }

    public static void linkIntermediateLinks(NodeContainer process, List links) {
        if (links == null) {
            return;
        }
        Map catchLinks = new HashMap<>();
        Map> throwLinks = new HashMap<>();
        Collection noNameLinks = new ArrayList<>();
        Collection duplicatedTarget = new LinkedHashSet<>();
        Collection unconnectedTarget = new ArrayList<>();

        // collect errors and nodes in first loop
        for (IntermediateLink link : links) {
            if (link.getName() == null || link.getName().isEmpty()) {
                noNameLinks.add(link);
            } else if (link.isThrowLink()) {
                throwLinks.computeIfAbsent(link.getName(), s -> new ArrayList<>()).add(link);
            } else {
                IntermediateLink duplicateLink = catchLinks.putIfAbsent(link.getName(), link);
                if (duplicateLink != null) {
                    duplicatedTarget.add(duplicateLink);
                    duplicatedTarget.add(link);
                }
            }
        }

        // second loop for connection
        for (IntermediateLink catchLink : catchLinks.values()) {
            Collection associatedLinks = throwLinks.remove(catchLink.getName());
            if (associatedLinks != null) {
                // connect throw to catch
                Node catchNode = findNodeByIdOrUniqueIdInMetadata(process, catchLink.getUniqueId());
                if (catchNode != null) {
                    for (IntermediateLink throwLink : associatedLinks) {
                        Node throwNode = findNodeByIdOrUniqueIdInMetadata(process,
                                throwLink.getUniqueId());
                        if (throwNode != null) {
                            Connection result = new ConnectionImpl(throwNode,
                                    org.jbpm.workflow.core.Node.CONNECTION_DEFAULT_TYPE, catchNode,
                                    org.jbpm.workflow.core.Node.CONNECTION_DEFAULT_TYPE);
                            result.setMetaData("linkNodeHidden", "yes");
                        }
                    }
                }
            } else {
                unconnectedTarget.add(catchLink);
            }
        }

        // throw exception if any error (this is done at the end of the process to show the user as much errors as possible) 
        StringBuilder errors = new StringBuilder();
        if (!noNameLinks.isEmpty()) {
            formatError(errors, "These nodes do not have a name ", noNameLinks.stream(), process);
        }
        if (!duplicatedTarget.isEmpty()) {
            formatError(errors, "\nThere are multiple catch nodes with the same name ", duplicatedTarget.stream(),
                    process);
        }
        if (!unconnectedTarget.isEmpty()) {
            formatError(errors, "\nThere is not connection from any throw link to these catch links ", unconnectedTarget
                    .stream(), process);
        }
        if (!throwLinks.isEmpty()) {
            formatError(errors, "\nThere is not connection to any catch link from these throw links ", throwLinks
                    .values()
                    .stream()
                    .flatMap(Collection::stream), process);
        }
        if (errors.length() > 0) {
            throw new ProcessParsingValidationException(errors.toString());
        }

    }

    private static void formatError(StringBuilder errors,
            String message,
            Stream stream,
            NodeContainer container) {
        errors.append(message).append(stream.map(IntermediateLink::getUniqueId).collect(Collectors.joining(", ", "{",
                "}")));
        if (container instanceof Process) {
            errors.append(" for process ").append(((Process) container).getId());
        } else if (container instanceof Node) {
            errors.append(" for subprocess ").append(((Node) container).getId());
        }
    }

    private static Object findNodeOrDataStoreByUniqueId(Definitions definitions, NodeContainer nodeContainer, final String nodeRef, String errorMsg) {
        if (definitions != null) {
            List dataStores = definitions.getDataStores();
            if (dataStores != null) {
                for (DataStore dataStore : dataStores) {
                    if (nodeRef.equals(dataStore.getId())) {
                        return dataStore;
                    }
                }
            }
        }
        return findNodeByIdOrUniqueIdInMetadata(nodeContainer, nodeRef, errorMsg);
    }

    private static Node findNodeByIdOrUniqueIdInMetadata(
            NodeContainer nodeContainer, String targetRef) {
        return findNodeByIdOrUniqueIdInMetadata(nodeContainer, targetRef, "Could not find target node for connection:" + targetRef);
    }

    private static Node findNodeByIdOrUniqueIdInMetadata(NodeContainer nodeContainer, final String nodeRef, String errorMsg) {
        Node node = null;
        // try looking for a node with same "UniqueId" (in metadata)
        for (Node containerNode : nodeContainer.getNodes()) {
            if (nodeRef.equals(containerNode.getMetaData().get("UniqueId"))) {
                node = containerNode;
                break;
            }
        }
        if (node == null) {
            throw new ProcessParsingValidationException(errorMsg);
        }
        return node;
    }

    @Override
    public Class generateNodeFor() {
        return RuleFlowProcess.class;
    }

    public static void linkConnections(NodeContainer nodeContainer, List connections) {
        if (connections != null) {
            for (SequenceFlow connection : connections) {
                String sourceRef = connection.getSourceRef();
                Node source = findNodeByIdOrUniqueIdInMetadata(nodeContainer, sourceRef, "Could not find source node for connection:" + sourceRef);

                if (source instanceof EventNode) {
                    for (EventFilter eventFilter : ((EventNode) source).getEventFilters()) {
                        if (eventFilter instanceof EventTypeFilter) {
                            if ("Compensation".equals(((EventTypeFilter) eventFilter).getType())) {
                                // While this isn't explicitly stated in the spec,
                                // BPMN Method & Style, 2nd Ed. (Silver), states this on P. 131
                                throw new ProcessParsingValidationException(
                                        "A Compensation Boundary Event can only be *associated* with a compensation activity via an Association, not via a Sequence Flow element.");
                            }
                        }
                    }
                }

                String targetRef = connection.getTargetRef();
                Node target = findNodeByIdOrUniqueIdInMetadata(nodeContainer, targetRef, "Could not find target node for connection:" + targetRef);

                Connection result = new ConnectionImpl(
                        source, org.jbpm.workflow.core.Node.CONNECTION_DEFAULT_TYPE,
                        target, org.jbpm.workflow.core.Node.CONNECTION_DEFAULT_TYPE);
                result.setMetaData("bendpoints", connection.getBendpoints());
                result.setMetaData("UniqueId", connection.getId());

                if ("true".equals(System.getProperty("jbpm.enable.multi.con"))) {
                    NodeImpl nodeImpl = (NodeImpl) source;
                    Constraint constraint = buildConstraint(connection, nodeImpl);
                    if (constraint != null) {
                        nodeImpl.addConstraint(new ConnectionRef(connection.getId(), target.getId(), org.jbpm.workflow.core.Node.CONNECTION_DEFAULT_TYPE),
                                constraint);
                    }

                } else if (source instanceof Split) {
                    Split split = (Split) source;
                    Constraint constraint = buildConstraint(connection, split);
                    split.addConstraint(
                            new ConnectionRef(connection.getId(), target.getId(), org.jbpm.workflow.core.Node.CONNECTION_DEFAULT_TYPE),
                            constraint);
                }
            }
        }
    }

    public static void linkBoundaryEvents(NodeContainer nodeContainer) {
        for (Node node : nodeContainer.getNodes()) {
            if (node instanceof EventNode) {
                final String attachedTo = (String) node.getMetaData().get("AttachedTo");
                if (attachedTo != null) {
                    for (EventFilter filter : ((EventNode) node).getEventFilters()) {
                        String type = ((EventTypeFilter) filter).getType();
                        Node attachedNode = findNodeByIdOrUniqueIdInMetadata(nodeContainer, attachedTo, "Could not find node to attach to: " + attachedTo);

                        // 
                        if (!(attachedNode instanceof StateBasedNode) && !type.equals("Compensation")) {
                            throw new ProcessParsingValidationException("Boundary events are supported only on StateBasedNode, found node: "
                                    + attachedNode.getClass().getName() + " [" + attachedNode.getMetaData().get("UniqueId") + "]");
                        }

                        if (type.startsWith("Escalation")) {
                            linkBoundaryEscalationEvent(node, attachedTo, attachedNode);
                        } else if (type.startsWith("Error-")) {
                            linkBoundaryErrorEvent(node, attachedTo, attachedNode);
                        } else if (type.startsWith("Timer-")) {
                            linkBoundaryTimerEvent(node, attachedTo, attachedNode);
                        } else if (type.equals("Compensation")) {
                            linkBoundaryCompensationEvent(node);
                        } else if (node.getMetaData().get("SignalName") != null || type.startsWith("Message-")) {
                            linkBoundarySignalEvent(node, attachedTo);
                        } else if (type.startsWith("Condition-")) {
                            linkBoundaryConditionEvent(nodeContainer, node, attachedTo);
                        }
                    }
                }
            }
        }
    }

    private static void linkBoundaryEscalationEvent(Node node, String attachedTo, Node attachedNode) {
        boolean cancelActivity = (Boolean) node.getMetaData().get("CancelActivity");
        String escalationCode = (String) node.getMetaData().get("EscalationEvent");
        String escalationStructureRef = (String) node.getMetaData().get("EscalationStructureRef");

        ContextContainer compositeNode = (ContextContainer) attachedNode;
        ExceptionScope exceptionScope = (ExceptionScope) compositeNode.getDefaultContext(ExceptionScope.EXCEPTION_SCOPE);
        if (exceptionScope == null) {
            exceptionScope = new ExceptionScope();
            compositeNode.addContext(exceptionScope);
            compositeNode.setDefaultContext(exceptionScope);
        }

        String variable = ((EventNode) node).getVariableName();
        ActionExceptionHandler exceptionHandler = new ActionExceptionHandler();
        DroolsConsequenceAction action =
                createJavaAction(new SignalProcessInstanceAction("Escalation-" + attachedTo + "-" + escalationCode, variable, null, SignalProcessInstanceAction.PROCESS_INSTANCE_SCOPE));
        exceptionHandler.setAction(action);
        exceptionHandler.setFaultVariable(variable);
        exceptionScope.setExceptionHandler(escalationCode, exceptionHandler);
        if (escalationStructureRef != null) {
            exceptionScope.setExceptionHandler(escalationStructureRef, exceptionHandler);
        }

        if (cancelActivity) {
            List actions = ((EventNode) node).getActions(ExtendedNodeImpl.EVENT_NODE_EXIT);
            if (actions == null) {
                actions = new ArrayList<>();
            }
            DroolsConsequenceAction cancelAction = new DroolsConsequenceAction("java", "");
            cancelAction.setMetaData("Action", new CancelNodeInstanceAction(attachedTo));
            actions.add(cancelAction);
            ((EventNode) node).setActions(ExtendedNodeImpl.EVENT_NODE_EXIT, actions);
        }
    }

    private static void linkBoundaryErrorEvent(Node node, String attachedTo, Node attachedNode) {
        ContextContainer compositeNode = (ContextContainer) attachedNode;
        ExceptionScope exceptionScope = (ExceptionScope) compositeNode.getDefaultContext(ExceptionScope.EXCEPTION_SCOPE);
        if (exceptionScope == null) {
            exceptionScope = new ExceptionScope();
            compositeNode.addContext(exceptionScope);
            compositeNode.setDefaultContext(exceptionScope);
        }
        String errorCode = (String) node.getMetaData().get("ErrorEvent");
        boolean hasErrorCode = (Boolean) node.getMetaData().get("HasErrorEvent");
        String errorStructureRef = (String) node.getMetaData().get("ErrorStructureRef");
        ActionExceptionHandler exceptionHandler = new ActionExceptionHandler();

        String variable = ((EventNode) node).getVariableName();
        SignalProcessInstanceAction signalAction = new SignalProcessInstanceAction("Error-" + attachedTo + "-" + errorCode, variable, null, SignalProcessInstanceAction.PROCESS_INSTANCE_SCOPE);
        DroolsConsequenceAction action = createJavaAction(signalAction);
        exceptionHandler.setAction(action);
        exceptionHandler.setFaultVariable(variable);
        exceptionScope.setExceptionHandler(hasErrorCode ? errorCode : null, exceptionHandler);
        if (errorStructureRef != null) {
            exceptionScope.setExceptionHandler(errorStructureRef, exceptionHandler);
        }

        List actions = ((EventNode) node).getActions(ExtendedNodeImpl.EVENT_NODE_EXIT);
        if (actions == null) {
            actions = new ArrayList<>();
        }
        DroolsConsequenceAction cancelAction = new DroolsConsequenceAction("java", null);
        cancelAction.setMetaData("Action", new CancelNodeInstanceAction(attachedTo));
        actions.add(cancelAction);
        ((EventNode) node).setActions(ExtendedNodeImpl.EVENT_NODE_EXIT, actions);
    }

    private static void linkBoundaryTimerEvent(Node node, String attachedTo, Node attachedNode) {
        boolean cancelActivity = (Boolean) node.getMetaData().get("CancelActivity");
        StateBasedNode compositeNode = (StateBasedNode) attachedNode;
        String timeDuration = (String) node.getMetaData().get("TimeDuration");
        String timeCycle = (String) node.getMetaData().get("TimeCycle");
        String timeDate = (String) node.getMetaData().get("TimeDate");
        Timer timer = new Timer();
        if (timeDuration != null) {
            timer.setDelay(timeDuration);
            timer.setTimeType(Timer.TIME_DURATION);
            DroolsConsequenceAction consequenceAction = createJavaAction(new SignalProcessInstanceAction("Timer-" + attachedTo + "-" + timeDuration + "-" + node.getId(),
                    kcontext -> kcontext.getNodeInstance().getStringId(), SignalProcessInstanceAction.PROCESS_INSTANCE_SCOPE));
            compositeNode.addTimer(timer, consequenceAction);
        } else if (timeCycle != null) {
            int index = timeCycle.indexOf("###");
            if (index != -1) {
                String period = timeCycle.substring(index + 3);
                timeCycle = timeCycle.substring(0, index);
                timer.setPeriod(period);
            }
            timer.setDelay(timeCycle);
            timer.setTimeType(Timer.TIME_CYCLE);

            String finalTimeCycle = timeCycle;

            DroolsConsequenceAction action =
                    createJavaAction(new SignalProcessInstanceAction("Timer-" + attachedTo + "-" + finalTimeCycle + (timer.getPeriod() == null ? "" : "###" + timer.getPeriod()) + "-" + node.getId(),
                            kcontext -> kcontext.getNodeInstance().getStringId(), SignalProcessInstanceAction.PROCESS_INSTANCE_SCOPE));
            compositeNode.addTimer(timer, action);
        } else if (timeDate != null) {
            timer.setDate(timeDate);
            timer.setTimeType(Timer.TIME_DATE);
            DroolsConsequenceAction action = createJavaAction(new SignalProcessInstanceAction("Timer-" + attachedTo + "-" + timeDate + "-" + node.getId(),
                    kcontext -> kcontext.getNodeInstance().getStringId(), SignalProcessInstanceAction.PROCESS_INSTANCE_SCOPE));
            compositeNode.addTimer(timer, action);
        }

        if (cancelActivity) {
            List actions = ((EventNode) node).getActions(ExtendedNodeImpl.EVENT_NODE_EXIT);
            if (actions == null) {
                actions = new ArrayList<>();
            }
            DroolsConsequenceAction action = createJavaAction(new CancelNodeInstanceAction(attachedTo));
            actions.add(action);
            ((EventNode) node).setActions(ExtendedNodeImpl.EVENT_NODE_EXIT, actions);
        }
    }

    private static void linkBoundaryCompensationEvent(Node node) {
        /**
         * BPMN2 Spec, p. 264:
         * "For an Intermediate event attached to the boundary of an activity:"
         * ...
         * The Activity the Event is attached to will provide the Id necessary
         * to match the Compensation Event with the Event that threw the compensation"
         * 
         * In other words: "activityRef" is and should be IGNORED
         */

        String activityRef = (String) node.getMetaData().get("ActivityRef");
        if (activityRef != null) {
            logger.warn("Attribute activityRef={} will be IGNORED since this is a Boundary Compensation Event.", activityRef);
        }

        // linkAssociations takes care of the rest
    }

    private static void linkBoundarySignalEvent(Node node, String attachedTo) {
        boolean cancelActivity = (Boolean) node.getMetaData().get("CancelActivity");
        if (cancelActivity) {
            List actions = ((EventNode) node).getActions(ExtendedNodeImpl.EVENT_NODE_EXIT);
            if (actions == null) {
                actions = new ArrayList<>();
            }
            DroolsConsequenceAction action = createJavaAction(new CancelNodeInstanceAction(attachedTo));
            actions.add(action);
            ((EventNode) node).setActions(ExtendedNodeImpl.EVENT_NODE_EXIT, actions);
        }
    }

    private static void linkBoundaryConditionEvent(NodeContainer nodeContainer, Node node, String attachedTo) {
        String processId = ((RuleFlowProcess) nodeContainer).getId();
        String eventType = "RuleFlowStateEvent-" + processId + "-" + ((EventNode) node).getUniqueId() + "-" + attachedTo;
        ((EventTypeFilter) ((EventNode) node).getEventFilters().get(0)).setType(eventType);
        boolean cancelActivity = (Boolean) node.getMetaData().get("CancelActivity");
        if (cancelActivity) {
            List actions = ((EventNode) node).getActions(ExtendedNodeImpl.EVENT_NODE_EXIT);
            if (actions == null) {
                actions = new ArrayList<>();
            }
            DroolsConsequenceAction action = createJavaAction(new CancelNodeInstanceAction(attachedTo));
            actions.add(action);
            ((EventNode) node).setActions(ExtendedNodeImpl.EVENT_NODE_EXIT, actions);
        }
    }

    public static void linkAssociations(Definitions definitions, NodeContainer nodeContainer, List associations) {
        if (associations != null) {
            for (Association association : associations) {
                String sourceRef = association.getSourceRef();
                Object source = null;
                try {
                    source = findNodeOrDataStoreByUniqueId(definitions, nodeContainer, sourceRef,
                            "Could not find source [" + sourceRef + "] for association " + association.getId() + "]");
                } catch (IllegalArgumentException e) {
                    // source not found
                }
                String targetRef = association.getTargetRef();
                Object target = null;
                try {
                    target = findNodeOrDataStoreByUniqueId(definitions, nodeContainer, targetRef,
                            "Could not find target [" + targetRef + "] for association [" + association.getId() + "]");
                } catch (IllegalArgumentException e) {
                    // target not found
                }
                if (source == null || target == null) {
                    // TODO: ignoring this association for now
                } else if (target instanceof DataStore || source instanceof DataStore) {
                    // TODO: ignoring data store associations for now
                } else if (source instanceof EventNode) {
                    EventNode sourceNode = (EventNode) source;
                    KogitoNode targetNode = (KogitoNode) target;
                    checkBoundaryEventCompensationHandler(association, sourceNode, targetNode);

                    // make sure IsForCompensation is set to true on target
                    NodeImpl targetNodeImpl = (NodeImpl) target;
                    String isForCompensation = "isForCompensation";
                    Object compensationObject = targetNodeImpl.getMetaData(isForCompensation);
                    if (compensationObject == null) {
                        targetNodeImpl.setMetaData(isForCompensation, true);
                        logger.warn("Setting {} attribute to true for node {}", isForCompensation, targetRef);
                    } else if (!Boolean.parseBoolean(compensationObject.toString())) {
                        throw new ProcessParsingValidationException(isForCompensation + " attribute [" + compensationObject + "] should be true for Compensation Activity [" + targetRef + "]");
                    }

                    // put Compensation Handler in CompensationHandlerNode
                    NodeContainer sourceParent = sourceNode.getParentContainer();
                    NodeContainer targetParent = targetNode.getParentContainer();
                    if (!sourceParent.equals(targetParent)) {
                        throw new ProcessParsingValidationException("Compensation Associations may not cross (sub-)process boundaries,");
                    }

                    // connect boundary event to compensation activity
                    ConnectionImpl connection = new ConnectionImpl(sourceNode, org.jbpm.workflow.core.Node.CONNECTION_DEFAULT_TYPE, targetNode, org.jbpm.workflow.core.Node.CONNECTION_DEFAULT_TYPE);
                    connection.setMetaData("UniqueId", null);
                    connection.setMetaData("hidden", true);
                    connection.setMetaData("association", true);

                    // Compensation use cases: 
                    // - boundary event --associated-> activity
                    // - implicit sub process compensation handler + recursive? 

                    /**
                     * BPMN2 spec, p.442:
                     * "A Compensation Event Sub-process becomes enabled when its parent Activity transitions into state
                     * Completed. At that time, a snapshot of the data associated with the parent Acitivity is taken and kept for
                     * later usage by the Compensation Event Sub-Process."
                     */
                }
            }
        }
    }

    /**
     * This logic belongs in {@link RuleFlowProcessValidator} -- except that {@link Association}s are a jbpm-bpmn2 class,
     * and {@link RuleFlowProcessValidator} is a jbpm-flow class..
     * 

* Maybe we should have a BPMNProcessValidator class? * * @param association The association to check. * @param source The source of the association. * @param target The target of the association. */ private static void checkBoundaryEventCompensationHandler(Association association, Node source, Node target) { // check that // - event node is boundary event node if (!(source instanceof BoundaryEventNode)) { throw new ProcessParsingValidationException("(Compensation) activities may only be associated with Boundary Event Nodes (not with" + source.getClass().getSimpleName() + " nodes [node " + ((String) source.getMetaData().get("UniqueId")) + "]."); } BoundaryEventNode eventNode = (BoundaryEventNode) source; // - event node has compensationEvent List eventFilters = eventNode.getEventFilters(); boolean compensationCheckPassed = false; if (eventFilters != null) { for (EventFilter filter : eventFilters) { if (filter instanceof EventTypeFilter) { String type = ((EventTypeFilter) filter).getType(); if (type != null && type.equals("Compensation")) { compensationCheckPassed = true; } } } } if (!compensationCheckPassed) { throw new ProcessParsingValidationException("An Event [" + ((String) eventNode.getMetaData("UniqueId")) + "] linked from an association [" + association.getId() + "] must be a (Boundary) Compensation Event."); } // - boundary event node is attached to the correct type of node? /** * Tasks: * business: RuleSetNode * manual: WorkItemNode * receive: WorkItemNode * script: ActionNode * send: WorkItemNode * service: WorkItemNode * task: WorkItemNode * user: HumanTaskNode */ String attachedToId = eventNode.getAttachedToNodeId(); Node attachedToNode = null; for (Node node : eventNode.getParentContainer().getNodes()) { if (attachedToId.equals(node.getMetaData().get("UniqueId"))) { attachedToNode = node; break; } } if (attachedToNode == null) { throw new ProcessParsingValidationException("Boundary Event [" + ((String) eventNode.getMetaData("UniqueId")) + "] is not attached to a node [" + attachedToId + "] that can be found."); } if (!(attachedToNode instanceof RuleSetNode || attachedToNode instanceof WorkItemNode || attachedToNode instanceof ActionNode || attachedToNode instanceof HumanTaskNode || attachedToNode instanceof CompositeNode || attachedToNode instanceof SubProcessNode)) { throw new ProcessParsingValidationException("Compensation Boundary Event [" + ((String) eventNode.getMetaData("UniqueId")) + "] must be attached to a task or sub-process."); } // - associated node is a task or subProcess compensationCheckPassed = false; if (target instanceof WorkItemNode || target instanceof HumanTaskNode || target instanceof CompositeContextNode || target instanceof SubProcessNode) { compensationCheckPassed = true; } else if (target instanceof ActionNode) { Object nodeTypeObj = ((ActionNode) target).getMetaData("NodeType"); if (nodeTypeObj != null && nodeTypeObj.equals("ScriptTask")) { compensationCheckPassed = true; } } if (!compensationCheckPassed) { throw new ProcessParsingValidationException("An Activity [" + ((String) ((NodeImpl) target).getMetaData("UniqueId")) + "] associated with a Boundary Compensation Event must be a Task or a (non-Event) Sub-Process"); } // - associated node does not have outgoingConnections of it's own compensationCheckPassed = true; NodeImpl targetNode = (NodeImpl) target; Map> connectionsMap = targetNode.getOutgoingConnections(); ConnectionImpl outgoingConnection = null; for (String connectionType : connectionsMap.keySet()) { List connections = connectionsMap.get(connectionType); if (connections != null && !connections.isEmpty()) { for (org.kie.api.definition.process.Connection connection : connections) { Object hiddenObj = connection.getMetaData().get("hidden"); if (hiddenObj != null && ((Boolean) hiddenObj)) { continue; } outgoingConnection = (ConnectionImpl) connection; compensationCheckPassed = false; break; } } } if (!compensationCheckPassed) { throw new ProcessParsingValidationException("A Compensation Activity [" + ((String) targetNode.getMetaData("UniqueId")) + "] may not have any outgoing connection [" + (String) outgoingConnection.getMetaData("UniqueId") + "]"); } } private void assignLanes(RuleFlowProcess process, List lanes) { List laneNames = new ArrayList<>(); Map laneMapping = new HashMap<>(); if (lanes != null) { for (Lane lane : lanes) { String name = lane.getName(); if (name != null) { Swimlane swimlane = new Swimlane(); swimlane.setName(name); process.getSwimlaneContext().addSwimlane(swimlane); laneNames.add(name); for (String flowElementRef : lane.getFlowElements()) { laneMapping.put(flowElementRef, name); } } } } assignLanes(process, laneMapping); } private void postProcessNodes(RuleFlowProcess process, NodeContainer container) { List eventSubProcessHandlers = new ArrayList<>(); for (Node node : container.getNodes()) { if (node instanceof StateNode) { StateNode stateNode = (StateNode) node; String condition = (String) stateNode.getMetaData("Condition"); Constraint constraint = new ConstraintImpl(); constraint.setConstraint(condition); constraint.setType("rule"); for (org.kie.api.definition.process.Connection connection : stateNode.getDefaultOutgoingConnections()) { stateNode.setConstraint(connection, constraint); } } else if (node instanceof NodeContainer) { // prepare event sub process if (node instanceof EventSubProcessNode) { EventSubProcessNode eventSubProcessNode = (EventSubProcessNode) node; Node[] nodes = eventSubProcessNode.getNodes(); for (Node subNode : nodes) { // avoids cyclomatic complexity if (subNode == null || !(subNode instanceof StartNode)) { continue; } List triggers = ((StartNode) subNode).getTriggers(); if (triggers == null) { continue; } for (Trigger trigger : triggers) { if (trigger instanceof EventTrigger) { final List filters = ((EventTrigger) trigger).getEventFilters(); for (EventFilter filter : filters) { if (filter instanceof EventTypeFilter) { eventSubProcessNode.addEvent((EventTypeFilter) filter); String type = ((EventTypeFilter) filter).getType(); if (type.startsWith("Error-") || type.startsWith("Escalation")) { String faultCode = (String) subNode.getMetaData().get("FaultCode"); String replaceRegExp = "Error-|Escalation-"; final String signalType = type; ExceptionScope exceptionScope = (ExceptionScope) ((ContextContainer) eventSubProcessNode.getParentContainer()).getDefaultContext(ExceptionScope.EXCEPTION_SCOPE); if (exceptionScope == null) { exceptionScope = new ExceptionScope(); ((ContextContainer) eventSubProcessNode.getParentContainer()).addContext(exceptionScope); ((ContextContainer) eventSubProcessNode.getParentContainer()).setDefaultContext(exceptionScope); } String faultVariable = null; if (trigger.getInAssociations() != null && !trigger.getInAssociations().isEmpty()) { faultVariable = findVariable(trigger.getInAssociations().get(0).getTarget().getLabel(), process.getVariableScope()); } ActionExceptionHandler exceptionHandler = new ActionExceptionHandler(); DroolsConsequenceAction action = new DroolsConsequenceAction("java", ""); action.setMetaData("Action", new SignalProcessInstanceAction(signalType, faultVariable, null, SignalProcessInstanceAction.PROCESS_INSTANCE_SCOPE)); exceptionHandler.setAction(action); exceptionHandler.setFaultVariable(faultVariable); if (faultCode != null) { String trimmedType = type.replaceFirst(replaceRegExp, ""); exceptionScope.setExceptionHandler(trimmedType, exceptionHandler); eventSubProcessHandlers.add(trimmedType); } else { exceptionScope.setExceptionHandler(faultCode, exceptionHandler); } } else if (type.equals("Compensation")) { // 1. Find the parent sub-process to this event sub-process NodeContainer parentSubProcess = null; NodeContainer subProcess = eventSubProcessNode.getParentContainer(); Object isForCompensationObj = eventSubProcessNode.getMetaData("isForCompensation"); if (isForCompensationObj == null) { eventSubProcessNode.setMetaData("isForCompensation", true); logger.warn("Overriding empty value of \"isForCompensation\" attribute on Event Sub-Process [{}] and setting it to true.", eventSubProcessNode.getMetaData("UniqueId")); } String compensationHandlerId = ""; if (subProcess instanceof RuleFlowProcess) { // If jBPM deletes the process (instance) as soon as the process completes.. // ..how do you expect to signal compensation on the completed process (instance)?!? throw new ProcessParsingValidationException("Compensation Event Sub-Processes at the process level are not supported."); } if (subProcess instanceof Node) { parentSubProcess = ((KogitoNode) subProcess).getParentContainer(); compensationHandlerId = (String) ((CompositeNode) subProcess).getMetaData(Metadata.UNIQUE_ID); } // 2. The event filter (never fires, purely for dumping purposes) has already been added // 3. Add compensation scope addCompensationScope(process, eventSubProcessNode, parentSubProcess, compensationHandlerId); } } } } else if (trigger instanceof ConstraintTrigger) { ConstraintTrigger constraintTrigger = (ConstraintTrigger) trigger; if (constraintTrigger.getConstraint() != null) { String processId = ((RuleFlowProcess) container).getId(); String type = "RuleFlowStateEventSubProcess-Event-" + processId + "-" + eventSubProcessNode.getUniqueId(); EventTypeFilter eventTypeFilter = new EventTypeFilter(); eventTypeFilter.setType(type); eventSubProcessNode.addEvent(eventTypeFilter); } } } } } postProcessNodes(process, (NodeContainer) node); } else if (node instanceof EndNode) { handleIntermediateOrEndThrowCompensationEvent((EndNode) node); } else if (node instanceof ActionNode) { handleIntermediateOrEndThrowCompensationEvent((ActionNode) node); } else if (node instanceof EventNode) { final EventNode eventNode = (EventNode) node; if (!(eventNode instanceof BoundaryEventNode) && eventNode.getDefaultIncomingConnections().isEmpty()) { throw new ProcessParsingValidationException("Event node '" + node.getName() + "' [" + node.getId() + "] has no incoming connection"); } } } // process fault node to disable termnate parent if there is event subprocess handler for (Node node : container.getNodes()) { if (node instanceof FaultNode) { FaultNode faultNode = (FaultNode) node; if (eventSubProcessHandlers.contains(faultNode.getFaultName())) { faultNode.setTerminateParent(false); } } } } private void assignLanes(NodeContainer nodeContainer, Map laneMapping) { for (Node node : nodeContainer.getNodes()) { String lane = null; String uniqueId = (String) node.getMetaData().get("UniqueId"); if (uniqueId != null) { lane = laneMapping.get(uniqueId); } else { lane = laneMapping.get(XmlBPMNProcessDumper.getUniqueNodeId(node)); } if (lane != null) { ((NodeImpl) node).setMetaData("Lane", lane); if (node instanceof HumanTaskNode) { ((HumanTaskNode) node).setSwimlane(lane); } } if (node instanceof NodeContainer) { assignLanes((NodeContainer) node, laneMapping); } } } private static Constraint buildConstraint(SequenceFlow connection, NodeImpl node) { if (connection.getExpression() == null) { return null; } Constraint constraint = new ConstraintImpl(); String defaultConnection = (String) node.getMetaData("Default"); if (defaultConnection != null && defaultConnection.equals(connection.getId())) { constraint.setDefault(true); } if (connection.getName() != null) { constraint.setName(connection.getName()); } else { constraint.setName(""); } if (connection.getType() != null) { constraint.setType(connection.getType()); } else { constraint.setType("code"); } if (connection.getLanguage() != null) { constraint.setDialect(connection.getLanguage()); } if (connection.getExpression() != null) { constraint.setConstraint(connection.getExpression()); } constraint.setPriority(connection.getPriority()); return constraint; } protected static void addCompensationScope(final RuleFlowProcess process, final Node node, final org.kie.api.definition.process.NodeContainer parentContainer, final String compensationHandlerId) { process.getMetaData().put("Compensation", true); assert parentContainer instanceof ContextContainer : "Expected parent node to be a CompositeContextNode, not a " + parentContainer.getClass().getSimpleName(); ContextContainer contextContainer = (ContextContainer) parentContainer; CompensationScope scope = null; boolean addScope = false; if (contextContainer.getContexts(CompensationScope.COMPENSATION_SCOPE) == null) { addScope = true; } else { scope = (CompensationScope) contextContainer.getContexts(CompensationScope.COMPENSATION_SCOPE).get(0); if (scope == null) { addScope = true; } } if (addScope) { scope = new CompensationScope(); contextContainer.addContext(scope); contextContainer.setDefaultContext(scope); scope.setContextContainer(contextContainer); } CompensationHandler handler = new CompensationHandler(); handler.setNode(node); if (scope.getExceptionHandler(compensationHandlerId) != null) { throw new ProcessParsingValidationException( "More than one compensation handler per node (" + compensationHandlerId + ")" + " is not supported!"); } scope.setExceptionHandler(compensationHandlerId, handler); } protected void handleIntermediateOrEndThrowCompensationEvent(ExtendedNodeImpl throwEventNode) { if (throwEventNode.getMetaData("compensation-activityRef") != null) { String activityRef = (String) throwEventNode.getMetaData().remove("compensation-activityRef"); NodeContainer nodeParent = throwEventNode.getParentContainer(); if (nodeParent instanceof EventSubProcessNode) { boolean compensationEventSubProcess = false; List startTriggers = ((EventSubProcessNode) nodeParent).findStartNode().getTriggers(); CESP_CHECK: for (Trigger trigger : startTriggers) { if (trigger instanceof EventTrigger) { for (EventFilter filter : ((EventTrigger) trigger).getEventFilters()) { if (((EventTypeFilter) filter).getType().equals(Metadata.EVENT_TYPE_COMPENSATION)) { compensationEventSubProcess = true; break CESP_CHECK; } } } } if (compensationEventSubProcess) { // BPMN2 spec, p. 252, p. 248: intermediate and end compensation event visibility scope nodeParent = ((NodeImpl) nodeParent).getParentContainer(); } } String parentId; if (nodeParent instanceof RuleFlowProcess) { parentId = ((RuleFlowProcess) nodeParent).getId(); } else { parentId = (String) ((NodeImpl) nodeParent).getMetaData("UniqueId"); } String compensationEvent; if (activityRef.isEmpty()) { // general/implicit compensation compensationEvent = CompensationScope.IMPLICIT_COMPENSATION_PREFIX + parentId; } else { // specific compensation compensationEvent = activityRef; } DroolsConsequenceAction compensationAction = new DroolsConsequenceAction("java", ""); compensationAction.setMetaData("Action", new ProcessInstanceCompensationAction(compensationEvent)); if (throwEventNode instanceof ActionNode) { ((ActionNode) throwEventNode).setAction(compensationAction); } else if (throwEventNode instanceof EndNode) { List actions = new ArrayList<>(); actions.add(compensationAction); ((EndNode) throwEventNode).setActions(ExtendedNodeImpl.EVENT_NODE_ENTER, actions); } } } /** * Finds the right variable by its name to make sure that when given as id it will be also matched * * @param variableName name or id of the variable * @param variableScope VariableScope of given process * @return returns found variable name or given 'variableName' otherwise */ protected String findVariable(String variableName, VariableScope variableScope) { if (variableName == null) { return null; } return variableScope.getVariables().stream().filter(v -> v.matchByIdOrName(variableName)).map(v -> v.getName()).findFirst().orElse(variableName); } public static DroolsConsequenceAction createJavaAction(Action action) { DroolsConsequenceAction consequenceAction = new DroolsConsequenceAction("java", ""); consequenceAction.setMetaData("Action", action); return consequenceAction; } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy