package alien4cloud.application; import static alien4cloud.paas.function.FunctionEvaluator.isGetInput; import java.util.*; import java.util.Map.Entry; import javax.annotation.Resource; import org.alien4cloud.tosca.model.templates.*; import org.springframework.stereotype.Service; import com.google.common.collect.Maps; import com.google.common.collect.Sets; import alien4cloud.component.ICSARRepositorySearchService; import alien4cloud.exception.AlreadyExistException; import alien4cloud.exception.CyclicReferenceException; import org.alien4cloud.tosca.model.definitions.AbstractPropertyValue; import org.alien4cloud.tosca.model.definitions.FunctionPropertyValue; import org.alien4cloud.tosca.model.types.NodeType; import alien4cloud.paas.wf.Workflow; import alien4cloud.paas.wf.WorkflowsBuilderService; import alien4cloud.paas.wf.WorkflowsBuilderService.TopologyContext; import alien4cloud.topology.TopologyServiceCore; import alien4cloud.utils.MapUtil; import lombok.extern.slf4j.Slf4j; @Slf4j @Service public class TopologyCompositionService { @Resource private ICSARRepositorySearchService csarRepoSearchService; @Resource private TopologyServiceCore topologyServiceCore; @Resource private WorkflowsBuilderService workflowBuilderService; public void processTopologyComposition(Topology topology) { Deque<CompositionCouple> stack = new ArrayDeque<CompositionCouple>(); recursivelyBuildSubstitutionStack(topology, stack, ""); // now this stack contains all the embedded topology templates if (!stack.isEmpty()) { // iterate over the stack in descending order (manage the deepest topologies at a first time). Iterator<CompositionCouple> compositionIterator = stack.descendingIterator(); while (compositionIterator.hasNext()) { processComposition(compositionIterator.next()); } if (log.isDebugEnabled()) { log.debug(String.format("Topology composition has been processed for topology <%s> substituting %d embeded topologies", topology.getId(), stack.size())); } // std workflows are reinitialized when some composition is processed // TODO: find a better way to manage this TopologyContext topologyContext = workflowBuilderService.buildTopologyContext(topology); workflowBuilderService.reinitWorkflow(Workflow.INSTALL_WF, topologyContext); workflowBuilderService.reinitWorkflow(Workflow.UNINSTALL_WF, topologyContext); } } /** * Process the composition: * <ul> * <li>remove the 'proxy' node from the parent. * <li>merge the child topology nodes into the parent nodes. * <li> * </ul> * * @param compositionCouple */ private void processComposition(CompositionCouple compositionCouple) { // first of all, remove the proxy node from the parent NodeTemplate proxyNodeTemplate = compositionCouple.parent.getNodeTemplates().remove(compositionCouple.nodeName); // properties of the proxy are used to feed the property values of child node that use get_input for (NodeTemplate childNodeTemplate : compositionCouple.child.getNodeTemplates().values()) { for (Entry<String, AbstractPropertyValue> propertyEntry : childNodeTemplate.getProperties().entrySet()) { AbstractPropertyValue pValue = propertyEntry.getValue(); if (isGetInput(pValue)) { String inputName = ((FunctionPropertyValue) pValue).getTemplateName(); propertyEntry.setValue(proxyNodeTemplate.getProperties().get(inputName)); } } for (Entry<String, Capability> capabilityEntry : childNodeTemplate.getCapabilities().entrySet()) { if (capabilityEntry.getValue().getProperties() != null) { for (Entry<String, AbstractPropertyValue> propertyEntry : capabilityEntry.getValue().getProperties().entrySet()) { AbstractPropertyValue pValue = propertyEntry.getValue(); if (isGetInput(pValue)) { String inputName = ((FunctionPropertyValue) pValue).getTemplateName(); propertyEntry.setValue(proxyNodeTemplate.getProperties().get(inputName)); } } } } } // all relations from the proxy must now start from the corresponding node if (proxyNodeTemplate.getRelationships() != null) { for (Entry<String, RelationshipTemplate> e : proxyNodeTemplate.getRelationships().entrySet()) { String relationShipKey = e.getKey(); RelationshipTemplate proxyRelationShip = e.getValue(); String requirementName = proxyRelationShip.getRequirementName(); SubstitutionTarget substitutionTarget = compositionCouple.child.getSubstitutionMapping().getRequirements().get(requirementName); NodeTemplate nodeTemplate = compositionCouple.child.getNodeTemplates().get(substitutionTarget.getNodeTemplateName()); if (nodeTemplate.getRelationships() == null) { Map<String, RelationshipTemplate> relationships = Maps.newHashMap(); nodeTemplate.setRelationships(relationships); } nodeTemplate.getRelationships().put(relationShipKey, proxyRelationShip); proxyRelationShip.setRequirementName(substitutionTarget.getTargetId()); } } // all relations that target the proxy must be redirected to the corresponding child node for (NodeTemplate otherNodes : compositionCouple.parent.getNodeTemplates().values()) { if (otherNodes.getRelationships() != null) { for (RelationshipTemplate relationshipTemplate : otherNodes.getRelationships().values()) { if (relationshipTemplate.getTarget().equals(compositionCouple.nodeName)) { SubstitutionTarget st = compositionCouple.child.getSubstitutionMapping().getCapabilities() .get(relationshipTemplate.getTargetedCapabilityName()); relationshipTemplate.setTarget(st.getNodeTemplateName()); relationshipTemplate.setTargetedCapabilityName(st.getTargetId()); } } } } if (compositionCouple.parent.getOutputAttributes() != null) { Set<String> outputAttributes = compositionCouple.parent.getOutputAttributes().remove(compositionCouple.nodeName); if (outputAttributes != null) { for (String proxyAttributeName : outputAttributes) { sustituteGetAttribute(compositionCouple.child, compositionCouple.parent, proxyAttributeName); } } } // the parent itself expose stuffs, we eventually need to replace substitution targets if (compositionCouple.parent.getSubstitutionMapping() != null) { if (compositionCouple.parent.getSubstitutionMapping().getCapabilities() != null) { for (Entry<String, SubstitutionTarget> substitutionCapabilityEntry : compositionCouple.parent.getSubstitutionMapping().getCapabilities() .entrySet()) { if (substitutionCapabilityEntry.getValue().getNodeTemplateName().equals(compositionCouple.nodeName)) { String targetCapability = substitutionCapabilityEntry.getValue().getTargetId(); // just substitute the substitution target substitutionCapabilityEntry.setValue(compositionCouple.child.getSubstitutionMapping().getCapabilities().get(targetCapability)); } } } if (compositionCouple.parent.getSubstitutionMapping().getRequirements() != null) { for (Entry<String, SubstitutionTarget> e : compositionCouple.parent.getSubstitutionMapping().getRequirements().entrySet()) { if (e.getValue().getNodeTemplateName().equals(compositionCouple.nodeName)) { String targetCapability = e.getValue().getTargetId(); // just substitute the substitution target e.setValue(compositionCouple.child.getSubstitutionMapping().getRequirements().get(targetCapability)); } } } } // merge each child nodes into the parent compositionCouple.parent.getNodeTemplates().putAll(compositionCouple.child.getNodeTemplates()); } /** * Ugly code : since we don't name outputs in alien topology, we are not able to determine if an output is related to a property, to an attribute or to a * capability property. This is done in the same order than alien4cloud.topology.TopologyServiceCore.updateSubstitutionType(Topology) processes substitution * outputs. */ private void sustituteGetAttribute(Topology child, Topology parent, String proxyAttributeName) { if (child.getOutputAttributes() != null) { for (Entry<String, Set<String>> oae : child.getOutputAttributes().entrySet()) { String nodeName = oae.getKey(); for (String oa : oae.getValue()) { if (oa.equals(proxyAttributeName)) { // ok the proxy attribute name matches the embedded node attribute Map<String, Set<String>> parentOas = parent.getOutputAttributes(); if (parentOas == null) { parentOas = Maps.newHashMap(); parent.setOutputAttributes(parentOas); } Set<String> parentNodeOas = parentOas.get(nodeName); if (parentNodeOas == null) { parentNodeOas = Sets.newHashSet(); parentOas.put(nodeName, parentNodeOas); } parentNodeOas.add(proxyAttributeName); return; } } } } if (child.getOutputProperties() != null) { for (Entry<String, Set<String>> ope : child.getOutputProperties().entrySet()) { String nodeName = ope.getKey(); for (String op : ope.getValue()) { if (op.equals(proxyAttributeName)) { // ok the proxy attribute name matches the embedded node property Map<String, Set<String>> parentOps = parent.getOutputProperties(); if (parentOps == null) { parentOps = Maps.newHashMap(); parent.setOutputProperties(parentOps); } Set<String> parentNodeOps = parentOps.get(nodeName); if (parentNodeOps == null) { parentNodeOps = Sets.newHashSet(); parentOps.put(nodeName, parentNodeOps); } parentNodeOps.add(proxyAttributeName); return; } } } } if (child.getOutputCapabilityProperties() != null) { for (Entry<String, Map<String, Set<String>>> ocpe : child.getOutputCapabilityProperties().entrySet()) { String nodeName = ocpe.getKey(); for (Entry<String, Set<String>> cpes : ocpe.getValue().entrySet()) { String embededCapabilityName = cpes.getKey(); for (String op : cpes.getValue()) { if (op.equals(proxyAttributeName)) { // ok the embedded output capability property matches the proxy type output attribute Map<String, Map<String, Set<String>>> parentOcps = parent.getOutputCapabilityProperties(); if (parentOcps == null) { parentOcps = Maps.newHashMap(); parent.setOutputCapabilityProperties(parentOcps); } Map<String, Set<String>> parentNodeOcps = parentOcps.get(nodeName); if (parentNodeOcps == null) { parentNodeOcps = Maps.newHashMap(); parentOcps.put(nodeName, parentNodeOcps); } Set<String> parentCapabilityOps = parentNodeOcps.get(embededCapabilityName); if (parentCapabilityOps == null) { parentCapabilityOps = Sets.newHashSet(); parentNodeOcps.put(embededCapabilityName, parentCapabilityOps); } parentCapabilityOps.add(proxyAttributeName); return; } } } } } } /** * Deeply explore this topology to detect if some type must be substituted by the corresponding topology template content and feed the {@link Deque}. <br> * BTW, rename the nodes by prefixing all the node names. */ private void recursivelyBuildSubstitutionStack(Topology topology, Deque<CompositionCouple> stack, String prefix) { if (topology == null || topology.getNodeTemplates() == null || topology.getNodeTemplates().isEmpty()) { return; } for (Entry<String, NodeTemplate> nodeEntry : topology.getNodeTemplates().entrySet()) { String nodeName = nodeEntry.getKey(); String type = nodeEntry.getValue().getType(); NodeType nodeType = csarRepoSearchService.getRequiredElementInDependencies(NodeType.class, type, topology.getDependencies()); if (nodeType.getSubstitutionTopologyId() != null) { // this node type is a proxy for a topology template Topology child = topologyServiceCore.getOrFail(nodeType.getSubstitutionTopologyId()); CompositionCouple couple = new CompositionCouple(topology, child, nodeName, nodeName + "_"); renameNodes(couple); stack.offer(couple); recursivelyBuildSubstitutionStack(child, stack, nodeName + "_"); } } } private void renameNodes(CompositionCouple compositionCouple) { Topology topology = compositionCouple.child; String[] nodeNames = new String[topology.getNodeTemplates().size()]; nodeNames = topology.getNodeTemplates().keySet().toArray(nodeNames); for (String nodeName : nodeNames) { String newName = ensureNodeNameIsUnique(topology.getNodeTemplates().keySet(), compositionCouple.nodeNamePrefix + nodeName, 0); renameNodeTemplate(topology, nodeName, newName); } } private String ensureNodeNameIsUnique(Set<String> keys, String prefix, int suffixeNumber) { String name = (suffixeNumber > 0) ? prefix + suffixeNumber : prefix; if (keys.contains(name)) { return ensureNodeNameIsUnique(keys, prefix, ++suffixeNumber); } else { return name; } } private void renameNodeTemplate(Topology topology, String oldName, String newName) { // if the prefixed name is already used by another node ? // quite improbable but ... if (topology.getNodeTemplates().containsKey(newName)) { throw new AlreadyExistException(String.format("A node with name '%s' already exists in this topology", newName)); } NodeTemplate nodeTemplate = topology.getNodeTemplates().remove(oldName); // manage relationships that target this node for (NodeTemplate otherNodes : topology.getNodeTemplates().values()) { if (otherNodes.getRelationships() == null || otherNodes.getRelationships().isEmpty()) { continue; } for (RelationshipTemplate relationshipTemplate : otherNodes.getRelationships().values()) { if (relationshipTemplate.getTarget().equals(oldName)) { relationshipTemplate.setTarget(newName); } } } // all output stuffs MapUtil.replaceKey(topology.getOutputProperties(), oldName, newName); MapUtil.replaceKey(topology.getOutputCapabilityProperties(), oldName, newName); MapUtil.replaceKey(topology.getOutputAttributes(), oldName, newName); // group members must be updated if (topology.getGroups() != null) { for (NodeGroup nodeGroup : topology.getGroups().values()) { Set<String> members = nodeGroup.getMembers(); if (members != null && members.remove(oldName)) { members.add(newName); } } } // substitutions if (topology.getSubstitutionMapping() != null) { renameNodeTemplateInSubstitutionTargets(topology.getSubstitutionMapping().getCapabilities(), oldName, newName); renameNodeTemplateInSubstitutionTargets(topology.getSubstitutionMapping().getRequirements(), oldName, newName); } // finally the node itself topology.getNodeTemplates().put(newName, nodeTemplate); } private void renameNodeTemplateInSubstitutionTargets(Map<String, SubstitutionTarget> substitutionTargets, String oldName, String newName) { if (substitutionTargets != null) { for (SubstitutionTarget s : substitutionTargets.values()) { if (s.getNodeTemplateName().equals(oldName)) { s.setNodeTemplateName(newName); } } } } /** * Deeply explore composition in order to detect cyclic reference: if a descendant references the mainTopologyId. */ public void recursivelyDetectTopologyCompositionCyclicReference(String mainTopologyId, String substitutionTopologyId) { Topology child = topologyServiceCore.getOrFail(substitutionTopologyId); if (child == null || child.getNodeTemplates() == null || child.getNodeTemplates().isEmpty()) { return; } for (Entry<String, NodeTemplate> nodeEntry : child.getNodeTemplates().entrySet()) { String type = nodeEntry.getValue().getType(); NodeType nodeType = csarRepoSearchService.getElementInDependencies(NodeType.class, type, child.getDependencies()); if (nodeType.getSubstitutionTopologyId() != null) { if (nodeType.getSubstitutionTopologyId().equals(mainTopologyId)) { throw new CyclicReferenceException("Cyclic reference : a topology template can not reference itself (even indirectly)"); } recursivelyDetectTopologyCompositionCyclicReference(mainTopologyId, nodeType.getSubstitutionTopologyId()); } } } private static class CompositionCouple { /** The topology that embeds another one. */ private final Topology parent; /** The topology template that will substitute the type. */ private final Topology child; /** The node name referencing the child in the parent. */ private final String nodeName; /** The prefix that will be used to rename nodes. */ private final String nodeNamePrefix; public CompositionCouple(Topology parent, Topology child, String nodeName, String nodeNamePrefix) { super(); this.parent = parent; this.child = child; this.nodeName = nodeName; this.nodeNamePrefix = nodeNamePrefix; } } }