/*******************************************************************************
* Copyright (c) 2012-2015 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.notification.EventService;
import org.eclipse.che.api.core.notification.EventSubscriber;
import org.eclipse.che.api.project.server.handlers.CreateModuleHandler;
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.type.Attribute;
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.ProjectType;
import org.eclipse.che.api.project.server.type.ProjectTypeRegistry;
import org.eclipse.che.api.project.server.type.Variable;
import org.eclipse.che.api.project.shared.dto.SourceEstimation;
import org.eclipse.che.api.vfs.server.Path;
import org.eclipse.che.api.vfs.server.VirtualFileSystemRegistry;
import org.eclipse.che.api.vfs.server.observation.VirtualFileEvent;
import org.eclipse.che.commons.lang.Pair;
import org.eclipse.che.commons.lang.cache.Cache;
import org.eclipse.che.commons.lang.cache.SLRUCache;
import org.eclipse.che.dto.server.DtoFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import javax.inject.Inject;
import javax.inject.Singleton;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* @author andrew00x
*/
@Singleton
public final class DefaultProjectManager implements ProjectManager {
private static final Logger LOG = LoggerFactory.getLogger(DefaultProjectManager.class);
private static final int CACHE_NUM = 1 << 2;
private static final int CACHE_MASK = CACHE_NUM - 1;
private static final int SEG_SIZE = 32;
private final Lock[] miscLocks;
private final Cache<Pair<String, String>, ProjectMisc>[] miscCaches;
private final VirtualFileSystemRegistry fileSystemRegistry;
private final EventService eventService;
private final EventSubscriber<VirtualFileEvent> vfsSubscriber;
private final ProjectTypeRegistry projectTypeRegistry;
private final ProjectHandlerRegistry handlers;
@Inject
@SuppressWarnings("unchecked")
public DefaultProjectManager(VirtualFileSystemRegistry fileSystemRegistry,
EventService eventService,
ProjectTypeRegistry projectTypeRegistry,
ProjectHandlerRegistry handlers) {
this.fileSystemRegistry = fileSystemRegistry;
this.eventService = eventService;
this.projectTypeRegistry = projectTypeRegistry;
//this.handler = handler;
this.handlers = handlers;
this.miscCaches = new Cache[CACHE_NUM];
this.miscLocks = new Lock[CACHE_NUM];
for (int i = 0; i < CACHE_NUM; i++) {
miscLocks[i] = new ReentrantLock();
miscCaches[i] = new SLRUCache<Pair<String, String>, ProjectMisc>(SEG_SIZE, SEG_SIZE) {
@Override
protected void evict(Pair<String, String> key, ProjectMisc value) {
if (value.isUpdated()) {
final int index = key.hashCode() & CACHE_MASK;
miscLocks[index].lock();
try {
writeProjectMisc(value.getProject(), value);
} catch (Exception e) {
LOG.error(e.getMessage(), e);
} finally {
miscLocks[index].unlock();
}
super.evict(key, value);
}
}
};
}
vfsSubscriber = new EventSubscriber<VirtualFileEvent>() {
@Override
public void onEvent(VirtualFileEvent event) {
final String workspace = event.getWorkspaceId();
final String path = event.getPath();
if (path.endsWith(Constants.CODENVY_DIR + "/misc.xml")) {
return;
}
switch (event.getType()) {
case CONTENT_UPDATED:
case CREATED:
case DELETED:
case MOVED:
case RENAMED: {
final int length = path.length();
for (int i = 1; i < length && (i = path.indexOf('/', i)) > 0; i++) {
final String projectPath = path.substring(0, i);
try {
final Project project = getProject(workspace, projectPath);
if (project != null) {
getProjectMisc(project).setModificationDate(System.currentTimeMillis());
}
} catch (Exception e) {
LOG.error(e.getMessage(), e);
}
}
break;
}
}
}
};
}
/**
* Gets the list of projects in {@code workspace}.
*
* @param workspace
* id of workspace
* @return the list of projects in specified workspace.
* @throws ServerException
* if an error occurs
*/
public List<Project> getProjects(String workspace) throws ServerException {
final FolderEntry myRoot = getProjectsRoot(workspace);
final List<Project> projects = new ArrayList<>();
for (FolderEntry folder : myRoot.getChildFolders()) {
if (folder.isProjectFolder()) {
projects.add(new Project(folder, this));
}
}
return projects;
}
/**
* Gets single project by id of workspace and project's path in this workspace.
*
* @param workspace
* id of workspace
* @param projectPath
* project's path
* @return requested project or {@code null} if project was not found
* @throws ForbiddenException
* if user which perform operation doesn't have access to the requested project
* @throws ServerException
* if other error occurs
*/
public Project getProject(String workspace, String projectPath) throws ForbiddenException, ServerException {
final FolderEntry myRoot = getProjectsRoot(workspace);
final VirtualFileEntry child = myRoot.getChild(projectPath.startsWith("/") ? projectPath.substring(1) : projectPath);
if (child != null && child.isFolder() && ((FolderEntry)child).isProjectFolder()) {
return new Project((FolderEntry)child, this);
}
return null;
}
/**
* Creates new project.
*
* @param workspace
* id of workspace
* @param name
* project's name
* @param projectConfig
* project description
* @return newly created project
* @throws ConflictException
* if operation causes conflict, e.g. name conflict if project with specified name already exists
* @throws ForbiddenException
* if user which perform operation doesn't have required permissions
* @throws ServerException
* if other error occurs
*/
public Project createProject(String workspace,
String name,
ProjectConfig projectConfig,
Map<String, String> options,
String visibility) throws ConflictException, ForbiddenException, ServerException {
final FolderEntry myRoot = getProjectsRoot(workspace);
final FolderEntry projectFolder = myRoot.createFolder(name);
final Project project = new Project(projectFolder, this);
final CreateProjectHandler generator = handlers.getCreateProjectHandler(projectConfig.getTypeId());
if (generator != null) {
generator.onCreateProject(project.getBaseFolder(),
projectConfig.getAttributes(), options);
}
project.updateConfig(projectConfig);
final ProjectMisc misc = project.getMisc();
misc.setCreationDate(System.currentTimeMillis());
misc.save(); // Important to save misc!!
if (visibility != null) {
project.setVisibility(visibility);
}
return project;
}
/**
* Adds module to parent project. If module does not exist creates it before.
*
* @param workspace
* @param projectPath
* - parent project path
* @param modulePath
* - path for the module to add
* @param moduleConfig
* - module configuration (optional, needed only if module does not exist)
* @param options
* - options for module creation (optional, same as moduleConfig)
* @param visibility
* - visibility for the module (optional, same as moduleConfig)
* @return
* @throws ConflictException
* @throws ForbiddenException
* @throws ServerException
* @throws NotFoundException
*/
public Project addModule(String workspace, String projectPath, String modulePath, ProjectConfig moduleConfig, Map<String,
String> options, String visibility)
throws ConflictException, ForbiddenException, ServerException, NotFoundException {
Project parentProject = getProject(workspace, projectPath);
if (parentProject == null)
throw new NotFoundException("Parent Project not found " + projectPath);
if (!projectPath.startsWith("/")) {
projectPath = "/" + projectPath;
}
String absModulePath = modulePath.startsWith("/") ? modulePath : projectPath + "/" + modulePath;
VirtualFileEntry moduleFolder = getProjectsRoot(workspace).getChild(absModulePath);
if (moduleFolder != null && moduleFolder.isFile())
throw new ConflictException("Item exists on " + absModulePath + " but is not a folder or project");
Project module;
// there are no source folder for module
// create folder and make it project and update config
if (moduleFolder == null) {
if (moduleConfig == null)
throw new ConflictException("Module not found on " + absModulePath + " and module configuration is not defined");
String parentPath = Path.fromString(absModulePath).getParent().toString();
String name = Path.fromString(modulePath).getName();
final VirtualFileEntry parentFolder = getProjectsRoot(workspace).getChild(parentPath);
if (parentFolder == null || parentFolder.isFile())
throw new NotFoundException("Parent Folder not found " + parentPath);
// create folder for module
moduleFolder = ((FolderEntry)parentFolder).createFolder(name);
module = new Project((FolderEntry)moduleFolder, this);
final CreateProjectHandler generator = this.getHandlers().getCreateProjectHandler(moduleConfig.getTypeId());
if (generator != null) {
generator.onCreateProject(module.getBaseFolder(), moduleConfig.getAttributes(), options);
}
module.updateConfig(moduleConfig);
final ProjectMisc misc = module.getMisc();
misc.setCreationDate(System.currentTimeMillis());
misc.save(); // Important to save misc!!
if (visibility != null) {
module.setVisibility(visibility);
}
} else if (!((FolderEntry)moduleFolder).isProjectFolder()) {
// folder exists but is not a project, just update config
if (moduleConfig == null)
throw new ConflictException("Folder at " + absModulePath + " is not a project and module configuration is not defined");
module = new Project((FolderEntry)moduleFolder, this);
module.updateConfig(moduleConfig);
} else {
// project module exists
module = getProject(workspace, absModulePath);
}
// finally adds the module to parent
parentProject.getModules().add(modulePath);
CreateModuleHandler moduleHandler = this.getHandlers().getCreateModuleHandler(parentProject.getConfig().getTypeId());
if (moduleHandler != null) {
moduleHandler.onCreateModule(parentProject.getBaseFolder(), absModulePath, module.getConfig(), options);
}
return module;
}
/**
* Gets root folder of project tree.
*
* @param workspace
* id of workspace
* @return root folder
* @throws ServerException
* if an error occurs
*/
public FolderEntry getProjectsRoot(String workspace) throws ServerException {
return new FolderEntry(workspace, fileSystemRegistry.getProvider(workspace).getMountPoint(true).getRoot());
}
/**
* Gets ProjectMisc.
*
* @param project
* project
* @return ProjectMisc
* @throws ServerException
* if an error occurs
* @see ProjectMisc
*/
public ProjectMisc getProjectMisc(Project project) throws ServerException {
final String workspace = project.getWorkspace();
final String path = project.getPath();
final Pair<String, String> key = Pair.of(workspace, path);
final int index = key.hashCode() & CACHE_MASK;
miscLocks[index].lock();
try {
ProjectMisc misc = miscCaches[index].get(key);
if (misc == null) {
miscCaches[index].put(key, misc = readProjectMisc(project));
}
return misc;
} finally {
miscLocks[index].unlock();
}
}
private ProjectMisc readProjectMisc(Project project) throws ServerException {
try {
ProjectMisc misc;
final FileEntry miscFile = (FileEntry)project.getBaseFolder().getChild(Constants.CODENVY_DIR + "/misc.xml");
if (miscFile != null) {
try (InputStream in = miscFile.getInputStream()) {
final Properties properties = new Properties();
properties.loadFromXML(in);
misc = new ProjectMisc(properties, project);
} catch (IOException e) {
throw new ServerException(e.getMessage(), e);
}
} else {
misc = new ProjectMisc(project);
}
return misc;
} catch (ForbiddenException e) {
// If have access to the project then must have access to its meta-information. If don't have access then treat that as
// server error.
throw new ServerException(e.getServiceError());
}
}
/**
* Gets ProjectMisc.
*
* @param project
* project
* @param misc
* ProjectMisc
* @throws ServerException
* if an error occurs
* @see ProjectMisc
*/
public void saveProjectMisc(Project project, ProjectMisc misc) throws ServerException {
if (misc.isUpdated()) {
final String workspace = project.getWorkspace();
final String path = project.getPath();
final Pair<String, String> key = Pair.of(workspace, path);
final int index = key.hashCode() & CACHE_MASK;
miscLocks[index].lock();
try {
miscCaches[index].remove(key);
writeProjectMisc(project, misc);
miscCaches[index].put(key, misc);
} finally {
miscLocks[index].unlock();
}
}
}
private void writeProjectMisc(Project project, ProjectMisc misc) throws ServerException {
try {
final ByteArrayOutputStream bout = new ByteArrayOutputStream();
try {
misc.asProperties().storeToXML(bout, null);
} catch (IOException e) {
throw new ServerException(e.getMessage(), e);
}
FileEntry miscFile = (FileEntry)project.getBaseFolder().getChild(Constants.CODENVY_DIR + "/misc.xml");
if (miscFile != null) {
miscFile.updateContent(bout.toByteArray(), null);
} else {
FolderEntry codenvy = (FolderEntry)project.getBaseFolder().getChild(Constants.CODENVY_DIR);
if (codenvy == null) {
try {
codenvy = project.getBaseFolder().createFolder(Constants.CODENVY_DIR);
} catch (ConflictException e) {
// Already checked existence of folder ".codenvy".
throw new ServerException(e.getServiceError());
}
}
try {
codenvy.createFile("misc.xml", bout.toByteArray(), null);
} catch (ConflictException e) {
// Not expected, existence of file already checked
throw new ServerException(e.getServiceError());
}
}
LOG.debug("Save misc file of project {} in {}", project.getPath(), project.getWorkspace());
} catch (ForbiddenException e) {
// If have access to the project then must have access to its meta-information. If don't have access then treat that as
// server error.
throw new ServerException(e.getServiceError());
}
}
@PostConstruct
void start() {
eventService.subscribe(vfsSubscriber);
}
@PreDestroy
void stop() {
eventService.unsubscribe(vfsSubscriber);
for (int i = 0, length = miscLocks.length; i < length; i++) {
miscLocks[i].lock();
try {
miscCaches[i].clear();
} finally {
miscLocks[i].unlock();
}
}
}
public VirtualFileSystemRegistry getVirtualFileSystemRegistry() {
return fileSystemRegistry;
}
public ProjectTypeRegistry getProjectTypeRegistry() {
return this.projectTypeRegistry;
}
public ProjectHandlerRegistry getHandlers() {
return handlers;
}
public Map<String, AttributeValue> estimateProject(String workspace, String path, String projectTypeId)
throws ServerException, ForbiddenException, NotFoundException, ValueStorageException,
ProjectTypeConstraintException {
ProjectType projectType = projectTypeRegistry.getProjectType(projectTypeId);
if (projectType == null)
throw new NotFoundException("Project Type " + projectTypeId + " not found.");
final VirtualFileEntry baseFolder = getProjectsRoot(workspace).getChild(path.startsWith("/") ? path.substring(1) : path);
if (!baseFolder.isFolder()) {
throw new NotFoundException("Not a folder: " + path);
}
Map<String, AttributeValue> attributes = new HashMap<>();
for (Attribute attr : projectType.getAttributes()) {
if (attr.isVariable() && ((Variable)attr).getValueProviderFactory() != null) {
Variable var = (Variable)attr;
// getValue throws ValueStorageException if not valid
attributes.put(attr.getName(), var.getValue((FolderEntry)baseFolder));
}
}
return attributes;
}
// ProjectSuggestion
public List<SourceEstimation> resolveSources(String workspace, String path, boolean transientOnly)
throws ServerException, ForbiddenException, NotFoundException,
ProjectTypeConstraintException {
final List<SourceEstimation> estimations = new ArrayList<>();
for (ProjectType type : projectTypeRegistry.getProjectTypes(ProjectTypeRegistry.CHILD_TO_PARENT_COMPARATOR)) {
if (transientOnly && type.isPersisted())
continue;
final HashMap<String, List<String>> attributes = new HashMap<>();
try {
for (Map.Entry<String, AttributeValue> attr : estimateProject(workspace, path, type.getId()).entrySet()) {
attributes.put(attr.getKey(), attr.getValue().getList());
}
if (!attributes.isEmpty()) {
estimations.add(
DtoFactory.getInstance().createDto(SourceEstimation.class)
.withType(type.getId())
.withAttributes(attributes));
}
} catch (ValueStorageException e) {
// just not added
//e.printStackTrace();
}
}
if (estimations.isEmpty()) {
estimations.add(
DtoFactory.getInstance().createDto(SourceEstimation.class)
.withType(BaseProjectType.ID));
}
return estimations;
}
/**
* Converts existed Folder to Project
* - using projectConfig if it is not null or use internal metainformation (/.codenvy)
*
* @param workspace
* @param projectConfig
* @param visibility
* @return
* @throws ConflictException
* @throws ForbiddenException
* @throws ServerException
* @throws ProjectTypeConstraintException
*/
@Override
public Project convertFolderToProject(String workspace, String path, ProjectConfig projectConfig, String visibility)
throws ConflictException, ForbiddenException, ServerException, NotFoundException {
final VirtualFileEntry projectEntry = getProjectsRoot(workspace).getChild(path);
if (projectEntry == null || !projectEntry.isFolder())
throw new NotFoundException("Not found or not a folder " + path);
FolderEntry projectFolder = (FolderEntry)projectEntry;
final Project project = new Project(projectFolder, this);
// Update config
if (projectConfig != null && projectConfig.getTypeId() != null) {
//TODO: need add checking for concurebcy attributes name in giving config and in estimation
Map<String, AttributeValue> estimateProject = estimateProject(workspace, path, projectConfig.getTypeId());
projectConfig.getAttributes().putAll(estimateProject);
project.updateConfig(projectConfig);
} else { // try to get config (it will throw exception in case config is not valid)
project.getConfig();
}
final ProjectMisc misc = project.getMisc();
misc.setCreationDate(System.currentTimeMillis());
misc.save(); // Important to save misc!!
if (visibility != null) {
project.setVisibility(visibility);
}
return project;
}
}