/** * Copyright (C) 2015 Orange * 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 com.francetelecom.clara.cloud.paas.activation.v1; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.ListIterator; import java.util.Map; import java.util.Set; import org.activiti.bpmn.model.FlowElement; import org.activiti.bpmn.model.ParallelGateway; import org.activiti.bpmn.model.Process; import org.activiti.bpmn.model.SequenceFlow; import org.activiti.bpmn.model.ServiceTask; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.util.Assert; import com.francetelecom.clara.cloud.commons.TechnicalException; import com.francetelecom.clara.cloud.model.DependantModelItem; import com.francetelecom.clara.cloud.paas.activation.ActivationStepEnum; import com.google.common.base.Objects; /** * Convenient methods to help activiti process factory. */ final class ActivitiProcessUtils { private static Logger logger = LoggerFactory.getLogger(ActivitiProcessUtils.class.getName()); static class NodeTask { Set<NodeTask> dependOn = new HashSet<NodeTask>(); Set<NodeTask> dependOnMe = new HashSet<NodeTask>(); DependantModelItem item = null; String outputId = null; String inputId = null; @Override public String toString() { return Objects.toStringHelper(this) // .add("dependOn", dependOn) .add("dependOnMe", dependOnMe) // .add("item", item) //.add("outputId", outputId) //.add("inputId", inputId) .addValue("name="+(item != null ? item.getName():"")) .toString(); } } static String generateId(String serviceName, NodeTask node) { return serviceName+"-"+node.item.hashCode()+"-si"; } /** * Creates a tree of nodes representing tasks in the process * @param nodes Map of node: key is item name * @param cache A map that is used as a cache * @param item The entity to add to the tree * @param canParrallel true if task can be paralellize (set to false if not sure) * @param step The activation step * @param pluginStrategy The plugin strategy * @return A tree of nodes representing tasks in the process */ static Set<NodeTask> addService(Map<String, NodeTask> nodes, Map<String, Set<NodeTask>> cache, DependantModelItem item, boolean canParrallel, ActivationStepEnum step, ActivationPluginStrategy pluginStrategy) { if (item == null) { throw new TechnicalException("Unable to create activation step (tree node) from a null model item"); } logger.trace("add service item {}={}", item.getClass().getSimpleName(), item); Set<NodeTask> returnNodes = cache.get(item.getName()); if (returnNodes == null) { returnNodes = new HashSet<NodeTask>(); boolean ignoreNode = pluginStrategy.getPlugin(item.getClass(), step) == null; if (ignoreNode) { for (DependantModelItem dependancy : item.listDepedencies()) { returnNodes.addAll(addService(nodes, cache, dependancy, canParrallel, step, pluginStrategy)); } } else { // Create a node for this service NodeTask node = new NodeTask(); node.item = item; NodeTask lastDependNode = null; int lastDependNodeIndex = -1; for (DependantModelItem dependancy : item.listDepedencies()) { // Go thru all items and get corresponding dependant nodes // We must do this first loop to be sure that all dependancies // have been inserted before computing the right place to insert our // node addService(nodes, cache, dependancy, canParrallel, step, pluginStrategy); } for (DependantModelItem dependancy : item.listDepedencies()) { // get corresponding dependant nodes (cache will be used) Set<NodeTask> dependantNodes = addService(nodes, cache, dependancy, canParrallel, step, pluginStrategy); for (NodeTask dependNode : dependantNodes) { // Check that this service is not us (it can be) if (!dependNode.item.equals(item)) { if (canParrallel) { // Inform the node that we depend on him dependNode.dependOnMe.add(node); // Add a dependancy to the service we depend on node.dependOn.add(dependNode); } else { // Find position where to insert the node in the sequencial tree int dependNodeIndex = findIndexOf(nodes, dependNode); if (dependNodeIndex > lastDependNodeIndex) { lastDependNodeIndex = dependNodeIndex; lastDependNode = dependNode; } } } } } if (!canParrallel) { if (lastDependNode == null) { lastDependNode = findFirst(nodes); } if (lastDependNode != null) { // Note, invarriant is "for all node, dependOnMe.size() equals to 0 or 1 AND dependOn.size() equals to 0 or 1" if (lastDependNode.dependOnMe.size() == 0) { // Inform the node that we depend on him lastDependNode.dependOnMe.add(node); logger.debug("Insert "+node.item.getClass().getSimpleName()+"#"+node.item.getId()+"[name="+node.item.getName()+"] after "+lastDependNode.item.getClass().getSimpleName()+"#"+lastDependNode.item.getId()); } else { // Insert the node between this current node and the dependOnMe node NodeTask other = lastDependNode.dependOnMe.iterator().next(); lastDependNode.dependOnMe.clear(); lastDependNode.dependOnMe.add(node); other.dependOn.clear(); other.dependOn.add(node); node.dependOnMe.add(other); logger.debug("Insert "+node.item.getClass().getSimpleName()+"#"+node.item.getId()+"[name="+node.item.getName()+"] between "+lastDependNode.item.getClass().getSimpleName()+"#"+lastDependNode.item.getId()+" and "+other.item.getClass().getSimpleName()+"#"+other.item.getId()); } // Add a dependancy to the service we depend on node.dependOn.add(lastDependNode); } } logger.trace("putting item{}={}", item.getClass().getSimpleName(), item); nodes.put(item.getName(), node); returnNodes.add(node); } cache.put(item.getName(), returnNodes); } return returnNodes; } /** * Return position of the node into the tree which is sequencial (dependOn and dependOnMe have only 0 or 1 element) * @param nodes List of nodes * @param dependNode Node to position * @return index of node (starts at zero) */ public static int findIndexOf(Map<String, NodeTask> nodes, NodeTask dependNode) { int index = 0; if (dependNode.dependOn.size() > 0) { index = 1 + findIndexOf(nodes, dependNode.dependOn.iterator().next()); } logger.debug("Position of "+dependNode.item.getClass().getSimpleName()+"#"+dependNode.item.getId()+" => "+index); return index; } /** * Return the first node of the sequential tree (dependOn and dependOnMe have only 0 or 1 element) * @param nodes List of nodes * @return The first node */ static NodeTask findFirst(Map<String, NodeTask> nodes) { NodeTask first = null; for (NodeTask node : nodes.values()) { if (node.dependOn.size() == 0) { first = node; break; } } return first; } public static void logSequence(Map<String, NodeTask> nodes, ActivationStepEnum step) { if (!logger.isDebugEnabled()) { return; } NodeTask first = findFirst(nodes); logger.debug("nodes sequence for step {} : {}", step, displayDependOnMe(first)); } private static String displayDependOnMe(NodeTask node) { if (node == null) { return ""; } String ret; if (node.item != null) { ret = " -> " + node.item.getClass().getSimpleName() + "#" + node.item.getId(); } else { ret = " -> (no item) "; } if (node.dependOnMe != null && node.dependOnMe.iterator().hasNext()) { ret += displayDependOnMe(node.dependOnMe.iterator().next()); } return ret; } static String generateGatewayId(Collection<NodeTask> nodes) { StringBuffer id = new StringBuffer(); for (NodeTask node : nodes) { id.append(node.item.hashCode()); id.append("_"); } return id.toString(); } /** * Remove useless gateway (1 incoming and 1 outgoing, replace by sequenceflow). * Use activiti syntax for gateways (do not use outgoing and incoming tag, use sequenceflow). * Replace subsequent sequenceflow by only one sequenceflow: A -> -> B become A -> B. * @param process Process to simplify * @param taskCount Total task count */ static void simplifyProcess(Process process, int taskCount) { HashMap<String, FlowElement> elements = new HashMap<String, FlowElement>(); ListIterator<FlowElement> it; for (FlowElement flowElement: process.getFlowElements()) { elements.put(flowElement.getId(), flowElement); } it = ((List<FlowElement>) process.getFlowElements()).listIterator(); while (it.hasNext()) { FlowElement flowElement = it.next(); if (flowElement instanceof ParallelGateway) { ParallelGateway gw = (ParallelGateway) flowElement; if (gw.getIncomingFlows().size() == 1 && gw.getOutgoingFlows().size() == 1) { // This can be replaced by a sequenceFlow tag SequenceFlow flow = new SequenceFlow(); flow.setId(gw.getId()); flow.setSourceRef(gw.getIncomingFlows().get(0).getSourceRef()); flow.setTargetRef(gw.getOutgoingFlows().get(0).getTargetRef()); if (gw.getIncomingFlows().get(0).getSourceRef().endsWith(ActivitiProcessFactory.NODE_SUFFIX_CONDITIONNAL)) { flow.setConditionExpression("${errCode == 0}"); } // Remove old gateway and replace it by the new element it.set(flow); } else if (gw.getIncomingFlows().size() > 0 && gw.getOutgoingFlows().size() > 0) { // Activiti special notation: do not use outgoing and incoming tag, use sequenceflow ParallelGateway tParallelGateway = new ParallelGateway(); tParallelGateway.setId(gw.getId()); // Remove old gateway and replace it by the new element it.set(tParallelGateway); for (SequenceFlow in : gw.getIncomingFlows()) { SequenceFlow flow = new SequenceFlow(); flow.setId("gw-"+in.toString()+"-"+gw.getId()); flow.setSourceRef(in.getId()); flow.setTargetRef(tParallelGateway.getId()); if (in.toString().endsWith(ActivitiProcessFactory.NODE_SUFFIX_CONDITIONNAL)) { flow.setConditionExpression("${errCode == 0}"); } it.add(flow); } for (SequenceFlow out : gw.getOutgoingFlows()) { SequenceFlow flow = new SequenceFlow(); flow.setId("gw-"+gw.getId()+"-"+out.toString()); flow.setSourceRef(tParallelGateway.getId()); flow.setTargetRef(out.getId()); it.add(flow); } } } else if (flowElement instanceof ServiceTask) { ServiceTask task = (ServiceTask) flowElement; task.setImplementation(task.getImplementation().replace(ActivitiProcessFactory.TASK_COUNT_KEY, String.valueOf(taskCount))); } } // Now, replace subsequent sequenceflow by only one sequenceflow // A -> -> B become A -> B boolean continueSimplification = true; while (continueSimplification) { continueSimplification = false; elements.clear(); it = ((List<FlowElement>) process.getFlowElements()).listIterator(); while (it.hasNext()) { FlowElement flowElement = it.next(); if (flowElement instanceof SequenceFlow) { SequenceFlow flow = (SequenceFlow) flowElement; Assert.hasText(flow.getSourceRef(),"sourceRef is null flow="+flow.getId()); FlowElement source = elements.get(flow.getSourceRef()); if (source instanceof SequenceFlow) { // Invalid, must do a short cut ((SequenceFlow)source).setTargetRef(flow.getTargetRef()); it.remove(); continueSimplification = true; } } } } } }