/******************************************************************************* * 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 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.model.project.NewProjectConfig; import org.eclipse.che.api.core.model.project.ProjectConfig; import org.eclipse.che.api.core.notification.EventService; import org.eclipse.che.api.project.server.handlers.ProjectHandlerRegistry; import org.eclipse.che.api.project.server.handlers.ProjectInitHandler; import org.eclipse.che.api.project.server.type.BaseProjectType; import org.eclipse.che.api.project.server.type.ProjectTypeRegistry; 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.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.annotation.PostConstruct; import javax.inject.Inject; import javax.inject.Singleton; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; import java.util.stream.Collectors; /** * Stores internal representation of Projects registered in the Workspace Agent. * * @author gazarenkov */ @Singleton public class ProjectRegistry { private static final Logger LOG = LoggerFactory.getLogger(ProjectRegistry.class); private final Map<String, RegisteredProject> projects; private final WorkspaceProjectsSyncer workspaceHolder; private final VirtualFileSystem vfs; private final ProjectTypeRegistry projectTypeRegistry; private final ProjectHandlerRegistry handlers; private final FolderEntry root; private final EventService eventService; private boolean initialized; @Inject public ProjectRegistry(WorkspaceProjectsSyncer workspaceHolder, VirtualFileSystemProvider vfsProvider, ProjectTypeRegistry projectTypeRegistry, ProjectHandlerRegistry handlers, EventService eventService) throws ServerException { this.eventService = eventService; this.projects = new ConcurrentHashMap<>(); this.workspaceHolder = workspaceHolder; this.vfs = vfsProvider.getVirtualFileSystem(); this.projectTypeRegistry = projectTypeRegistry; this.handlers = handlers; this.root = new FolderEntry(vfs.getRoot()); } @PostConstruct public void initProjects() throws ConflictException, NotFoundException, ServerException, ForbiddenException { List<? extends ProjectConfig> projectConfigs = workspaceHolder.getProjects(); // take all the projects from ws's config for (ProjectConfig projectConfig : projectConfigs) { final String path = projectConfig.getPath(); final VirtualFile vf = vfs.getRoot().getChild(Path.of(path)); final FolderEntry projectFolder = ((vf == null) ? null : new FolderEntry(vf, this)); putProject(projectConfig, projectFolder, false, false); } initUnconfiguredFolders(); initialized = true; for (RegisteredProject project : projects.values()) { // only for projects with sources if(project.getBaseFolder() != null) { fireInitHandlers(project); } } } /** * @return all the registered projects */ public List<RegisteredProject> getProjects() { checkInitializationState(); initUnconfiguredFolders(); return new ArrayList<>(projects.values()); } /** * @param projectPath * project path * @return project or null if not found */ public RegisteredProject getProject(String projectPath) { checkInitializationState(); initUnconfiguredFolders(); return projects.get(absolutizePath(projectPath)); } /** * @param parentPath * parent path * @return list projects of pojects */ public List<String> getProjects(String parentPath) { checkInitializationState(); initUnconfiguredFolders(); final Path root = Path.of(absolutizePath(parentPath)); return projects.keySet() .stream() .filter(key -> Path.of(key).isChild(root)) .collect(Collectors.toList()); } /** * @param path * - path of child project * @return the project owned this path. */ public RegisteredProject getParentProject(String path) { checkInitializationState(); // return this if a project if (getProject(path) != null) { return getProject(path); } // otherwise try to find matched parent Path test; while ((test = Path.of(path).getParent()) != null) { final RegisteredProject project = projects.get(test.toString()); if (project != null) { return project; } path = test.toString(); } return null; } /** * Creates RegisteredProject and caches it. * * @param config * project config * @param folder * base folder of project * @param updated * whether this configuration was updated * @param detected * whether this is automatically detected or explicitly defined project * @return project * @throws ServerException * when path for project is undefined */ RegisteredProject putProject(ProjectConfig config, FolderEntry folder, boolean updated, boolean detected) throws ServerException { final RegisteredProject project = new RegisteredProject(folder, config, updated, detected, this.projectTypeRegistry); projects.put(project.getPath(), project); return project; } /** * Removes all projects on and under the incoming path. * * @param path * from where to remove * @throws ServerException */ void removeProjects(String path) throws ServerException { List<RegisteredProject> removed = new ArrayList<>(); Optional.ofNullable(projects.remove(path)).ifPresent(removed::add); getProjects(path).forEach(p -> Optional.ofNullable(projects.remove(p)) .ifPresent(removed::add)); removed.forEach(registeredProject -> eventService.publish(new ProjectDeletedEvent(registeredProject.getPath()))); } /* ------------------------------------------ */ /* to use from extension */ /* ------------------------------------------ */ /** * Extension writer should call this method to apply changes which (supposedly) change * Attributes defined with particular Project Type * If incoming Project Type is primary and: * - If the folder located on projectPath is a Project, its Primary PT will be converted to incoming PT * - If the folder located on projectPath is NOT a Project the folder will be converted to "detected" Project with incoming Primary PT * If incoming Project Type is mixin and: * - If the folder located on projectPath is a Project, this PT will be added (if not already there) to its Mixin PTs * - If the folder located on projectPath is NOT a Project - ConflictException will be thrown * For example: * - extension code knows that particular file content is used by Value Provider * so this method should be called when content of this file changed to check * and update attributes. * OR * If Extension writer wants to force initializing folder to be Project * For example: * - extension code knows that particular folder inside should (or may) be treated * as sub-project of same as "parent" project type * * @param projectPath * absolute project path * @param type * type to be updated or added * @param asMixin * whether the type supposed to be mixin (true) or primary (false) * @return refreshed project * @throws ConflictException * @throws NotFoundException * @throws ServerException */ public RegisteredProject setProjectType(String projectPath, String type, boolean asMixin) throws ConflictException, NotFoundException, ServerException { final RegisteredProject project = getProject(projectPath); final NewProjectConfig conf; List<String> newMixins = new ArrayList<>(); if (project == null) { if (asMixin) { throw new ConflictException("Can not assign as mixin type '" + type + "' since the " + projectPath + " is not a project."); } else { final String path = absolutizePath(projectPath); final String name = Path.of(projectPath).getName(); conf = new NewProjectConfigImpl(path, type, newMixins, name, name, null, null, null); return putProject(conf, root.getChildFolder(path), true, true); } } else { newMixins = project.getMixins(); String newType = project.getType(); if (asMixin) { if (!newMixins.contains(type)) { newMixins.add(type); } } else { newType = type; } conf = new NewProjectConfigImpl(project.getPath(), newType, newMixins, project.getName(), project.getDescription(), project.getAttributes(), null, project.getSource()); return putProject(conf, project.getBaseFolder(), true, project.isDetected()); } } /** * Extension writer should call this method to apply changes which supposedly * make the Project no longer have particular Project Type. * In a case of removing primary project type: * - if the project was NOT detected BASE Project Type will be set as primary * - if the project was detected it will be converted back to the folder * For example: * - extension code knows that removing some file inside project's file system * will (or may) cause removing particular project type * * @param projectPath * project path * @param type * project type * @return refreshed project or null if such a project not found or was removed * @throws ConflictException * @throws ForbiddenException * @throws NotFoundException * @throws ServerException */ public RegisteredProject removeProjectType(String projectPath, String type) throws ConflictException, ForbiddenException, NotFoundException, ServerException { final RegisteredProject project = getProject(projectPath); if (project == null) { return null; } List<String> newMixins = project.getMixins(); String newType = project.getType(); if (newMixins.contains(type)) { newMixins.remove(type); } else if (newType.equals(type)) { if (project.isDetected()) { projects.remove(project.getPath()); return null; } newType = BaseProjectType.ID; } final NewProjectConfig conf = new NewProjectConfigImpl(project.getPath(), newType, newMixins, project.getName(), project.getDescription(), project.getAttributes(), null, project.getSource()); return putProject(conf, project.getBaseFolder(), true, project.isDetected()); } /** * @param path * a path * @return absolute (with lead slash) path */ static String absolutizePath(String path) { return (path.startsWith("/")) ? path : "/".concat(path); } /** Try to initialize projects from unconfigured folders on root. */ private void initUnconfiguredFolders() { try { for (FolderEntry folder : root.getChildFolders()) { if (!projects.containsKey(folder.getVirtualFile().getPath().toString())) { putProject(null, folder, true, false); } } } catch (ServerException e) { LOG.warn(e.getLocalizedMessage()); } } /** * Fires init handlers for all the project types of incoming project. * * @param project * the project * @throws ForbiddenException * @throws ConflictException * @throws NotFoundException * @throws ServerException */ void fireInitHandlers(RegisteredProject project) throws ForbiddenException, ConflictException, NotFoundException, ServerException { // primary type fireInit(project, project.getType()); // mixins for (String mixin : project.getMixins()) { fireInit(project, mixin); } } void fireInit(RegisteredProject project, String type) throws ForbiddenException, ConflictException, NotFoundException, ServerException { ProjectInitHandler projectInitHandler = handlers.getProjectInitHandler(type); if (projectInitHandler != null) { projectInitHandler.onProjectInitialized(this, project.getBaseFolder()); } } private void checkInitializationState() { if (!initialized) { throw new IllegalStateException("Projects are not initialized yet"); } } }