content: 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.TechnicalException;
  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;
                  }
  
                  try {
                      // check if the relationship is still valid
                      validateRelationShip(nodeTemplate, relationshipTemplate, topology);
                      // check if the type is from the current archive. if so then we should rebuild the relationship template
                      if (isFrom(relationshipType, updatedDependency)) {
                          // FIXME instead of systematically rebuilding the relationship, maybe check what really changed in the type before rebuilding it?
                          // FIXME Check the relationship is still valid concerning valid source/targets
                          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) {
          // This call should never throw a NotFoundException
          NodeType nodeType = ToscaContext.getOrFail(NodeType.class, nodeTemplate.getType());
          // validate the requirement
          checkRequirement(relationshipTemplate, nodeTemplate, nodeType);
          // validate the targeted capability
          checkCapability(relationshipTemplate.getTargetedCapabilityName(), topology.getNodeTemplates().get(relationshipTemplate.getTarget()));
      }
  
      /**
       * validate the requirement:<br>
       * <ul>
       * <li>Check that a node still has the requirement, by checking directly from the related node type</li>
       * <li>Check that the requirement type of the node hasn't changed</li>
       * </ul>
       *
       * @param relationshipTemplate The name of the requirement to check
       * @param nodeTemplate The node template in which to check for the requirement
       * @param nodeType
       * @throws InvalidStateException when something is wrong
       */
      private void checkRequirement(RelationshipTemplate relationshipTemplate, NodeTemplate nodeTemplate, NodeType nodeType) {
          Map<String, RequirementDefinition> requirementMap = AlienUtils.fromListToMap(nodeType.getRequirements(), "id", true);
          if (!AlienUtils.safe(requirementMap).containsKey(relationshipTemplate.getRequirementName())) {
              throw new InvalidStateException(
                      "A requirement with name [" + relationshipTemplate.getName() + "] cannot be found in the node [" + nodeTemplate.getName() + "].");
          }
  
          // check that the requirement type hasn't changed
          RequirementDefinition requirement = AlienUtils.safe(requirementMap).get(relationshipTemplate.getRequirementName());
          if (!Objects.equals(requirement.getType(), relationshipTemplate.getRequirementType())) {
              // the requirementType has changed. delete the relationship
              // TODO check if this requirement is compatible with the previous one, and mabey try to rebuild the relationship
              throw new InvalidStateException(
                      "The type of the requirement [" + relationshipTemplate.getRequirementName() + "], node [" + nodeTemplate.getName() + "] has changed");
          }
      }
  
      /**
       * 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) {
          // FIXME check if the relationship is still valid concerning binded capability
          // 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 InvalidStateException("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);
          }
      }
  
      private class InvalidStateException extends TechnicalException {
          public InvalidStateException(String message) {
              super(message);
          }
      }
  }
  