package org.alien4cloud.tosca.editor.services;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import javax.inject.Inject;
import org.alien4cloud.tosca.catalog.index.CsarService;
import org.alien4cloud.tosca.editor.EditionContextManager;
import org.alien4cloud.tosca.editor.EditorService;
import org.alien4cloud.tosca.editor.operations.AbstractEditorOperation;
import org.alien4cloud.tosca.editor.operations.RecoverTopologyOperation;
import org.alien4cloud.tosca.editor.operations.nodetemplate.DeleteNodeOperation;
import org.alien4cloud.tosca.editor.operations.nodetemplate.RebuildNodeOperation;
import org.alien4cloud.tosca.editor.operations.relationshiptemplate.DeleteRelationshipOperation;
import org.alien4cloud.tosca.editor.operations.relationshiptemplate.RebuildRelationshipOperation;
import org.alien4cloud.tosca.model.CSARDependency;
import org.alien4cloud.tosca.model.Csar;
import org.alien4cloud.tosca.model.definitions.CapabilityDefinition;
import org.alien4cloud.tosca.model.definitions.RequirementDefinition;
import org.alien4cloud.tosca.model.templates.NodeTemplate;
import org.alien4cloud.tosca.model.templates.RelationshipTemplate;
import org.alien4cloud.tosca.model.templates.Topology;
import org.alien4cloud.tosca.model.types.AbstractToscaType;
import org.alien4cloud.tosca.model.types.NodeType;
import org.alien4cloud.tosca.model.types.RelationshipType;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import alien4cloud.exception.NotFoundException;
import alien4cloud.tosca.context.ToscaContext;
import alien4cloud.tosca.context.ToscaContextual;
import alien4cloud.utils.AlienUtils;
import lombok.extern.slf4j.Slf4j;
/**
* Helper service for editor context that allows to get recovery operations based on a given topology.
*/
@Slf4j
@Service
public class EditorTopologyRecoveryHelperService {
@Inject
private CsarService csarService;
@Inject
private EditorService editorService;
/**
* analyse a given topology and build a {@link RecoverTopologyOperation} to apply to the topology to make it synch
* with the dependencies present in the repository
*
* @param topology
* @return a {@link RecoverTopologyOperation}
*/
@ToscaContextual(requiresNew = true)
public RecoverTopologyOperation buildRecoveryOperation(Topology topology) {
RecoverTopologyOperation operation = new RecoverTopologyOperation();
buildRecoveryOperation(topology, operation);
return CollectionUtils.isNotEmpty(operation.getUpdatedDependencies()) ? operation : null;
}
/**
* analyse a given topology and fill in a {@link RecoverTopologyOperation} to apply to the topology to make it synch
* with the dependencies present in the repository
*
* @param topology
* @param operation
*/
@ToscaContextual(requiresNew = true)
public void buildRecoveryOperation(Topology topology, RecoverTopologyOperation operation) {
Set<CSARDependency> updatedDependencies = getUpdatedDependencies(topology);
operation.setUpdatedDependencies(updatedDependencies);
operation.setRecoveringOperations(buildRecoveryOperations(topology, updatedDependencies));
if (EditionContextManager.get().getOperations() != null && EditionContextManager.get().getLastOperationIndex() > -1) {
operation.setLastOperationId(EditionContextManager.get().getOperations().get(EditionContextManager.get().getLastOperationIndex()).getId());
} else {
operation.setLastOperationId("null");
}
}
/**
* Given a set of dependencies, analyse a given topology and build a list of {@link AbstractEditorOperation} to apply to the topology to make it synch
* with the dependencies present in the repository
*
* @param topology The topology we want to recover
* @param updatedDependencies The updated dependencies within the topology
* @return a list of {@link AbstractEditorOperation} representing the operations to perform on the topology for recovery
*/
public List<AbstractEditorOperation> buildRecoveryOperations(Topology topology, Set<CSARDependency> updatedDependencies) {
List<AbstractEditorOperation> recoveryOperations = Lists.newArrayList();
if (!topology.isEmpty()) {
for (CSARDependency updatedDependency : AlienUtils.safe(updatedDependencies)) {
buildNodesRecoveryOperations(topology, updatedDependency, recoveryOperations);
buildRelationshipsRecoveryOperations(topology, updatedDependency, recoveryOperations);
}
}
return recoveryOperations;
}
/**
*
* Analyse the relationships and build recovery operations
*
* @param topology The topology to recover
* @param updatedDependency The updated dependencies within the topology
* @param recoveryOperations The list into which to add the recovery operations
*/
private void buildRelationshipsRecoveryOperations(Topology topology, CSARDependency updatedDependency, List<AbstractEditorOperation> recoveryOperations) {
Set<String> deletedNodes = getDeletedNodes(recoveryOperations);
for (NodeTemplate nodeTemplate : topology.getNodeTemplates().values()) {
// skip, because the relationships will be deleted with the node
if (deletedNodes.contains(nodeTemplate.getName())) {
continue;
}
for (RelationshipTemplate relationshipTemplate : AlienUtils.safe(nodeTemplate.getRelationships()).values()) {
// skip, because the relationships will be deleted with the target node
if (deletedNodes.contains(relationshipTemplate.getTarget())) {
continue;
}
RelationshipType relationshipType = ToscaContext.get(RelationshipType.class, relationshipTemplate.getType());
// this means the type has been deleted. then we should delete the template from the topology
if (relationshipType == null) {
addDeleteRelationshipOperation(nodeTemplate, relationshipTemplate, recoveryOperations);
continue;
}
// check if the type is from the current archive. if so then we should rebuild the relationship template
if (isFrom(relationshipType, updatedDependency)) {
try {
// check if the relationship is still valid, ie the related requirement and capability still exist within the related types
validateRelationShip(nodeTemplate, relationshipTemplate, topology);
// FIXME instead of systematically rebuilding the relationship, maybe check what really changed in the type before rebuilding it?
addRebuildRelationshipOperation(nodeTemplate, relationshipTemplate, recoveryOperations);
} catch (Exception e) {
// remove the relationship if an error occurs
if (log.isDebugEnabled()) {
log.debug("Error when trying to recover a relationship", e);
}
addDeleteRelationshipOperation(nodeTemplate, relationshipTemplate, recoveryOperations);
}
}
}
}
}
/**
* checks if a relationship is still valid
*
* @param nodeTemplate The source node of the relationship
* @param relationshipTemplate The relationship to validate
* @param topology The related topology
*/
private void validateRelationShip(NodeTemplate nodeTemplate, RelationshipTemplate relationshipTemplate, Topology topology) {
// validate that the node itself still has the requirement
checkRequirement(relationshipTemplate.getRequirementName(), nodeTemplate);
// validate the targeted capability
checkCapability(relationshipTemplate.getTargetedCapabilityName(), topology.getNodeTemplates().get(relationshipTemplate.getTarget()));
}
/**
* validate that a node still has the requirement, by checking directly from the related node type
*
* @param requirementName The name of the requirement to check
* @param nodeTemplate The node template in which to check for the requirement
*/
private void checkRequirement(String requirementName, NodeTemplate nodeTemplate) {
// This call should never throw a NotFoundException
NodeType indexedNodeType = ToscaContext.getOrFail(NodeType.class, nodeTemplate.getType());
Map<String, RequirementDefinition> requirementMap = AlienUtils.fromListToMap(indexedNodeType.getRequirements(), "id", true);
if (!AlienUtils.safe(requirementMap).containsKey(requirementName)) {
throw new NotFoundException("A requirement with name [" + requirementName + "] cannot be found in the node [" + nodeTemplate.getName() + "].");
}
}
/**
* validate that a node still has a capability, by checkibg directly from the related node type
*
* @param capabilityName The name of the capability to check
* @param nodeTemplate The node template in which to check for the capability
*/
private void checkCapability(String capabilityName, NodeTemplate nodeTemplate) {
// This call should never throw a NotFoundException
NodeType indexedNodeType = ToscaContext.getOrFail(NodeType.class, nodeTemplate.getType());
Map<String, CapabilityDefinition> capabilitiesMap = AlienUtils.fromListToMap(indexedNodeType.getCapabilities(), "id", true);
if (!AlienUtils.safe(capabilitiesMap).containsKey(capabilityName)) {
throw new NotFoundException("A capability with name [" + capabilityName + "] cannot be found in the node [" + nodeTemplate.getName() + "].");
}
}
/**
* Analyse the node templates and build recovery operations
*
* @param topology The topology to recover
* @param updatedDependency The updated dependencies within the topology
* @param recoveryOperations The list into which to add the recovery operations
*/
private void buildNodesRecoveryOperations(Topology topology, CSARDependency updatedDependency, List<AbstractEditorOperation> recoveryOperations) {
for (NodeTemplate template : topology.getNodeTemplates().values()) {
NodeType nodeType = ToscaContext.get(NodeType.class, template.getType());
// this means the type has been deleted. then we should delete the template from the topology
if (nodeType == null) {
addDeleteNodeOperation(template, recoveryOperations);
continue;
}
// check if the type is from the current archive. if so then we should rebuild the nodeTemplate
if (isFrom(nodeType, updatedDependency)) {
// FIXME instead of systematically rebuilding the node, maybe check what really changed in the nodeType before rebuilding it?
addRebuildNodeOperation(template, recoveryOperations);
}
}
}
private void addRebuildNodeOperation(NodeTemplate template, List<AbstractEditorOperation> recoveryOperations) {
RebuildNodeOperation operation = new RebuildNodeOperation();
operation.setNodeName(template.getName());
recoveryOperations.add(operation);
}
private void addDeleteNodeOperation(NodeTemplate template, List<AbstractEditorOperation> recoveryOperations) {
DeleteNodeOperation operation = new DeleteNodeOperation();
operation.setNodeName(template.getName());
recoveryOperations.add(operation);
}
private void addRebuildRelationshipOperation(NodeTemplate nodeTemplate, RelationshipTemplate relationshipTemplate,
List<AbstractEditorOperation> recoveryOperations) {
RebuildRelationshipOperation operation = new RebuildRelationshipOperation();
operation.setNodeName(nodeTemplate.getName());
operation.setRelationshipName(relationshipTemplate.getName());
recoveryOperations.add(operation);
}
private void addDeleteRelationshipOperation(NodeTemplate nodeTemplate, RelationshipTemplate relationshipTemplate,
List<AbstractEditorOperation> recoveryOperations) {
DeleteRelationshipOperation operation = new DeleteRelationshipOperation();
operation.setNodeName(nodeTemplate.getName());
operation.setRelationshipName(relationshipTemplate.getName());
recoveryOperations.add(operation);
}
/**
* Return a Set of updated {@link CSARDependency} that have changed since last added in a given topology
*
* @param topology
* @return
*/
public Set<CSARDependency> getUpdatedDependencies(Topology topology) {
Set<CSARDependency> dependencies = topology.getDependencies();
Set<CSARDependency> updatedDependencies = Sets.newHashSet();
for (CSARDependency csarDependency : dependencies) {
CSARDependency updatedDependency = getUpdatedDependencyIfNeeded(csarDependency);
if (updatedDependency != null) {
updatedDependencies.add(updatedDependency);
}
}
return updatedDependencies;
}
/**
* Update a dependency according to what is currently in the repository
*
* @param initialDependency
* @return
*/
private CSARDependency getUpdatedDependencyIfNeeded(CSARDependency initialDependency) {
CSARDependency updatedDependency = null;
Csar csar = csarService.getOrFail(initialDependency.getName(), initialDependency.getVersion());
if ((StringUtils.isNotBlank(initialDependency.getHash()) || StringUtils.isNotBlank(csar.getHash()))
&& !Objects.equals(initialDependency.getHash(), csar.getHash())) {
updatedDependency = new CSARDependency(csar.getName(), csar.getVersion(), csar.getHash());
}
return updatedDependency;
}
private boolean isFrom(AbstractToscaType element, CSARDependency dependency) {
return Objects.equals(element.getArchiveName(), dependency.getName()) /* && Objects.equals(element.getArchiveVersion(), dependency.getVersion()) */;
}
/**
* Get the names of the nodes to be deleted according to the recovery operations
*
* @param recoveryOperations
* @return A set containing the names of the to-be-deleted nodes
*/
private Set<String> getDeletedNodes(List<AbstractEditorOperation> recoveryOperations) {
Set<String> names = Sets.newHashSet();
for (AbstractEditorOperation operation : AlienUtils.safe(recoveryOperations)) {
if (operation instanceof DeleteNodeOperation) {
names.add(((DeleteNodeOperation) operation).getNodeName());
}
}
return names;
}
@ToscaContextual(requiresNew = true)
public void processRecoveryOperations(Topology topology, Collection<AbstractEditorOperation> operations) {
for (AbstractEditorOperation recoveryOperation : AlienUtils.safe(operations)) {
editorService.process(recoveryOperation);
}
}
}