/*******************************************************************************
* Copyright (c) 2012-2017 Codenvy, S.A.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Codenvy, S.A. - initial API and implementation
*******************************************************************************/
package org.eclipse.che.api.project.server;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import org.eclipse.che.api.core.BadRequestException;
import org.eclipse.che.api.core.ConflictException;
import org.eclipse.che.api.core.ForbiddenException;
import org.eclipse.che.api.core.NotFoundException;
import org.eclipse.che.api.core.ServerException;
import org.eclipse.che.api.core.UnauthorizedException;
import org.eclipse.che.api.core.model.project.NewProjectConfig;
import org.eclipse.che.api.core.model.project.ProjectConfig;
import org.eclipse.che.api.core.model.project.SourceStorage;
import org.eclipse.che.api.core.model.project.type.ProjectType;
import org.eclipse.che.api.core.util.LineConsumerFactory;
import org.eclipse.che.api.project.server.RegisteredProject.Problem;
import org.eclipse.che.api.project.server.handlers.CreateProjectHandler;
import org.eclipse.che.api.project.server.handlers.ProjectHandlerRegistry;
import org.eclipse.che.api.project.server.importer.ProjectImporter;
import org.eclipse.che.api.project.server.importer.ProjectImporterRegistry;
import org.eclipse.che.api.project.server.type.AttributeValue;
import org.eclipse.che.api.project.server.type.BaseProjectType;
import org.eclipse.che.api.project.server.type.ProjectTypeDef;
import org.eclipse.che.api.project.server.type.ProjectTypeRegistry;
import org.eclipse.che.api.project.server.type.ProjectTypeResolution;
import org.eclipse.che.api.project.shared.dto.event.FileWatcherEventType;
import org.eclipse.che.api.vfs.Path;
import org.eclipse.che.api.vfs.VirtualFile;
import org.eclipse.che.api.vfs.VirtualFileSystem;
import org.eclipse.che.api.vfs.VirtualFileSystemProvider;
import org.eclipse.che.api.vfs.impl.file.FileTreeWatcher;
import org.eclipse.che.api.vfs.impl.file.FileWatcherNotificationHandler;
import org.eclipse.che.api.vfs.impl.file.FileWatcherNotificationListener;
import org.eclipse.che.api.vfs.search.Searcher;
import org.eclipse.che.api.vfs.search.SearcherProvider;
import org.eclipse.che.api.vfs.watcher.FileWatcherManager;
import org.eclipse.che.commons.lang.concurrent.LoggingUncaughtExceptionHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.annotation.PreDestroy;
import javax.inject.Inject;
import javax.inject.Singleton;
import java.io.IOException;
import java.nio.file.PathMatcher;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.stream.Collectors;
import static com.google.common.base.Strings.isNullOrEmpty;
import static java.lang.String.format;
import static org.eclipse.che.api.core.ErrorCodes.NOT_UPDATED_PROJECT;
/**
* Facade for all project related operations.
*
* @author gazarenkov
*/
@Singleton
public class ProjectManager {
private static final Logger LOG = LoggerFactory.getLogger(ProjectManager.class);
private final VirtualFileSystem vfs;
private final ProjectTypeRegistry projectTypeRegistry;
private final ProjectRegistry projectRegistry;
private final ProjectHandlerRegistry handlers;
private final ProjectImporterRegistry importers;
private final FileTreeWatcher fileWatcher;
private final FileWatcherNotificationHandler fileWatchNotifier;
private final ExecutorService executor;
private final WorkspaceProjectsSyncer workspaceProjectsHolder;
private final FileWatcherManager fileWatcherManager;
@Inject
public ProjectManager(VirtualFileSystemProvider vfsProvider,
ProjectTypeRegistry projectTypeRegistry,
ProjectRegistry projectRegistry,
ProjectHandlerRegistry handlers,
ProjectImporterRegistry importers,
FileWatcherNotificationHandler fileWatcherNotificationHandler,
FileTreeWatcher fileTreeWatcher,
WorkspaceProjectsSyncer workspaceProjectsHolder,
FileWatcherManager fileWatcherManager) throws ServerException {
this.vfs = vfsProvider.getVirtualFileSystem();
this.projectTypeRegistry = projectTypeRegistry;
this.projectRegistry = projectRegistry;
this.handlers = handlers;
this.importers = importers;
this.fileWatchNotifier = fileWatcherNotificationHandler;
this.fileWatcher = fileTreeWatcher;
this.workspaceProjectsHolder = workspaceProjectsHolder;
this.fileWatcherManager = fileWatcherManager;
executor = Executors.newFixedThreadPool(1 + Runtime.getRuntime().availableProcessors(),
new ThreadFactoryBuilder().setNameFormat("ProjectService-IndexingThread-")
.setUncaughtExceptionHandler(
LoggingUncaughtExceptionHandler.getInstance())
.setDaemon(true).build());
}
void initWatcher() throws IOException {
FileWatcherNotificationListener defaultListener =
new FileWatcherNotificationListener(file -> !(file.getPath().toString().contains(".che")
|| file.getPath().toString().contains(".#"))) {
@Override
public void onFileWatcherEvent(VirtualFile virtualFile, FileWatcherEventType eventType) {
LOG.debug("FS event detected: " + eventType + " " + virtualFile.getPath().toString() + " " + virtualFile.isFile());
}
};
fileWatchNotifier.addNotificationListener(defaultListener);
try {
fileWatcher.startup();
} catch (IOException e) {
LOG.error(e.getMessage(), e);
fileWatchNotifier.removeNotificationListener(defaultListener);
}
}
@PreDestroy
void stop() {
executor.shutdownNow();
}
public FolderEntry getProjectsRoot() throws ServerException {
return new FolderEntry(vfs.getRoot(), projectRegistry);
}
public Searcher getSearcher() throws NotFoundException, ServerException {
final SearcherProvider provider = vfs.getSearcherProvider();
if (provider == null) {
throw new NotFoundException("SearcherProvider is not defined in VFS");
}
return provider.getSearcher(vfs);
}
public void addWatchListener(FileWatcherNotificationListener listener) {
fileWatchNotifier.addNotificationListener(listener);
}
public void removeWatchListener(FileWatcherNotificationListener listener) {
fileWatchNotifier.removeNotificationListener(listener);
}
public void addWatchExcludeMatcher(PathMatcher matcher) {
fileWatcher.addExcludeMatcher(matcher);
}
public void removeWatchExcludeMatcher(PathMatcher matcher) {
fileWatcher.removeExcludeMatcher(matcher);
}
/**
* @return all the projects
*
* @throws ServerException
* if projects are not initialized yet
*/
public List<RegisteredProject> getProjects() throws ServerException {
return projectRegistry.getProjects();
}
/**
* @param projectPath
*
* @return project
*
* @throws ServerException
* if projects are not initialized yet
* @throws ServerException
* if project not found
*/
public RegisteredProject getProject(String projectPath) throws ServerException, NotFoundException {
final RegisteredProject project = projectRegistry.getProject(projectPath);
if (project == null) {
throw new NotFoundException(format("Project '%s' doesn't exist.", projectPath));
}
return project;
}
/**
* Create project:
* - take project config
*
* @param projectConfig
* project configuration
* @param options
* options for generator
*
* @return new project
*
* @throws ConflictException
* @throws ForbiddenException
* @throws ServerException
* @throws NotFoundException
*/
public RegisteredProject createProject(ProjectConfig projectConfig, Map<String, String> options) throws ConflictException,
ForbiddenException,
ServerException,
NotFoundException {
fileWatcherManager.suspend();
try {
// path and primary type is mandatory
if (projectConfig.getPath() == null) {
throw new ConflictException("Path for new project should be defined ");
}
if (projectConfig.getType() == null) {
throw new ConflictException("Project Type is not defined " + projectConfig.getPath());
}
final String path = ProjectRegistry.absolutizePath(projectConfig.getPath());
if (projectRegistry.getProject(path) != null) {
throw new ConflictException("Project config already exists for " + path);
}
return doCreateProject(projectConfig, options);
} finally {
fileWatcherManager.resume();
}
}
/** Note: Use {@link FileWatcherManager#suspend()} and {@link FileWatcherManager#resume()} while creating a project */
private RegisteredProject doCreateProject(ProjectConfig projectConfig, Map<String, String> options) throws ConflictException,
ForbiddenException,
ServerException,
NotFoundException {
final String path = ProjectRegistry.absolutizePath(projectConfig.getPath());
final CreateProjectHandler generator = handlers.getCreateProjectHandler(projectConfig.getType());
FolderEntry projectFolder;
if (generator != null) {
Map<String, AttributeValue> valueMap = new HashMap<>();
Map<String, List<String>> attributes = projectConfig.getAttributes();
if (attributes != null) {
for (Map.Entry<String, List<String>> entry : attributes.entrySet()) {
valueMap.put(entry.getKey(), new AttributeValue(entry.getValue()));
}
}
if (options == null) {
options = new HashMap<>();
}
Path projectPath = Path.of(path);
generator.onCreateProject(projectPath, valueMap, options);
projectFolder = new FolderEntry(vfs.getRoot().getChild(projectPath), projectRegistry);
} else {
projectFolder = new FolderEntry(vfs.getRoot().createFolder(path), projectRegistry);
}
final RegisteredProject project = projectRegistry.putProject(projectConfig, projectFolder, true, false);
workspaceProjectsHolder.sync(projectRegistry);
projectRegistry.fireInitHandlers(project);
return project;
}
/**
* Create batch of projects according to their configurations.
* <p/>
* Notes: - a project will be created by importing when project configuration contains {@link SourceStorage} object,
* otherwise this one will be created corresponding its {@link NewProjectConfig}:
* <li> - {@link NewProjectConfig} object contains only one mandatory {@link NewProjectConfig#setPath(String)} field.
* In this case Project will be created as project of {@link BaseProjectType} type </li>
* <li> - a project will be created as project of {@link BaseProjectType} type with {@link Problem#code} = 12
* when declared primary project type is not registered, </li>
* <li> - a project will be created with {@link Problem#code} = 12 and without mixin project type
* when declared mixin project type is not registered</li>
* <li> - for creating a project by generator {@link NewProjectConfig#getOptions()} should be specified.</li>
*
* @param projectConfigList
* the list of configurations to create projects
* @param rewrite
* whether rewrite or not (throw exception otherwise) if such a project exists
* @return the list of new projects
* @throws BadRequestException
* when {@link NewProjectConfig} object not contains mandatory {@link NewProjectConfig#setPath(String)} field.
* @throws ConflictException
* when the same path project exists and {@code rewrite} is {@code false}
* @throws ForbiddenException
* when trying to overwrite the project and this one contains at least one locked file
* @throws NotFoundException
* when parent folder does not exist
* @throws UnauthorizedException
* if user isn't authorized to access to location at importing source code
* @throws ServerException
* if other error occurs
*/
public List<RegisteredProject> createBatchProjects(List<? extends NewProjectConfig> projectConfigList, boolean rewrite, ProjectOutputLineConsumerFactory lineConsumerFactory)
throws BadRequestException, ConflictException, ForbiddenException, NotFoundException, ServerException, UnauthorizedException,
IOException {
fileWatcherManager.suspend();
try {
final List<RegisteredProject> projects = new ArrayList<>(projectConfigList.size());
validateProjectConfigurations(projectConfigList, rewrite);
final List<NewProjectConfig> sortedConfigList = projectConfigList
.stream()
.sorted((config1, config2) -> config1.getPath().compareTo(config2.getPath()))
.collect(Collectors.toList());
for (NewProjectConfig projectConfig : sortedConfigList) {
RegisteredProject registeredProject;
final String pathToProject = projectConfig.getPath();
//creating project(by config or by importing source code)
try {
final SourceStorage sourceStorage = projectConfig.getSource();
if (sourceStorage != null && !isNullOrEmpty(sourceStorage.getLocation())) {
doImportProject(pathToProject, sourceStorage, rewrite, lineConsumerFactory.setProjectName(projectConfig.getPath()));
} else if (!isVirtualFileExist(pathToProject)) {
registeredProject = doCreateProject(projectConfig, projectConfig.getOptions());
projects.add(registeredProject);
continue;
}
} catch (Exception e) {
if (!isVirtualFileExist(pathToProject)) {//project folder is absent
rollbackCreatingBatchProjects(projects);
throw e;
}
}
//update project
if (isVirtualFileExist(pathToProject)) {
try {
registeredProject = updateProject(projectConfig);
} catch (Exception e) {
registeredProject = projectRegistry.putProject(projectConfig, asFolder(pathToProject), true, false);
final Problem problem = new Problem(NOT_UPDATED_PROJECT,
"The project is not updated, caused by " + e.getLocalizedMessage());
registeredProject.getProblems().add(problem);
}
} else {
registeredProject = projectRegistry.putProject(projectConfig, null, true, false);
}
projects.add(registeredProject);
}
return projects;
} finally {
fileWatcherManager.resume();
}
}
private void rollbackCreatingBatchProjects(List<RegisteredProject> projects) {
for (RegisteredProject project : projects) {
try {
final FolderEntry projectFolder = project.getBaseFolder();
if (projectFolder != null) {
projectFolder.getVirtualFile().delete();
}
projectRegistry.removeProjects(project.getPath());
} catch (Exception e) {
LOG.warn(e.getLocalizedMessage());
}
}
}
private void validateProjectConfigurations(List<? extends NewProjectConfig> projectConfigList, boolean rewrite)
throws NotFoundException, ServerException, ConflictException, ForbiddenException, BadRequestException {
for (NewProjectConfig projectConfig : projectConfigList) {
final String pathToProject = projectConfig.getPath();
if (isNullOrEmpty(pathToProject)) {
throw new BadRequestException("Path for new project should be defined");
}
final String path = ProjectRegistry.absolutizePath(pathToProject);
final RegisteredProject registeredProject = projectRegistry.getProject(path);
if (registeredProject != null && rewrite) {
delete(path);
} else if (registeredProject != null) {
throw new ConflictException(format("Project config already exists for %s", path));
}
final String projectTypeId = projectConfig.getType();
if (isNullOrEmpty(projectTypeId)) {
projectConfig.setType(BaseProjectType.ID);
}
}
}
/**
* Updating project means:
* - getting the project (should exist)
* - updating name and description
* - changing project types and provided attributes
* - refreshing provided (transient) project types and attributes
*
* @param newConfig
* new config
*
* @return updated config
*
* @throws ForbiddenException
* @throws ServerException
* @throws NotFoundException
* @throws ConflictException
*/
public RegisteredProject updateProject(ProjectConfig newConfig) throws ForbiddenException,
ServerException,
NotFoundException,
ConflictException {
final String path = newConfig.getPath();
if (path == null) {
throw new ConflictException("Project path is not defined");
}
final FolderEntry baseFolder = asFolder(path);
if (baseFolder == null) {
throw new NotFoundException(format("Folder '%s' doesn't exist.", path));
}
final RegisteredProject project = projectRegistry.putProject(newConfig, baseFolder, true, false);
workspaceProjectsHolder.sync(projectRegistry);
projectRegistry.fireInitHandlers(project);
return project;
}
/**
*
* Import source code as a Basic type of Project
*
* @param path where to import
* @param sourceStorage where sources live
* @param rewrite whether rewrite or not (throw exception othervise) if such a project exists
*
* @return Project
*
* @throws ServerException
* @throws IOException
* @throws ForbiddenException
* @throws UnauthorizedException
* @throws ConflictException
* @throws NotFoundException
*/
public RegisteredProject importProject(String path, SourceStorage sourceStorage, boolean rewrite, LineConsumerFactory lineConsumerFactory) throws ServerException,
IOException,
ForbiddenException,
UnauthorizedException,
ConflictException,
NotFoundException {
fileWatcherManager.suspend();
try {
return doImportProject(path, sourceStorage, rewrite, lineConsumerFactory);
} finally {
fileWatcherManager.resume();
}
}
/** Note: Use {@link FileWatcherManager#suspend()} and {@link FileWatcherManager#resume()} while importing source code */
private RegisteredProject doImportProject(String path, SourceStorage sourceStorage, boolean rewrite, LineConsumerFactory lineConsumerFactory) throws ServerException,
IOException,
ForbiddenException,
UnauthorizedException,
ConflictException,
NotFoundException {
final ProjectImporter importer = importers.getImporter(sourceStorage.getType());
if (importer == null) {
throw new NotFoundException(format("Unable import sources project from '%s'. Sources type '%s' is not supported.",
sourceStorage.getLocation(), sourceStorage.getType()));
}
String normalizePath = (path.startsWith("/")) ? path : "/".concat(path);
FolderEntry folder = asFolder(normalizePath);
if (folder != null && !rewrite) {
throw new ConflictException(format("Project %s already exists ", path));
}
if (folder == null) {
folder = getProjectsRoot().createFolder(normalizePath);
}
try {
importer.importSources(folder, sourceStorage, lineConsumerFactory);
} catch (final Exception e) {
folder.remove();
throw e;
}
final String name = folder.getPath().getName();
for (ProjectConfig project : workspaceProjectsHolder.getProjects()) {
if (normalizePath.equals(project.getPath())) {
// TODO Needed for factory project importing with keepDir. It needs to find more appropriate solution
List<String> innerProjects = projectRegistry.getProjects(normalizePath);
for (String innerProject : innerProjects) {
RegisteredProject registeredProject = projectRegistry.getProject(innerProject);
projectRegistry.putProject(registeredProject, asFolder(registeredProject.getPath()), true, false);
}
RegisteredProject rp = projectRegistry.putProject(project, folder, true, false);
workspaceProjectsHolder.sync(projectRegistry);
return rp;
}
}
RegisteredProject rp = projectRegistry
.putProject(new NewProjectConfigImpl(normalizePath, name, BaseProjectType.ID, sourceStorage), folder, true, false);
workspaceProjectsHolder.sync(projectRegistry);
return rp;
}
/**
* Estimates if the folder can be treated as a project of particular type
*
* @param path to the folder
* @param projectTypeId project type to estimate
*
* @return resolution object
* @throws ServerException
* @throws NotFoundException
*/
public ProjectTypeResolution estimateProject(String path, String projectTypeId) throws ServerException,
NotFoundException {
final ProjectTypeDef projectType = projectTypeRegistry.getProjectType(projectTypeId);
if (projectType == null) {
throw new NotFoundException("Project Type to estimate needed.");
}
final FolderEntry baseFolder = asFolder(path);
if (baseFolder == null) {
throw new NotFoundException("Folder not found: " + path);
}
return projectType.resolveSources(baseFolder);
}
/**
* Estimates to which project types the folder can be converted to
*
* @param path to the folder
* @param transientOnly whether it can be estimated to the transient types of Project only
*
* @return list of resolutions
* @throws ServerException
* @throws NotFoundException
*/
public List<ProjectTypeResolution> resolveSources(String path, boolean transientOnly) throws ServerException, NotFoundException {
final List<ProjectTypeResolution> resolutions = new ArrayList<>();
for (ProjectType type : projectTypeRegistry.getProjectTypes(ProjectTypeRegistry.CHILD_TO_PARENT_COMPARATOR)) {
if (transientOnly && type.isPersisted()) {
continue;
}
final ProjectTypeResolution resolution = estimateProject(path, type.getId());
if (resolution.matched()) {
resolutions.add(resolution);
}
}
return resolutions;
}
/**
* deletes item including project
*
* @param path
*
* @throws ServerException
* @throws ForbiddenException
* @throws NotFoundException
* @throws ConflictException
*/
public void delete(String path) throws ServerException, ForbiddenException, NotFoundException, ConflictException {
final String apath = ProjectRegistry.absolutizePath(path);
// delete item
final VirtualFile item = vfs.getRoot().getChild(Path.of(apath));
if (item != null) {
item.delete();
}
// delete child projects
projectRegistry.removeProjects(apath);
workspaceProjectsHolder.sync(projectRegistry);
}
/**
* Copies item to new path with
*
* @param itemPath path to item to copy
* @param newParentPath path where the item should be copied to
* @param newName new item name
* @param overwrite whether existed (if any) item should be overwritten
*
* @return new item
* @throws ServerException
* @throws NotFoundException
* @throws ConflictException
* @throws ForbiddenException
*/
public VirtualFileEntry copyTo(String itemPath, String newParentPath, String newName, boolean overwrite) throws ServerException,
NotFoundException,
ConflictException,
ForbiddenException {
VirtualFile oldItem = vfs.getRoot().getChild(Path.of(itemPath));
if (oldItem == null) {
throw new NotFoundException("Item not found " + itemPath);
}
VirtualFile newParent = vfs.getRoot().getChild(Path.of(newParentPath));
if (newParent == null) {
throw new NotFoundException("New parent not found " + newParentPath);
}
final VirtualFile newItem = oldItem.copyTo(newParent, newName, overwrite);
final RegisteredProject owner = projectRegistry.getParentProject(newItem.getPath().toString());
if (owner == null) {
throw new NotFoundException("Parent project not found " + newItem.getPath().toString());
}
final VirtualFileEntry copy;
if (newItem.isFile()) {
copy = new FileEntry(newItem, projectRegistry);
} else {
copy = new FolderEntry(newItem, projectRegistry);
}
if (copy.isProject()) {
projectRegistry.getProject(copy.getProject()).getTypes();
// fire event
}
return copy;
}
/**
* Moves item to the new path
*
* @param itemPath path to the item
* @param newParentPath path of new parent
* @param newName new item's name
* @param overwrite whether existed (if any) item should be overwritten
*
* @return new item
* @throws ServerException
* @throws NotFoundException
* @throws ConflictException
* @throws ForbiddenException
*/
public VirtualFileEntry moveTo(String itemPath, String newParentPath, String newName, boolean overwrite) throws ServerException,
NotFoundException,
ConflictException,
ForbiddenException {
final VirtualFile oldItem = vfs.getRoot().getChild(Path.of(itemPath));
if (oldItem == null) {
throw new NotFoundException("Item not found " + itemPath);
}
final VirtualFile newParent;
if (newParentPath == null) {
// rename only
newParent = oldItem.getParent();
} else {
newParent = vfs.getRoot().getChild(Path.of(newParentPath));
}
if (newParent == null) {
throw new NotFoundException("New parent not found " + newParentPath);
}
// TODO lock token ?
final VirtualFile newItem = oldItem.moveTo(newParent, newName, overwrite, null);
final RegisteredProject owner = projectRegistry.getParentProject(newItem.getPath().toString());
if (owner == null) {
throw new NotFoundException("Parent project not found " + newItem.getPath().toString());
}
final VirtualFileEntry move;
if (newItem.isFile()) {
move = new FileEntry(newItem, projectRegistry);
} else {
move = new FolderEntry(newItem, projectRegistry);
}
if (move.isProject()) {
final RegisteredProject project = projectRegistry.getProject(itemPath);
NewProjectConfig projectConfig = new NewProjectConfigImpl(newItem.getPath().toString(),
project.getType(),
project.getMixins(),
newName,
project.getDescription(),
project.getAttributes(),
null,
project.getSource());
if (move instanceof FolderEntry) {
projectRegistry.removeProjects(project.getPath());
updateProject(projectConfig);
}
}
return move;
}
boolean isVirtualFileExist(String path) throws ServerException {
return asVirtualFileEntry(path) != null;
}
FolderEntry asFolder(String path) throws NotFoundException, ServerException {
final VirtualFileEntry entry = asVirtualFileEntry(path);
if (entry == null) {
return null;
}
if (!entry.isFolder()) {
throw new NotFoundException(format("Item '%s' isn't a folder. ", path));
}
return (FolderEntry)entry;
}
VirtualFileEntry asVirtualFileEntry(String path) throws ServerException {
final String apath = ProjectRegistry.absolutizePath(path);
final FolderEntry root = getProjectsRoot();
return root.getChild(apath);
}
FileEntry asFile(String path) throws NotFoundException, ServerException {
final VirtualFileEntry entry = asVirtualFileEntry(path);
if (entry == null) {
return null;
}
if (!entry.isFile()) {
throw new NotFoundException(format("Item '%s' isn't a file. ", path));
}
return (FileEntry)entry;
}
}