package org.alien4cloud.tosca.editor;
import static alien4cloud.utils.FileUtil.isZipFile;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import javax.annotation.PostConstruct;
import javax.inject.Inject;
import org.alien4cloud.tosca.catalog.index.CsarService;
import org.alien4cloud.tosca.editor.exception.EditionConcurrencyException;
import org.alien4cloud.tosca.editor.exception.EditorIOException;
import org.alien4cloud.tosca.editor.exception.RecoverTopologyException;
import org.alien4cloud.tosca.editor.operations.AbstractEditorOperation;
import org.alien4cloud.tosca.editor.operations.RecoverTopologyOperation;
import org.alien4cloud.tosca.editor.operations.ResetTopologyOperation;
import org.alien4cloud.tosca.editor.processors.IEditorCommitableProcessor;
import org.alien4cloud.tosca.editor.processors.IEditorOperationProcessor;
import org.alien4cloud.tosca.editor.services.EditorTopologyRecoveryHelperService;
import org.alien4cloud.tosca.editor.services.EditorTopologyUploadService;
import org.alien4cloud.tosca.editor.services.TopologySubstitutionService;
import org.alien4cloud.tosca.exporter.ArchiveExportService;
import org.alien4cloud.tosca.model.Csar;
import org.alien4cloud.tosca.model.templates.Topology;
import org.alien4cloud.tosca.topology.TopologyDTOBuilder;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.ApplicationContext;
import org.springframework.messaging.handler.annotation.DestinationVariable;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.stereotype.Service;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import alien4cloud.exception.NotFoundException;
import alien4cloud.git.SimpleGitHistoryEntry;
import alien4cloud.security.AuthorizationUtil;
import alien4cloud.topology.TopologyDTO;
import alien4cloud.topology.TopologyService;
import alien4cloud.topology.TopologyServiceCore;
import alien4cloud.topology.TopologyValidationResult;
import alien4cloud.topology.TopologyValidationService;
import alien4cloud.utils.CollectionUtils;
import alien4cloud.utils.FileUtil;
import alien4cloud.utils.ReflectionUtil;
/**
* This service manages command execution on the TOSCA topology template editor.
*/
@Service
public class EditorService {
@Inject
private ApplicationContext applicationContext;
@Inject
private ArchiveExportService exportService;
@Inject
private TopologyService topologyService;
@Inject
private TopologyServiceCore topologyServiceCore;
@Inject
private EditionContextManager editionContextManager;
@Inject
private TopologyDTOBuilder dtoBuilder;
@Inject
private EditorRepositoryService repositoryService;
@Inject
private EditorTopologyUploadService topologyUploadService;
@Inject
private EditorTopologyRecoveryHelperService recoveryHelperService;
@Inject
private TopologySubstitutionService topologySubstitutionServive;
@Inject
private TopologyValidationService topologyValidationService;
@Inject
private CsarService csarService;
@Value("${directories.alien}/${directories.upload_temp}")
private String tempUploadDir;
/** Processors map by type. */
private Map<Class<?>, IEditorOperationProcessor<? extends AbstractEditorOperation>> processorMap = Maps.newHashMap();
@PostConstruct
public void initialize() {
Map<String, IEditorOperationProcessor> processors = applicationContext.getBeansOfType(IEditorOperationProcessor.class);
for (IEditorOperationProcessor processor : processors.values()) {
Class<?> operationClass = ReflectionUtil.getGenericArgumentType(processor.getClass(), IEditorOperationProcessor.class, 0);
processorMap.put(operationClass, processor);
}
}
/**
* Check the authorization in the context of a topology edition.
*
* @param topologyId The id of the topology.
*/
public void checkAuthorization(String topologyId) {
try {
editionContextManager.init(topologyId);
topologyService.checkEditionAuthorizations(EditionContextManager.getTopology());
} finally {
editionContextManager.destroy();
}
}
/**
* Call this method only for checking optimistic locking and initializing edition context for method that don't process an operation (save, undo etc.)
*
* @param topologyId The id of the topology under edition.
* @param lastOperationId The id of the last operation.
*/
private void initContext(String topologyId, String lastOperationId) {
// create a fake operation for optimistic locking
AbstractEditorOperation optimisticLockOperation = new AbstractEditorOperation() {
@Override
public String commitMessage() {
return "This operation will never be enqueued and never commited.";
}
};
optimisticLockOperation.setPreviousOperationId(lastOperationId);
// init the edition context with the fake operation.
initContext(topologyId, optimisticLockOperation);
}
/**
* Initialize the edition context, (checks authorizations etc.)
*
* @param topologyId The id of the topology under edition.
* @param operation The operation to be processed.
*/
private void initContext(String topologyId, AbstractEditorOperation operation) {
editionContextManager.init(topologyId);
// check authorization to update a topology
topologyService.checkEditionAuthorizations(EditionContextManager.getTopology());
// If the version of the topology is not snapshot we don't allow modifications.
topologyService.throwsErrorIfReleased(EditionContextManager.getTopology());
// check that operations can be executed (based on a kind of optimistic locking
checkSynchronization(operation);
}
/**
* Ensure that the request is synchronized with the current state of the edition.
*
* @param operation, The operation under evaluation.
*/
private synchronized void checkSynchronization(AbstractEditorOperation operation) {
// there is an operation being processed so just fail (nobody could get the notification)
if (EditionContextManager.get().getCurrentOperation() != null) {
throw new EditionConcurrencyException();
}
List<AbstractEditorOperation> operations = EditionContextManager.get().getOperations();
// if someone performed some operations we have to ensure that the new operation is performed on top of a synchronized topology
if (EditionContextManager.get().getLastOperationIndex() == -1) {
if (operation.getPreviousOperationId() != null) {
throw new EditionConcurrencyException();
}
} else if (!operations.get(EditionContextManager.get().getLastOperationIndex()).getId().equals(operation.getPreviousOperationId())) {
throw new EditionConcurrencyException();
}
operation.setId(UUID.randomUUID().toString());
EditionContextManager.get().setCurrentOperation(operation);
return;
}
// trigger editor operation
@MessageMapping("/topology-editor/{topologyId}")
public <T extends AbstractEditorOperation> TopologyDTO execute(@DestinationVariable String topologyId, T operation) {
// get the topology context.
try {
initContext(topologyId, operation);
// check for topology potential recovery
checkTopologyRecovery();
doExecute(operation);
// return the topology context
return dtoBuilder.buildTopologyDTO(EditionContextManager.get());
} finally {
EditionContextManager.get().setCurrentOperation(null);
editionContextManager.destroy();
}
}
private <T extends AbstractEditorOperation> void doExecute(T operation) {
operation.setAuthor(AuthorizationUtil.getCurrentUser().getUserId());
// attach the topology tosca context and process the operation
process(operation);
List<AbstractEditorOperation> operations = EditionContextManager.get().getOperations();
if (EditionContextManager.get().getLastOperationIndex() != operations.size() - 1) {
// Clear the operations to 'redo'.
CollectionUtils.clearFrom(operations, EditionContextManager.get().getLastOperationIndex() + 1);
}
// update the last operation and index
EditionContextManager.get().getOperations().add(operation);
EditionContextManager.get().setLastOperationIndex(EditionContextManager.get().getOperations().size() - 1);
}
/**
* FIXME there is a cyclic dependency on beans here.
* Finds the proper processor and process an operation
*
* @param operation The operation to process
* @param <T> Type of the operation to process
*/
public <T extends AbstractEditorOperation> void process(T operation) {
IEditorOperationProcessor<T> processor = (IEditorOperationProcessor<T>) processorMap.get(operation.getClass());
processor.process(operation);
}
/**
* Undo or redo operations until the given index (including)
*
* @param topologyId The id of the topology for which to undo or redo operations.
* @param at The index on which to place the undo/redo cursor (-1 means no operations, then 0 is first operation etc.)
* @param lastOperationId The last known operation id for client optimistic locking.
* @return The topology DTO.
*/
public TopologyDTO undoRedo(String topologyId, int at, String lastOperationId) {
try {
initContext(topologyId, lastOperationId);
if (-1 > at || at > EditionContextManager.get().getOperations().size()) {
throw new NotFoundException("Unable to find the requested index for undo/redo");
}
checkTopologyRecovery();
if (at == EditionContextManager.get().getLastOperationIndex()) {
// nothing to change.
return dtoBuilder.buildTopologyDTO(EditionContextManager.get());
}
// TODO Improve this by avoiding dao query for (deep) cloning topology and keeping cache for TOSCA types that are required.
editionContextManager.reset();
for (int i = 0; i < at + 1; i++) {
AbstractEditorOperation operation = EditionContextManager.get().getOperations().get(i);
IEditorOperationProcessor processor = processorMap.get(operation.getClass());
processor.process(operation);
}
EditionContextManager.get().setLastOperationIndex(at);
return dtoBuilder.buildTopologyDTO(EditionContextManager.get());
} catch (IOException e) {
// FIXME undo should be fail-safe...
return null;
} finally {
EditionContextManager.get().setCurrentOperation(null);
editionContextManager.destroy();
}
}
/**
* Save a topology under edition. It updates the local repository files, the topology in elastic-search and perform a local git commit.
*
* @param topologyId The id of the topology under edition.
* @param lastOperationId The id of the last operation.
*/
public TopologyDTO save(String topologyId, String lastOperationId) {
try {
initContext(topologyId, lastOperationId);
doSave();
return dtoBuilder.buildTopologyDTO(EditionContextManager.get());
} catch (IOException e) {
// when there is a failure in file copy to the local repo.
// FIXME git revert to put back the local files state in the initial state.
throw new EditorIOException("Error while saving files state in local repository", e);
} finally {
EditionContextManager.get().setCurrentOperation(null);
editionContextManager.destroy();
}
}
private void doSave() throws IOException {
EditionContext context = EditionContextManager.get();
if (context.getLastOperationIndex() <= context.getLastSavedOperationIndex()) {
// nothing to save..
return;
}
StringBuilder commitMessage = new StringBuilder();
// copy and cleanup all temporary files from the executed operations.
for (int i = context.getLastSavedOperationIndex() + 1; i <= context.getLastOperationIndex(); i++) {
AbstractEditorOperation operation = context.getOperations().get(i);
IEditorOperationProcessor<?> processor = (IEditorOperationProcessor) processorMap.get(operation.getClass());
if (processor instanceof IEditorCommitableProcessor) {
((IEditorCommitableProcessor) processor).beforeCommit(operation);
}
commitMessage.append(operation.getAuthor()).append(": ").append(operation.commitMessage()).append("\n");
}
saveYamlAndZipFile();
Topology topology = EditionContextManager.getTopology();
// Save the topology in elastic search
topologyServiceCore.save(topology);
topologySubstitutionServive.updateSubstitutionType(topology, EditionContextManager.getCsar());
// Topology has changed means that dependencies might have changed, must update the dependencies
csarService.setDependencies(topology.getId(), topology.getDependencies());
// Local git commit
repositoryService.commit(EditionContextManager.get().getCsar(), commitMessage.toString());
// TODO add support for undo even after save, this require ability to rollback files to git state, we need file rollback support for that..
context.setOperations(Lists.newArrayList(context.getOperations().subList(context.getLastOperationIndex() + 1, context.getOperations().size())));
context.setLastOperationIndex(-1);
}
private void saveYamlAndZipFile() throws IOException {
// Update the yaml in the archive
Csar csar = EditionContextManager.getCsar();
Path targetPath = EditionContextManager.get().getLocalGitPath().resolve(csar.getYamlFilePath());
String yaml = exportService.getYaml(csar, EditionContextManager.getTopology());
try (BufferedWriter writer = Files.newBufferedWriter(targetPath)) {
writer.write(yaml);
}
// Update the archive zip for download
repositoryService.updateArchiveZip(EditionContextManager.getCsar().getName(), EditionContextManager.getCsar().getVersion());
}
/**
* Performs a git pull.
*/
public TopologyDTO pull(String topologyId, String username, String password, String remoteBranch) {
Path tempPath = null;
try {
editionContextManager.init(topologyId);
if (EditionContextManager.get().getLastSavedOperationIndex() == -1) {
repositoryService.clean(EditionContextManager.getCsar());
}
Path topologyPath = EditionContextManager.get().getLocalGitPath();
tempPath = Files.createTempDirectory(Paths.get(tempUploadDir), "");
repositoryService.pull(tempPath, EditionContextManager.getCsar(), username, password, remoteBranch);
topologyUploadService.processTopologyDir(tempPath, EditionContextManager.get().getTopology().getWorkspace());
try {
FileUtil.delete(topologyPath);
} catch (IOException e) {
// Ignored
}
FileUtil.copy(tempPath, topologyPath);
repositoryService.updateArchiveZip(EditionContextManager.getCsar().getName(), EditionContextManager.getCsar().getVersion());
// and finally save and commit
Topology topology = EditionContextManager.getTopology();
topologyServiceCore.save(topology);
// Topology has changed means that dependencies might have changed, must update the dependencies
csarService.setDependencies(topology.getId(), topology.getDependencies());
topologySubstitutionServive.updateSubstitutionType(topology, EditionContextManager.getCsar());
return dtoBuilder.buildTopologyDTO(EditionContextManager.get());
} catch (IOException e) {
throw new EditorIOException("Error while pulling remote branch into local repository for " + topologyId + " for user " + username, e);
} finally {
if (tempPath != null) {
try {
FileUtil.delete(tempPath);
} catch (IOException e) {
// Ignored
}
}
editionContextManager.destroy();
}
}
/**
* Push the content to a remote git repository.
* Note that conflicts are not managed in a4c. In case of conflicts a new branch is created for manual merge by users.
*/
public void push(String topologyId, String username, String password, String remoteBranch) {
try {
editionContextManager.init(topologyId);
repositoryService.push(EditionContextManager.getCsar(), username, password, remoteBranch);
} finally {
editionContextManager.destroy();
}
}
/**
* Configure the remote url of the git repository.
*
* @param remoteName The name for the repository.
* @param remoteUrl The url of the repository.
*/
public void setRemote(String topologyId, String remoteName, String remoteUrl) {
try {
editionContextManager.init(topologyId);
Csar csar = EditionContextManager.getCsar();
repositoryService.setRemote(csar, remoteName, remoteUrl);
} finally {
editionContextManager.destroy();
}
}
/**
* Retrieve the repository url of the git.
*
* @param topologyId the id of the topology.
* @param remoteName The name of the remote.
* @return The url corresponding to the remote name of the git repository of the topology.
*/
public String getRemoteUrl(String topologyId, String remoteName) {
try {
editionContextManager.init(topologyId);
Csar csar = EditionContextManager.getCsar();
return repositoryService.getRemoteUrl(csar, remoteName);
} finally {
editionContextManager.destroy();
}
}
/**
* Retrieve simplified vision of the git history for the given topology.
*
* @param topologyId The id of the topology.
* @param from from which index to get history.
* @param count number of histories entry to retrieve.
* @return a list of simplified git commit entry.
*/
public List<SimpleGitHistoryEntry> history(String topologyId, int from, int count) {
try { // No need to check current operation, we just want to get git history.
editionContextManager.init(topologyId);
// check authorization to update a topology
topologyService.checkEditionAuthorizations(EditionContextManager.getTopology());
return repositoryService.getHistory(EditionContextManager.getCsar(), from, count);
} finally {
editionContextManager.destroy();
}
}
/**
* Override the content of an archive from a full exising archive.
*
* @param topologyId The if of the topology to process.
* @param inputStream The input stream of the file that contains the archive.
*/
public void override(String topologyId, InputStream inputStream) throws IOException {
Path tempPath = null;
try {
// Initialize the editon context, null last operation id means that we just accept a context with no pending operations
initContext(topologyId, (String) null);
// first we need to copy the content to a temporary location, unzip and parse the archive
tempPath = Files.createTempFile(tempUploadDir, null, null);
Files.copy(inputStream, tempPath, StandardCopyOption.REPLACE_EXISTING);
// This throws an exception if not successful
topologyUploadService.processTopology(tempPath, EditionContextManager.get().getTopology().getWorkspace());
// meaning the topology is well imported in the editor context: override all the content of the git repository
// erase all content but .git directory
FileUtil.delete(EditionContextManager.get().getLocalGitPath(), EditionContextManager.get().getLocalGitPath().resolve(".git"));
// copy the archive content
if (isZipFile(tempPath)) {
// unzip the content
FileUtil.unzip(tempPath, EditionContextManager.get().getLocalGitPath());
} else {
// just copy the file
Path targetPath = EditionContextManager.get().getLocalGitPath().resolve(tempPath.getFileName());
Files.copy(tempPath, targetPath, StandardCopyOption.REPLACE_EXISTING);
}
// and finally save and commit
Topology topology = EditionContextManager.getTopology();
String commitMessage = AuthorizationUtil.getCurrentUser().getUserId() + ": Override all content of the topology archive from REST API.";
topologyServiceCore.save(topology);
// Topology has changed means that dependencies might have changed, must update the dependencies
csarService.setDependencies(topology.getId(), topology.getDependencies());
topologySubstitutionServive.updateSubstitutionType(topology, EditionContextManager.getCsar());
// Local git commit
repositoryService.commit(EditionContextManager.get().getCsar(), commitMessage);
} finally {
EditionContextManager.get().setCurrentOperation(null);
editionContextManager.destroy();
}
}
/**
* Checks if the topology needs to be recovered and eventually throws an error.
* The {@link RecoverTopologyOperation} is cache for later use in recovering process
*/
public void checkTopologyRecovery() {
Topology topology = EditionContextManager.getTopology();
EditionContext context = EditionContextManager.get();
context.setRecoveryOperation(recoveryHelperService.buildRecoveryOperation(topology));
if (context.getRecoveryOperation() != null) {
throw new RecoverTopologyException("The topology needs to be recovered.", context.getRecoveryOperation());
}
}
/**
* Execute an operation and directly trigger the save process
*
* @param topologyId The id of the topology.
* @param operation The operation to execute.
* @param <T>
* @return a {@link TopologyDTO}
*/
private <T extends AbstractEditorOperation> TopologyDTO executeAndSave(String topologyId, T operation) {
try {
// init the context.
initContext(topologyId, operation);
// execute the operation
doExecute(operation);
// save the context
doSave();
// return the topology DTO
return dtoBuilder.buildTopologyDTO(EditionContextManager.get());
} catch (IOException e) {
// when there is a failure in file copy to the local repo.
// FIXME git revert to put back the local files state in the initial state.
throw new EditorIOException("Error while saving files state in local repository", e);
} finally {
EditionContextManager.get().setCurrentOperation(null);
editionContextManager.destroy();
}
}
/**
* Recovers a topology
*
* @param topologyId The id of the topology.
* @param lastOperationId
* @return
*/
public TopologyDTO recover(String topologyId, String lastOperationId) {
// The recovering process is done via operation so that we can have it in the history
RecoverTopologyOperation operation = getRecoverTopologyOperation(topologyId);
operation.setPreviousOperationId(lastOperationId);
return executeAndSave(topologyId, operation);
}
private RecoverTopologyOperation getRecoverTopologyOperation(String topologyId) {
try {
editionContextManager.init(topologyId);
RecoverTopologyOperation operation = EditionContextManager.get().getRecoveryOperation();
EditionContextManager.get().setRecoveryOperation(null);
return operation != null ? operation : new RecoverTopologyOperation();
} finally {
editionContextManager.destroy();
}
}
/**
* Reset and save a topology
*
* @param topologyId The id of the topology.
* @param lastOperationId
* @return
*/
public TopologyDTO reset(String topologyId, String lastOperationId) {
// The resetting process is done via operation so that we can have it in the history
ResetTopologyOperation operation = new ResetTopologyOperation();
operation.setPreviousOperationId(lastOperationId);
return executeAndSave(topologyId, operation);
}
/**
* Validate if a topology is valid.
*
* @param topologyId The id of the topology.
* @return the validation result
*/
public TopologyValidationResult validateTopology(String topologyId) {
try {
editionContextManager.init(topologyId);
return topologyValidationService.validateTopology(EditionContextManager.getTopology());
} finally {
editionContextManager.destroy();
}
}
}