/*******************************************************************************
* 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.UnauthorizedException;
import org.eclipse.che.api.core.notification.EventService;
import org.eclipse.che.api.core.rest.Service;
import org.eclipse.che.api.core.rest.annotations.Description;
import org.eclipse.che.api.core.rest.annotations.GenerateLink;
import org.eclipse.che.api.core.rest.annotations.Required;
import org.eclipse.che.api.core.util.LineConsumer;
import org.eclipse.che.api.core.util.LineConsumerFactory;
import org.eclipse.che.api.project.server.handlers.GetModulesHandler;
import org.eclipse.che.api.project.server.handlers.PostImportProjectHandler;
import org.eclipse.che.api.project.server.handlers.ProjectHandlerRegistry;
import org.eclipse.che.api.project.server.handlers.ProjectTypeChangedHandler;
import org.eclipse.che.api.project.server.notification.ProjectItemModifiedEvent;
import org.eclipse.che.api.project.server.type.AttributeValue;
import org.eclipse.che.api.project.server.type.ProjectTypeRegistry;
import org.eclipse.che.api.project.shared.EnvironmentId;
import org.eclipse.che.api.project.shared.dto.GeneratorDescription;
import org.eclipse.che.api.project.shared.dto.ImportProject;
import org.eclipse.che.api.project.shared.dto.ImportResponse;
import org.eclipse.che.api.project.shared.dto.ImportSourceDescriptor;
import org.eclipse.che.api.project.shared.dto.ItemReference;
import org.eclipse.che.api.project.shared.dto.NewProject;
import org.eclipse.che.api.project.shared.dto.ProjectDescriptor;
import org.eclipse.che.api.project.shared.dto.ProjectProblem;
import org.eclipse.che.api.project.shared.dto.ProjectReference;
import org.eclipse.che.api.project.shared.dto.ProjectUpdate;
import org.eclipse.che.api.project.shared.dto.RunnerEnvironment;
import org.eclipse.che.api.project.shared.dto.RunnerEnvironmentLeaf;
import org.eclipse.che.api.project.shared.dto.RunnerEnvironmentTree;
import org.eclipse.che.api.project.shared.dto.RunnerSource;
import org.eclipse.che.api.project.shared.dto.Source;
import org.eclipse.che.api.project.shared.dto.SourceEstimation;
import org.eclipse.che.api.project.shared.dto.TreeElement;
import org.eclipse.che.api.vfs.server.ContentStream;
import org.eclipse.che.api.vfs.server.VirtualFile;
import org.eclipse.che.api.vfs.server.VirtualFileSystemImpl;
import org.eclipse.che.api.vfs.server.search.QueryExpression;
import org.eclipse.che.api.vfs.server.search.SearcherProvider;
import org.eclipse.che.api.vfs.shared.dto.AccessControlEntry;
import org.eclipse.che.api.vfs.shared.dto.Principal;
import org.eclipse.che.commons.env.EnvironmentContext;
import org.eclipse.che.dto.server.DtoFactory;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import com.wordnik.swagger.annotations.Api;
import com.wordnik.swagger.annotations.ApiOperation;
import com.wordnik.swagger.annotations.ApiParam;
import com.wordnik.swagger.annotations.ApiResponse;
import com.wordnik.swagger.annotations.ApiResponses;
import org.apache.commons.fileupload.FileItem;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.annotation.Nonnull;
import javax.annotation.PreDestroy;
import javax.annotation.security.RolesAllowed;
import javax.inject.Inject;
import javax.inject.Singleton;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.DefaultValue;
import javax.ws.rs.GET;
import javax.ws.rs.HeaderParam;
import javax.ws.rs.POST;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriBuilder;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import static com.google.common.base.MoreObjects.firstNonNull;
/**
* @author andrew00x
* @author Eugene Voevodin
* @author Artem Zatsarynnyy
* @author Valeriy Svydenko
*/
@Api(value = "/project",
description = "Project manager")
@Path("/project/{ws-id}")
@Singleton // important to have singleton
public class ProjectService extends Service {
private static final Logger LOG = LoggerFactory.getLogger(ProjectService.class);
@Inject
private ProjectManager projectManager;
@Inject
private ProjectImporterRegistry importers;
@Inject
private SearcherProvider searcherProvider;
@Inject
private EventService eventService;
@Inject
private ProjectHandlerRegistry projectHandlerRegistry;
private final ExecutorService executor = Executors.newFixedThreadPool(1 + Runtime.getRuntime().availableProcessors(),
new ThreadFactoryBuilder()
.setNameFormat("ProjectService-IndexingThread-")
.setDaemon(true).build());
@PreDestroy
void stop() {
executor.shutdownNow();
}
/**
* Class for internal use. Need for marking not valid project.
* This need for giving possibility to end user to fix problems in project settings.
* Will be useful then we will migrate IDE2 project to the IDE3 file system.
*/
private class NotValidProject extends Project {
public NotValidProject(FolderEntry baseFolder, ProjectManager manager) {
super(baseFolder, manager);
}
@Override
public ProjectConfig getConfig() throws ServerException, ValueStorageException {
throw new ServerException("Looks like this is not valid project. We will mark it as broken");
}
}
@ApiOperation(value = "Gets list of projects in root folder",
response = ProjectReference.class,
responseContainer = "List",
position = 1)
@ApiResponses(value = {
@ApiResponse(code = 200, message = "OK"),
@ApiResponse(code = 500, message = "Server error")})
@GenerateLink(rel = Constants.LINK_REL_GET_PROJECTS)
@GET
@Produces(MediaType.APPLICATION_JSON)
public List<ProjectReference> getProjects(@ApiParam(value = "ID of workspace to get projects", required = true)
@PathParam("ws-id") String workspace) throws IOException, ServerException,
ConflictException, ForbiddenException {
final List<Project> projects = projectManager.getProjects(workspace);
final List<ProjectReference> projectReferences = new ArrayList<>(projects.size());
for (Project project : projects) {
try {
projectReferences.add(DtoConverter.toReferenceDto2(project, getServiceContext().getServiceUriBuilder()));
} catch (RuntimeException e) {
// Ignore known error for single project.
// In result we won't have them in explorer tree but at least 'bad' projects won't prevent to show 'good' projects.
LOG.error(e.getMessage(), e);
NotValidProject notValidProject = new NotValidProject(project.getBaseFolder(), projectManager);
projectReferences.add(DtoConverter.toReferenceDto2(notValidProject, getServiceContext().getServiceUriBuilder()));
}
}
FolderEntry projectsRoot = projectManager.getProjectsRoot(workspace);
List<VirtualFileEntry> children = projectsRoot.getChildren();
for (VirtualFileEntry child : children) {
if (child.isFolder()) {
FolderEntry folderEntry = (FolderEntry)child;
if (!folderEntry.isProjectFolder()) {
NotValidProject notValidProject = new NotValidProject(folderEntry, projectManager);
projectReferences.add(DtoConverter.toReferenceDto2(notValidProject, getServiceContext().getServiceUriBuilder()));
// projectReferences.add(DtoConverter.toReferenceDto(notValidProject, getServiceContext().getServiceUriBuilder()));
}
}
}
return projectReferences;
}
@ApiOperation(value = "Gets project by ID of workspace and project's path",
response = ProjectDescriptor.class,
position = 2)
@ApiResponses(value = {
@ApiResponse(code = 200, message = "OK"),
@ApiResponse(code = 404, message = "Project with specified path doesn't exist in workspace"),
@ApiResponse(code = 403, message = "Access to requested project is forbidden"),
@ApiResponse(code = 500, message = "Server error")})
@GET
@Path("/{path:.*}")
@Produces(MediaType.APPLICATION_JSON)
public ProjectDescriptor getProject(@ApiParam(value = "ID of workspace to get projects", required = true)
@PathParam("ws-id") String workspace,
@ApiParam(value = "Path to requested project", required = true)
@PathParam("path") String path)
throws NotFoundException, ForbiddenException, ServerException, ConflictException {
Project project = projectManager.getProject(workspace, path);
if (project == null) {
FolderEntry projectsRoot = projectManager.getProjectsRoot(workspace);
VirtualFileEntry child = projectsRoot.getChild(path);
if (child != null && child.isFolder() && child.getParent().isRoot()) {
project = new NotValidProject((FolderEntry)child, projectManager);
} else {
throw new NotFoundException(String.format("Project '%s' doesn't exist in workspace '%s'.", path, workspace));
}
}
try {
ProjectDescriptor projectDescriptor = DtoConverter.toDescriptorDto2(project, getServiceContext().getServiceUriBuilder(),
projectManager.getProjectTypeRegistry());
return projectDescriptor;
} catch (InvalidValueException e) {
NotValidProject notValidProject = new NotValidProject(project.getBaseFolder(), projectManager);
return DtoConverter.toDescriptorDto2(notValidProject, getServiceContext().getServiceUriBuilder(),
projectManager.getProjectTypeRegistry());
}
}
@ApiOperation(value = "Creates new project",
response = ProjectDescriptor.class,
position = 3)
@ApiResponses(value = {
@ApiResponse(code = 200, message = "OK"),
@ApiResponse(code = 403, message = "Operation is forbidden"),
@ApiResponse(code = 409, message = "Project with specified name already exist in workspace"),
@ApiResponse(code = 500, message = "Server error")})
@POST
@GenerateLink(rel = Constants.LINK_REL_CREATE_PROJECT)
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public ProjectDescriptor createProject(@ApiParam(value = "ID of workspace to create project", required = true)
@PathParam("ws-id") String workspace,
@ApiParam(value = "Name for new project", required = true)
@Required
@Description("project name")
@QueryParam("name") String name,
@Description("descriptor of project") NewProject newProject)
throws ConflictException, ForbiddenException, ServerException {
final GeneratorDescription generatorDescription = newProject.getGeneratorDescription();
Map<String, String> options;
if (generatorDescription == null) {
options = Collections.emptyMap();
} else {
options = generatorDescription.getOptions();
}
final Project project = projectManager.createProject(workspace, name,
DtoConverter.fromDto2(newProject, projectManager.getProjectTypeRegistry()),
options,
newProject.getVisibility());
final ProjectDescriptor descriptor = DtoConverter.toDescriptorDto2(project, getServiceContext().getServiceUriBuilder(),
projectManager.getProjectTypeRegistry());
eventService.publish(new ProjectCreatedEvent(project.getWorkspace(), project.getPath()));
logProjectCreatedEvent(descriptor.getName(), descriptor.getType());
return descriptor;
}
@ApiOperation(value = "Get project modules",
notes = "Get project modules. Roles allowed: system/admin, system/manager.",
response = ProjectDescriptor.class,
responseContainer = "List",
position = 4)
@ApiResponses(value = {
@ApiResponse(code = 200, message = "OK"),
@ApiResponse(code = 403, message = "User not authorized to call this operation"),
@ApiResponse(code = 404, message = "Not found"),
@ApiResponse(code = 500, message = "Internal Server Error")})
@GET
@Path("/modules/{path:.*}")
@Produces(MediaType.APPLICATION_JSON)
public List<ProjectDescriptor> getModules(@ApiParam(value = "Workspace ID", required = true)
@PathParam("ws-id") String workspace,
@ApiParam(value = "Path to a project", required = true)
@PathParam("path") String path)
throws NotFoundException, ForbiddenException, ServerException, ConflictException, IOException {
Project parent = projectManager.getProject(workspace, path);
final List<String> modulePaths = new LinkedList<>();
final List<ProjectDescriptor> modules = new LinkedList<>();
for (String p : parent.getModules().get()) {
String modulePath = p.startsWith("/") ? p : parent.getPath() + "/" + p;
modulePaths.add(modulePath);
}
//get modules via handler
GetModulesHandler modulesHandler = projectHandlerRegistry.getModulesHandler(parent.getConfig().getTypeId());
if (modulesHandler != null) {
modulesHandler.onGetModules(parent.getBaseFolder(), modulePaths);
}
for (String modulePath : modulePaths) {
Project module = projectManager.getProject(workspace, modulePath);
modules.add(DtoConverter.toDescriptorDto2(module,
getServiceContext().getServiceUriBuilder(), projectManager.getProjectTypeRegistry()));
}
return modules;
}
@ApiOperation(value = "Create a new module",
notes = "Create a new module in a specified project",
response = ProjectDescriptor.class,
position = 5)
@ApiResponses(value = {
@ApiResponse(code = 200, message = "OK"),
@ApiResponse(code = 403, message = "User not authorized to call this operation"),
@ApiResponse(code = 404, message = "Not found"),
@ApiResponse(code = 409, message = "Module already exists"),
@ApiResponse(code = 500, message = "Internal Server Error")})
@POST
@Path("/{path:.*}")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public ProjectDescriptor createModule(@ApiParam(value = "Workspace ID", required = true)
@PathParam("ws-id") String workspace,
@ApiParam(value = "Path to a target directory", required = true)
@PathParam("path") String parentPath,
@ApiParam(value = "New module name", required = true)
@QueryParam("path") String path,
NewProject newProject)
throws NotFoundException, ConflictException, ForbiddenException, ServerException {
Project module = projectManager.addModule(workspace, parentPath, path,
(newProject == null) ? null : DtoConverter
.fromDto2(newProject, projectManager.getProjectTypeRegistry()),
(newProject == null) ? null : newProject.getGeneratorDescription().getOptions(),
(newProject == null) ? null : newProject.getVisibility());
final ProjectDescriptor descriptor = DtoConverter.toDescriptorDto2(module, getServiceContext().getServiceUriBuilder(),
projectManager.getProjectTypeRegistry());
eventService.publish(new ProjectCreatedEvent(module.getWorkspace(), module.getPath()));
logProjectCreatedEvent(descriptor.getName(), descriptor.getType());
return descriptor;
}
@ApiOperation(value = "Updates existing project",
response = ProjectDescriptor.class,
position = 6)
@ApiResponses(value = {
@ApiResponse(code = 200, message = "OK"),
@ApiResponse(code = 404, message = "Project with specified path doesn't exist in workspace"),
@ApiResponse(code = 403, message = "Operation is forbidden"),
@ApiResponse(code = 409, message = "Update operation causes conflicts"),
@ApiResponse(code = 500, message = "Server error")})
@PUT
@Path("/{path:.*}")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public ProjectDescriptor updateProject(@ApiParam(value = "ID of workspace", required = true)
@PathParam("ws-id") String workspace,
@ApiParam(value = "Path to updated project", required = true)
@PathParam("path") String path,
ProjectUpdate update)
throws NotFoundException, ConflictException, ForbiddenException, ServerException, IOException {
Project project = projectManager.getProject(workspace, path);
String oldProjectType = null;
List<String> oldMixinTypes = new ArrayList<>();
if (project == null) {
FolderEntry projectsRoot = projectManager.getProjectsRoot(workspace);
VirtualFileEntry child = projectsRoot.getChild(path);
if (child != null && child.isFolder() && child.getParent().isRoot()) {
project = new Project((FolderEntry)child, projectManager);
} else {
throw new NotFoundException(String.format("Project '%s' doesn't exist in workspace '%s'.", path, workspace));
}
} else {
try {
oldProjectType = project.getConfig().getTypeId();
oldMixinTypes = project.getConfig().getMixinTypes();
} catch (ProjectTypeConstraintException e) {
//here we allow changing bad project type on registered
LOG.warn(e.getMessage());
}
}
String visibility = update.getVisibility();
if (visibility != null && !visibility.isEmpty()) {
project.setVisibility(visibility);
}
project.updateConfig(DtoConverter.fromDto2(update, projectManager.getProjectTypeRegistry()));
//handle project type changes
//post actions on changing project type
//base or mixin
if (!update.getType().equals(oldProjectType)) {
ProjectTypeChangedHandler projectTypeChangedHandler = projectHandlerRegistry.getProjectTypeChangedHandler(update.getType());
if (projectTypeChangedHandler != null) {
projectTypeChangedHandler.onProjectTypeChanged(project.getBaseFolder());
}
}
List<String> mixinTypes = firstNonNull(update.getMixinTypes(), Collections.<String>emptyList());
for (String mixin : mixinTypes) {
if (!oldMixinTypes.contains(mixin)) {
ProjectTypeChangedHandler projectTypeChangedHandler = projectHandlerRegistry.getProjectTypeChangedHandler(mixin);
if (projectTypeChangedHandler != null) {
projectTypeChangedHandler.onProjectTypeChanged(project.getBaseFolder());
}
}
}
return DtoConverter.toDescriptorDto2(project, getServiceContext().getServiceUriBuilder(),
projectManager.getProjectTypeRegistry());
}
@ApiOperation(value = "Estimates if the folder supposed to be project of certain type",
response = Map.class,
position = 20)
@ApiResponses(value = {
@ApiResponse(code = 200, message = "OK"),
@ApiResponse(code = 404, message = "Project with specified path doesn't exist in workspace"),
@ApiResponse(code = 403, message = "Access to requested project is forbidden"),
@ApiResponse(code = 500, message = "Server error")})
@GET
@Path("/estimate/{path:.*}")
@Produces(MediaType.APPLICATION_JSON)
public Map<String, List<String>> estimateProject(@ApiParam(value = "ID of workspace to estimate projects", required = true)
@PathParam("ws-id") String workspace,
@ApiParam(value = "Path to requested project", required = true)
@PathParam("path") String path,
@ApiParam(value = "Project Type ID to estimate against", required = true)
@QueryParam("type") String projectType)
throws NotFoundException, ForbiddenException, ServerException, ConflictException {
final HashMap<String, List<String>> attributes = new HashMap<>();
for (Map.Entry<String, AttributeValue> attr : projectManager.estimateProject(workspace, path, projectType).entrySet()) {
attributes.put(attr.getKey(), attr.getValue().getList());
}
return attributes;
}
@GET
@Path("/resolve/{path:.*}")
@Produces(MediaType.APPLICATION_JSON)
public List<SourceEstimation> resolveSources(@ApiParam(value = "ID of workspace to estimate projects", required = true)
@PathParam("ws-id") String workspace,
@ApiParam(value = "Path to requested project", required = true)
@PathParam("path") String path)
throws NotFoundException, ForbiddenException, ServerException, ConflictException {
return projectManager.resolveSources(workspace, path, false);
}
@ApiOperation(value = "Create file",
notes = "Create a new file in a project. If file type isn't specified the server will resolve its type.",
position = 7)
@ApiResponses(value = {
@ApiResponse(code = 201, message = ""),
@ApiResponse(code = 403, message = "User not authorized to call this operation"),
@ApiResponse(code = 404, message = "Not found"),
@ApiResponse(code = 409, message = "File already exists"),
@ApiResponse(code = 500, message = "Internal Server Error")})
@POST
@Consumes({MediaType.MEDIA_TYPE_WILDCARD})
@Produces({MediaType.APPLICATION_JSON})
@Path("/file/{parent:.*}")
public Response createFile(@ApiParam(value = "Workspace ID", required = true)
@PathParam("ws-id") String workspace,
@ApiParam(value = "Path to a target directory", required = true)
@PathParam("parent") String parentPath,
@ApiParam(value = "New file name", required = true)
@QueryParam("name") String fileName,
@ApiParam(value = "New file content type")
@HeaderParam("content-type") MediaType contentType,
InputStream content) throws NotFoundException, ConflictException, ForbiddenException, ServerException {
final FolderEntry parent = asFolder(workspace, parentPath);
// Have issue with client side. Always have Content-type header is set even if client doesn't set it.
// In this case have Content-type is set with "text/plain; charset=UTF-8" which isn't acceptable.
// Have agreement with client to send Content-type header with "application/unknown" value if client doesn't want to specify media
// type of new file. In this case server takes care about resolving media type of file.
final FileEntry newFile;
if (contentType == null || ("application".equals(contentType.getType()) && "unknown".equals(contentType.getSubtype()))) {
newFile = parent.createFile(fileName, content, null);
} else {
newFile = parent.createFile(fileName, content, (contentType.getType() + '/' + contentType.getSubtype()));
}
final UriBuilder uriBuilder = getServiceContext().getServiceUriBuilder();
final ItemReference fileReference = DtoConverter.toItemReferenceDto(newFile, uriBuilder.clone());
final URI location = uriBuilder.clone().path(getClass(), "getFile").build(workspace, newFile.getPath().substring(1));
eventService.publish(new ProjectItemModifiedEvent(ProjectItemModifiedEvent.EventType.CREATED,
workspace, projectPath(newFile.getPath()), newFile.getPath(), false));
return Response.created(location).entity(fileReference).build();
}
@ApiOperation(value = "Create a folder",
notes = "Create a folder is a specified project",
position = 8)
@ApiResponses(value = {
@ApiResponse(code = 201, message = ""),
@ApiResponse(code = 403, message = "User not authorized to call this operation"),
@ApiResponse(code = 404, message = "Not found"),
@ApiResponse(code = 409, message = "File already exists"),
@ApiResponse(code = 500, message = "Internal Server Error")})
@POST
@Produces({MediaType.APPLICATION_JSON})
@Path("/folder/{path:.*}")
public Response createFolder(@ApiParam(value = "Workspace ID", required = true)
@PathParam("ws-id") String workspace,
@ApiParam(value = "Path to a new folder destination", required = true)
@PathParam("path") String path)
throws ConflictException, ForbiddenException, ServerException {
final FolderEntry newFolder = projectManager.getProjectsRoot(workspace).createFolder(path);
final UriBuilder uriBuilder = getServiceContext().getServiceUriBuilder();
final ItemReference folderReference = DtoConverter.toItemReferenceDto(newFolder, uriBuilder.clone());
final URI location = uriBuilder.clone().path(getClass(), "getChildren").build(workspace, newFolder.getPath().substring(1));
eventService.publish(new ProjectItemModifiedEvent(ProjectItemModifiedEvent.EventType.CREATED,
workspace, projectPath(newFolder.getPath()), newFolder.getPath(), true));
return Response.created(location).entity(folderReference).build();
}
@ApiOperation(value = "Upload a file",
notes = "Upload a new file",
position = 9)
@ApiResponses(value = {
@ApiResponse(code = 201, message = ""),
@ApiResponse(code = 403, message = "User not authorized to call this operation"),
@ApiResponse(code = 404, message = "Not found"),
@ApiResponse(code = 409, message = "File already exists"),
@ApiResponse(code = 500, message = "Internal Server Error")})
@POST
@Consumes({MediaType.MULTIPART_FORM_DATA})
@Produces({MediaType.TEXT_HTML})
@Path("/uploadfile/{parent:.*}")
public Response uploadFile(@ApiParam(value = "Workspace ID", required = true)
@PathParam("ws-id") String workspace,
@ApiParam(value = "Destination path", required = true)
@PathParam("parent") String parentPath,
Iterator<FileItem> formData)
throws NotFoundException, ConflictException, ForbiddenException, ServerException {
final FolderEntry parent = asFolder(workspace, parentPath);
return VirtualFileSystemImpl.uploadFile(parent.getVirtualFile(), formData);
}
@ApiOperation(value = "Upload zip folder",
notes = "Upload folder from local zip",
response = Response.class,
position = 10)
@ApiResponses(value = {
@ApiResponse(code = 200, message = ""),
@ApiResponse(code = 401, message = "User not authorized to call this operation"),
@ApiResponse(code = 403, message = "Forbidden operation"),
@ApiResponse(code = 404, message = "Not found"),
@ApiResponse(code = 409, message = "Resource already exists"),
@ApiResponse(code = 500, message = "Internal Server Error")})
@POST
@Consumes({MediaType.MULTIPART_FORM_DATA})
@Produces(MediaType.APPLICATION_JSON)
@Path("/upload/zipfolder/{path:.*}")
public Response uploadFolderFromZip(@ApiParam(value = "Workspace ID", required = true)
@PathParam("ws-id") String workspace,
@ApiParam(value = "Path in the project", required = true)
@PathParam("path") String path,
Iterator<FileItem> formData)
throws ServerException, ConflictException, ForbiddenException, NotFoundException {
final FolderEntry parent = asFolder(workspace, path);
return VirtualFileSystemImpl.uploadZip(parent.getVirtualFile(), formData);
}
@ApiOperation(value = "Get file content",
notes = "Get file content by its name",
position = 11)
@ApiResponses(value = {
@ApiResponse(code = 200, message = "OK"),
@ApiResponse(code = 403, message = "User not authorized to call this operation"),
@ApiResponse(code = 404, message = "Not found"),
@ApiResponse(code = 500, message = "Internal Server Error")})
@GET
@Path("/file/{path:.*}")
public Response getFile(@ApiParam(value = "Workspace ID", required = true)
@PathParam("ws-id") String workspace,
@ApiParam(value = "Path to a file", required = true)
@PathParam("path") String path)
throws IOException, NotFoundException, ForbiddenException, ServerException {
final FileEntry file = asFile(workspace, path);
return Response.ok().entity(file.getInputStream()).type(file.getMediaType()).build();
}
@ApiOperation(value = "Update file",
notes = "Update an existing file with new content",
position = 12)
@ApiResponses(value = {
@ApiResponse(code = 200, message = ""),
@ApiResponse(code = 403, message = "User not authorized to call this operation"),
@ApiResponse(code = 404, message = "Not found"),
@ApiResponse(code = 500, message = "Internal Server Error")})
@PUT
@Consumes({MediaType.MEDIA_TYPE_WILDCARD})
@Path("/file/{path:.*}")
public Response updateFile(@ApiParam(value = "Workspace ID", required = true)
@PathParam("ws-id") String workspace,
@ApiParam(value = "Full path to a file", required = true)
@PathParam("path") String path,
@ApiParam(value = "Media Type")
@HeaderParam("content-type") MediaType contentType,
InputStream content) throws NotFoundException, ForbiddenException, ServerException {
final FileEntry file = asFile(workspace, path);
// Have issue with client side. Always have Content-type header is set even if client doesn't set it.
// In this case have Content-type is set with "text/plain; charset=UTF-8" which isn't acceptable.
// Have agreement with client to send Content-type header with "application/unknown" value if client doesn't want to specify media
// type of new file. In this case server takes care about resolving media type of file.
if (contentType == null || ("application".equals(contentType.getType()) && "unknown".equals(contentType.getSubtype()))) {
file.updateContent(content);
} else {
file.updateContent(content, contentType.getType() + '/' + contentType.getSubtype());
}
eventService.publish(new ProjectItemModifiedEvent(ProjectItemModifiedEvent.EventType.UPDATED,
workspace, projectPath(file.getPath()), file.getPath(), false));
return Response.ok().build();
}
@ApiOperation(value = "Delete a resource",
notes = "Delete resources. If you want to delete a single project, specify project name. If a folder or file needs to " +
"be deleted a path to the requested resource needs to be specified",
position = 13)
@ApiResponses(value = {
@ApiResponse(code = 204, message = ""),
@ApiResponse(code = 403, message = "User not authorized to call this operation"),
@ApiResponse(code = 404, message = "Not found"),
@ApiResponse(code = 500, message = "Internal Server Error")})
@DELETE
@Path("/{path:.*}")
public void delete(@ApiParam(value = "Workspace ID", required = true)
@PathParam("ws-id") String workspace,
@ApiParam(value = "Path to a resource to be deleted", required = true)
@PathParam("path") String path,
@QueryParam("module") String modulePath)
throws NotFoundException, ForbiddenException, ConflictException, ServerException {
final VirtualFileEntry entry = getVirtualFileEntry(workspace, path);
if (entry.isFolder() && ((FolderEntry)entry).isProjectFolder()) {
// In case of folder extract some information about project for logger before delete project.
Project project = new Project((FolderEntry)entry, projectManager);
// remove module only
if (modulePath != null) {
project.getModules().remove(modulePath);
return;
}
final String name = project.getName();
String projectType = null;
try {
projectType = project.getConfig().getTypeId();
} catch (ServerException | ValueStorageException e) {
// Let delete even project in invalid state.
LOG.error(e.getMessage(), e);
}
entry.remove();
LOG.info("EVENT#project-destroyed# PROJECT#{}# TYPE#{}# WS#{}# USER#{}#", name, projectType,
EnvironmentContext.getCurrent().getWorkspaceName(), EnvironmentContext.getCurrent().getUser().getName());
} else {
eventService.publish(new ProjectItemModifiedEvent(ProjectItemModifiedEvent.EventType.DELETED,
workspace, projectPath(entry.getPath()), entry.getPath(), entry.isFolder()));
entry.remove();
}
}
@ApiOperation(value = "Copy resource",
notes = "Copy resource to a new location which is specified in a query parameter",
position = 14)
@ApiResponses(value = {
@ApiResponse(code = 201, message = ""),
@ApiResponse(code = 403, message = "User not authorized to call this operation"),
@ApiResponse(code = 404, message = "Not found"),
@ApiResponse(code = 409, message = "Resource already exists"),
@ApiResponse(code = 500, message = "Internal Server Error")})
@POST
@Path("/copy/{path:.*}")
public Response copy(@ApiParam(value = "Workspace ID", required = true)
@PathParam("ws-id") String workspace,
@ApiParam(value = "Path to a resource", required = true)
@PathParam("path") String path,
@ApiParam(value = "Path to a new location", required = true)
@QueryParam("to") String newParent)
throws NotFoundException, ForbiddenException, ConflictException, ServerException {
final VirtualFileEntry entry = getVirtualFileEntry(workspace, path);
final VirtualFileEntry copy = entry.copyTo(newParent);
final URI location = getServiceContext().getServiceUriBuilder()
.path(getClass(), copy.isFile() ? "getFile" : "getChildren")
.build(workspace, copy.getPath().substring(1));
if (copy.isFolder() && ((FolderEntry)copy).isProjectFolder()) {
Project project = new Project((FolderEntry)copy, projectManager);
final String name = project.getName();
final String projectType = project.getConfig().getTypeId();
entry.remove();
logProjectCreatedEvent(name, projectType);
}
return Response.created(location).build();
}
@ApiOperation(value = "Move resource",
notes = "Move resource to a new location which is specified in a query parameter",
position = 15)
@ApiResponses(value = {
@ApiResponse(code = 201, message = ""),
@ApiResponse(code = 403, message = "User not authorized to call this operation"),
@ApiResponse(code = 404, message = "Not found"),
@ApiResponse(code = 409, message = "Resource already exists"),
@ApiResponse(code = 500, message = "Internal Server Error")})
@POST
@Path("/move/{path:.*}")
public Response move(@ApiParam(value = "Workspace ID", required = true)
@PathParam("ws-id") String workspace,
@ApiParam(value = "Path to a resource to be moved", required = true)
@PathParam("path") String path,
@ApiParam(value = "Path to a new location", required = true)
@QueryParam("to") String newParent)
throws NotFoundException, ForbiddenException, ConflictException, ServerException {
final VirtualFileEntry entry = getVirtualFileEntry(workspace, path);
entry.moveTo(newParent);
final URI location = getServiceContext().getServiceUriBuilder()
.path(getClass(), entry.isFile() ? "getFile" : "getChildren")
.build(workspace, entry.getPath().substring(1));
if (entry.isFolder() && ((FolderEntry)entry).isProjectFolder()) {
Project project = new Project((FolderEntry)entry, projectManager);
final String name = project.getName();
final String projectType = project.getConfig().getTypeId();
entry.remove();
LOG.info("EVENT#project-destroyed# PROJECT#{}# TYPE#{}# WS#{}# USER#{}#", name, projectType,
EnvironmentContext.getCurrent().getWorkspaceName(), EnvironmentContext.getCurrent().getUser().getName());
logProjectCreatedEvent(name, projectType);
}
return Response.created(location).build();
}
@ApiOperation(value = "Rename resource",
notes = "Rename resources. It can be project, module, folder or file",
position = 16)
@ApiResponses(value = {
@ApiResponse(code = 201, message = ""),
@ApiResponse(code = 403, message = "User not authorized to call this operation"),
@ApiResponse(code = 404, message = "Not found"),
@ApiResponse(code = 409, message = "Resource already exists"),
@ApiResponse(code = 500, message = "Internal Server Error")})
@POST
@Path("/rename/{path:.*}")
public Response rename(@ApiParam(value = "Workspace ID", required = true)
@PathParam("ws-id") String workspace,
@ApiParam(value = "Path to resource to be renamed", required = true)
@PathParam("path") String path,
@ApiParam(value = "New name", required = true)
@QueryParam("name") String newName,
@ApiParam(value = "New media type")
@QueryParam("mediaType") String newMediaType)
throws NotFoundException, ConflictException, ForbiddenException, ServerException {
final VirtualFileEntry entry = getVirtualFileEntry(workspace, path);
if (entry.isFile() && newMediaType != null) {
// Use the same rules as in method createFile to make client side simpler.
((FileEntry)entry).rename(newName, newMediaType);
} else {
entry.rename(newName);
}
final URI location = getServiceContext().getServiceUriBuilder()
.path(getClass(), entry.isFile() ? "getFile" : "getChildren")
.build(workspace, entry.getPath().substring(1));
return Response.created(location).build();
}
@ApiOperation(value = "Import resource",
notes = "Import resource. JSON with a designated importer and project location is sent. It is possible to import from " +
"VCS or ZIP",
response = ProjectDescriptor.class,
position = 17)
@ApiResponses(value = {
@ApiResponse(code = 200, message = ""),
@ApiResponse(code = 401, message = "User not authorized to call this operation"),
@ApiResponse(code = 403, message = "Forbidden operation"),
@ApiResponse(code = 409, message = "Resource already exists"),
@ApiResponse(code = 500, message = "Unsupported source type")})
@POST
@Path("/import/{path:.*}")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public ImportResponse importProject(@ApiParam(value = "Workspace ID", required = true)
@PathParam("ws-id") String workspace,
@ApiParam(value = "Path in the project", required = true)
@PathParam("path") String path,
@ApiParam(value = "Force rewrite existing project", allowableValues = "true,false")
@QueryParam("force") boolean force,
ImportProject importProject)
throws ConflictException, ForbiddenException, UnauthorizedException, IOException, ServerException, NotFoundException {
final ImportSourceDescriptor projectSource = importProject.getSource().getProject();
final ProjectImporter importer = importers.getImporter(projectSource.getType());
if (importer == null) {
throw new ServerException(String.format("Unable import sources project from '%s'. Sources type '%s' is not supported.",
projectSource.getLocation(), projectSource.getType()));
}
// Preparing websocket output publisher to broadcast output of import process to the ide clients while importing
final String fWorkspace = workspace;
final String fPath = path;
final LineConsumerFactory outputOutputConsumerFactory = new LineConsumerFactory() {
@Override
public LineConsumer newLineConsumer() {
return new ProjectImportOutputWSLineConsumer(fPath, fWorkspace, 300);
}
};
// Not all importers uses virtual file system API. In this case virtual file system API doesn't get events and isn't able to set
// correct creation time. Need do it manually.
long creationDate = System.currentTimeMillis();
VirtualFileEntry virtualFile = getVirtualFile(workspace, path, force);
final FolderEntry baseProjectFolder = (FolderEntry)virtualFile;
importer.importSources(baseProjectFolder, projectSource.getLocation(), projectSource.getParameters(), outputOutputConsumerFactory);
//project source already imported going to configure project
return configureProject(importProject, baseProjectFolder, workspace, creationDate);
}
private VirtualFileEntry getVirtualFile(String workspace, String path, boolean force) throws ServerException, ForbiddenException, ConflictException {
VirtualFileEntry virtualFile = projectManager.getProjectsRoot(workspace).getChild(path);
if (virtualFile != null && virtualFile.isFile()) {
// File with same name exist already exists.
throw new ConflictException(String.format("File with the name '%s' already exists.", path));
} else {
if (virtualFile == null) {
return projectManager.getProjectsRoot(workspace).createFolder(path);
} else if (!force) {
// Project already exists.
throw new ConflictException(String.format("Project with the name '%s' already exists.", path));
}
}
return virtualFile;
}
private ImportResponse configureProject(ImportProject importProject, FolderEntry baseProjectFolder, String workspace, long creationDate)
throws IOException, ForbiddenException, ConflictException, NotFoundException,
ServerException {
ImportResponse importResponse = DtoFactory.getInstance().createDto(ImportResponse.class);
ProjectTypeRegistry projectTypeRegistry = projectManager.getProjectTypeRegistry();
Project project;
ProjectDescriptor projectDescriptor;
ProjectConfig projectConfig = null;
String visibility = null;
NewProject newProject = importProject.getProject();
//try convert folder to project with giving config
try {
if (newProject != null) {
projectConfig = DtoConverter.fromDto2(newProject, projectTypeRegistry);
visibility = newProject.getVisibility();
}
project = projectManager.convertFolderToProject(workspace,
baseProjectFolder.getPath(),
projectConfig,
visibility);
projectDescriptor = DtoConverter.toDescriptorDto2(project,
getServiceContext().getServiceUriBuilder(),
projectManager.getProjectTypeRegistry());
PostImportProjectHandler postImportProjectHandler =
projectHandlerRegistry.getPostImportProjectHandler(projectDescriptor.getType());
if (postImportProjectHandler != null) {
postImportProjectHandler.onProjectImported(project.getBaseFolder());
}
} catch (ConflictException | ForbiddenException | ServerException | NotFoundException e) {
project = new NotValidProject(baseProjectFolder, projectManager);
projectDescriptor = DtoConverter.toDescriptorDto2(project,
getServiceContext().getServiceUriBuilder(),
projectManager.getProjectTypeRegistry());
ProjectProblem problem = DtoFactory.getInstance().createDto(ProjectProblem.class).withCode(1).withMessage(e.getMessage());
projectDescriptor.setProblems(Arrays.asList(problem));
}
importResponse.setProjectDescriptor(projectDescriptor);
//we will add project type estimations any way
List<SourceEstimation> sourceEstimations = projectManager.resolveSources(workspace, baseProjectFolder.getPath(), false);
importResponse.setSourceEstimations(sourceEstimations);
reindexProject(creationDate, baseProjectFolder, project);
importRunnerEnvironment(importProject, baseProjectFolder);
eventService.publish(new ProjectCreatedEvent(project.getWorkspace(), project.getPath()));
logProjectCreatedEvent(projectDescriptor.getName(), projectDescriptor.getType());
return importResponse;
}
@ApiOperation(value = "Upload zip project",
notes = "Upload project from local zip",
response = ImportResponse.class,
position = 18)
@ApiResponses(value = {
@ApiResponse(code = 200, message = ""),
@ApiResponse(code = 401, message = "User not authorized to call this operation"),
@ApiResponse(code = 403, message = "Forbidden operation"),
@ApiResponse(code = 409, message = "Resource already exists"),
@ApiResponse(code = 500, message = "Unsupported source type")})
@POST
@Consumes({MediaType.MULTIPART_FORM_DATA})
@Produces(MediaType.APPLICATION_JSON)
@Path("/upload/zipproject/{path:.*}")
public ImportResponse uploadProjectFromZip(@ApiParam(value = "Workspace ID", required = true)
@PathParam("ws-id") String workspace,
@ApiParam(value = "Path in the project", required = true)
@PathParam("path") String path,
@ApiParam(value = "Force rewrite existing project", allowableValues = "true,false")
@QueryParam("force") boolean force,
Iterator<FileItem> formData)
throws ServerException, IOException, ConflictException, ForbiddenException, NotFoundException {
// Not all importers uses virtual file system API. In this case virtual file system API doesn't get events and isn't able to set
// correct creation time. Need do it manually.
long creationDate = System.currentTimeMillis();
final FolderEntry baseProjectFolder = (FolderEntry)getVirtualFile(workspace, path, force);
int stripNumber = 0;
String projectName = "";
String projectDescription = "";
String privacy = "";
FileItem contentItem = null;
while (formData.hasNext()) {
FileItem item = formData.next();
if (!item.isFormField()) {
if (contentItem == null) {
contentItem = item;
} else {
throw new ServerException("More then one upload file is found but only one should be. ");
}
} else {
switch (item.getFieldName()) {
case ("name"):
projectName = item.getString().trim();
break;
case ("description"):
projectDescription = item.getString().trim();
break;
case ("privacy"):
privacy = Boolean.parseBoolean(item.getString().trim()) ? "public" : "private";
break;
case ("skipFirstLevel"):
stripNumber = Boolean.parseBoolean(item.getString().trim()) ? 1 : 0;
break;
}
}
}
if (contentItem == null) {
throw new ServerException("Cannot find zip file for upload.");
}
try (InputStream zip = contentItem.getInputStream()) {
baseProjectFolder.getVirtualFile().unzip(zip, true, stripNumber);
}
final DtoFactory dtoFactory = DtoFactory.getInstance();
NewProject newProject = dtoFactory.createDto(NewProject.class)
.withName(projectName)
.withDescription(projectDescription)
.withVisibility(privacy);
Source source = dtoFactory.createDto(Source.class)
.withRunners(new HashMap<String, RunnerSource>());
ImportProject importProject = dtoFactory.createDto(ImportProject.class)
.withProject(newProject)
.withSource(source);
//project source already imported going to configure project
return configureProject(importProject, baseProjectFolder, workspace, creationDate);
}
/**
* Import runner environment tha configure in ImportProject
*
* @param importProject
* @param baseProjectFolder
* @throws ForbiddenException
* @throws ServerException
* @throws ConflictException
* @throws IOException
*/
private void importRunnerEnvironment(ImportProject importProject, FolderEntry baseProjectFolder)
throws ForbiddenException, ServerException, ConflictException, IOException {
VirtualFileEntry environmentsFolder = baseProjectFolder.getChild(Constants.CODENVY_RUNNER_ENVIRONMENTS_DIR);
if (environmentsFolder != null && environmentsFolder.isFile()) {
throw new ConflictException(String.format("Unable import runner environments. File with the name '%s' already exists.",
Constants.CODENVY_RUNNER_ENVIRONMENTS_DIR));
} else if (environmentsFolder == null) {
environmentsFolder = baseProjectFolder.createFolder(Constants.CODENVY_RUNNER_ENVIRONMENTS_DIR);
}
for (Map.Entry<String, RunnerSource> runnerSource : importProject.getSource().getRunners().entrySet()) {
final String runnerSourceKey = runnerSource.getKey();
if (runnerSourceKey.startsWith("/docker/")) {
final RunnerSource runnerSourceValue = runnerSource.getValue();
if (runnerSourceValue != null) {
String name = runnerSourceKey.substring(8);
String runnerSourceLocation = runnerSourceValue.getLocation();
if (runnerSourceLocation.startsWith("https") || runnerSourceLocation.startsWith("http")) {
try (InputStream in = new java.net.URL(runnerSourceLocation).openStream()) {
// Add file without mediatype to avoid creation useless metadata files on virtual file system level.
// Dockerfile add in list of known files, see ContentTypeGuesser
// and content-types.attributes file.
((FolderEntry)environmentsFolder).createFolder(name).createFile("Dockerfile", in, null);
}
} else {
LOG.warn(
"ProjectService.importProject :: not valid runner source location available only http or https scheme but" +
" we get :" +
runnerSourceLocation);
}
}
}
}
}
/**
* Some importers don't use virtual file system API and changes are not indexed.
* Force searcher to reindex project to fix such issues.
*
* @param creationDate
* @param baseProjectFolder
* @param project
* @throws ServerException
*/
private void reindexProject(long creationDate, FolderEntry baseProjectFolder, Project project) throws ServerException {
final VirtualFile file = baseProjectFolder.getVirtualFile();
executor.execute(new Runnable() {
@Override
public void run() {
try {
searcherProvider.getSearcher(file.getMountPoint(), true).add(file);
} catch (Exception e) {
LOG.error(e.getMessage());
}
}
});
if (creationDate > 0) {
final ProjectMisc misc = project.getMisc();
misc.setCreationDate(creationDate);
}
}
@ApiOperation(value = "Import zip",
notes = "Import resources as zip",
position = 19)
@ApiResponses(value = {
@ApiResponse(code = 201, message = ""),
@ApiResponse(code = 403, message = "User not authorized to call this operation"),
@ApiResponse(code = 404, message = "Not found"),
@ApiResponse(code = 409, message = "Resource already exists"),
@ApiResponse(code = 500, message = "Internal Server Error")})
@POST
@Path("/import/{path:.*}")
@Consumes("application/zip")
public Response importZip(@ApiParam(value = "Workspace ID", required = true)
@PathParam("ws-id") String workspace,
@ApiParam(value = "Path to a location (where import to?)")
@PathParam("path") String path,
InputStream zip,
@DefaultValue("false") @QueryParam("skipFirstLevel") Boolean skipFirstLevel)
throws NotFoundException, ConflictException, ForbiddenException, ServerException {
final FolderEntry parent = asFolder(workspace, path);
VirtualFileSystemImpl.importZip(parent.getVirtualFile(), zip, true, skipFirstLevel);
if (parent.isProjectFolder()) {
Project project = new Project(parent, projectManager);
eventService.publish(new ProjectCreatedEvent(project.getWorkspace(), project.getPath()));
final String projectType = project.getConfig().getTypeId();
logProjectCreatedEvent(path, projectType);
}
return Response.created(getServiceContext().getServiceUriBuilder()
.path(getClass(), "getChildren")
.build(workspace, parent.getPath().substring(1))).build();
}
@ApiOperation(value = "Download ZIP",
notes = "Export resource as zip. It can be an entire project or folder",
position = 20)
@ApiResponses(value = {
@ApiResponse(code = 201, message = ""),
@ApiResponse(code = 403, message = "User not authorized to call this operation"),
@ApiResponse(code = 404, message = "Not found"),
@ApiResponse(code = 500, message = "Internal Server Error")})
@GET
@Path("/export/{path:.*}")
@Produces("application/zip")
public ContentStream exportZip(@ApiParam(value = "Workspace ID", required = true)
@PathParam("ws-id") String workspace,
@ApiParam(value = "Path to resource to be imported")
@PathParam("path") String path)
throws NotFoundException, ForbiddenException, ServerException {
final FolderEntry folder = asFolder(workspace, path);
return VirtualFileSystemImpl.exportZip(folder.getVirtualFile());
}
@POST
@Path("/export/{path:.*}")
@Consumes(MediaType.TEXT_PLAIN)
@Produces("application/zip")
public Response exportDiffZip(@PathParam("ws-id") String workspace, @PathParam("path") String path, InputStream in)
throws NotFoundException, ForbiddenException, ServerException {
final FolderEntry folder = asFolder(workspace, path);
return VirtualFileSystemImpl.exportZip(folder.getVirtualFile(), in);
}
@POST
@Path("/export/{path:.*}")
@Consumes(MediaType.TEXT_PLAIN)
@Produces(MediaType.MULTIPART_FORM_DATA)
public Response exportDiffZipMultipart(@PathParam("ws-id") String workspace, @PathParam("path") String path, InputStream in)
throws NotFoundException, ForbiddenException, ServerException {
final FolderEntry folder = asFolder(workspace, path);
return VirtualFileSystemImpl.exportZipMultipart(folder.getVirtualFile(), in);
}
@ApiOperation(value = "Get project children items",
notes = "Request all children items for a project, such as files and folders",
response = ItemReference.class,
responseContainer = "List",
position = 21)
@ApiResponses(value = {
@ApiResponse(code = 200, message = "OK"),
@ApiResponse(code = 403, message = "User not authorized to call this operation"),
@ApiResponse(code = 404, message = "Not found"),
@ApiResponse(code = 500, message = "Internal Server Error")})
@GET
@Path("/children/{parent:.*}")
@Produces(MediaType.APPLICATION_JSON)
public List<ItemReference> getChildren(@ApiParam(value = "Workspace ID", required = true)
@PathParam("ws-id") String workspace,
@ApiParam(value = "Path to a project", required = true)
@PathParam("parent") String path)
throws NotFoundException, ForbiddenException, ServerException {
final FolderEntry folder = asFolder(workspace, path);
final List<VirtualFileEntry> children = folder.getChildren();
final ArrayList<ItemReference> result = new ArrayList<>(children.size());
final UriBuilder uriBuilder = getServiceContext().getServiceUriBuilder();
for (VirtualFileEntry child : children) {
if (child.isFile()) {
result.add(DtoConverter.toItemReferenceDto((FileEntry)child, uriBuilder.clone()));
} else {
result.add(DtoConverter.toItemReferenceDto((FolderEntry)child, uriBuilder.clone()));
}
}
return result;
}
@ApiOperation(value = "Get project tree",
notes = "Get project tree. Depth is specified in a query parameter",
response = TreeElement.class,
responseContainer = "List",
position = 22)
@ApiResponses(value = {
@ApiResponse(code = 200, message = "OK"),
@ApiResponse(code = 403, message = "User not authorized to call this operation"),
@ApiResponse(code = 404, message = "Not found"),
@ApiResponse(code = 500, message = "Internal Server Error")})
@GET
@Path("/tree/{parent:.*}")
@Produces(MediaType.APPLICATION_JSON)
public TreeElement getTree(@ApiParam(value = "Workspace ID", required = true)
@PathParam("ws-id") String workspace,
@ApiParam(value = "Path to resource. Can be project or its folders", required = true)
@PathParam("parent") String path,
@ApiParam(value = "Tree depth. This parameter can be dropped. If not specified ?depth=1 is used by default")
@DefaultValue("1") @QueryParam("depth") int depth)
throws NotFoundException, ForbiddenException, ServerException {
final FolderEntry folder = asFolder(workspace, path);
final UriBuilder uriBuilder = getServiceContext().getServiceUriBuilder();
final DtoFactory dtoFactory = DtoFactory.getInstance();
return dtoFactory.createDto(TreeElement.class)
.withNode(DtoConverter.toItemReferenceDto(folder, uriBuilder.clone()))
.withChildren(getTree(folder, depth, uriBuilder, dtoFactory));
}
@ApiOperation(value = "Get file or folder",
response = TreeElement.class,
responseContainer = "List",
position = 28)
@ApiResponses(value = {
@ApiResponse(code = 200, message = "OK"),
@ApiResponse(code = 403, message = "User not authorized to call this operation"),
@ApiResponse(code = 404, message = "Not found"),
@ApiResponse(code = 500, message = "Internal Server Error")})
@GET
@Path("/item/{path:.*}")
@Produces(MediaType.APPLICATION_JSON)
public ItemReference getItem(@ApiParam(value = "Workspace ID", required = true)
@PathParam("ws-id") String workspace,
@ApiParam(value = "Path to resource. Can be project or its folders", required = true)
@PathParam("path") String path)
throws NotFoundException, ForbiddenException, ServerException, ValueStorageException,
ProjectTypeConstraintException {
Project project = projectManager.getProject(workspace, projectPath(path));
final VirtualFileEntry entry = project.getItem(path);
final UriBuilder uriBuilder = getServiceContext().getServiceUriBuilder();
ItemReference item;
if (entry.isFile()) {
item = DtoConverter.toItemReferenceDto((FileEntry)entry, uriBuilder.clone());
} else {
item = DtoConverter.toItemReferenceDto((FolderEntry)entry, uriBuilder.clone());
}
return item;
}
private List<TreeElement> getTree(FolderEntry folder, int depth, UriBuilder uriBuilder, DtoFactory dtoFactory) throws ServerException {
if (depth == 0) {
return null;
}
final List<FolderEntry> childFolders = folder.getChildFolders();
final List<TreeElement> nodes = new ArrayList<>(childFolders.size());
for (FolderEntry childFolder : childFolders) {
nodes.add(dtoFactory.createDto(TreeElement.class)
.withNode(DtoConverter.toItemReferenceDto(childFolder, uriBuilder.clone()))
.withChildren(getTree(childFolder, depth - 1, uriBuilder, dtoFactory)));
}
return nodes;
}
@ApiOperation(value = "Search for resources",
notes = "Search for resources applying a number of search filters as query parameters",
response = ItemReference.class,
responseContainer = "List",
position = 23)
@ApiResponses(value = {
@ApiResponse(code = 200, message = "OK"),
@ApiResponse(code = 403, message = "User not authorized to call this operation"),
@ApiResponse(code = 404, message = "Not found"),
@ApiResponse(code = 409, message = "Conflict error"),
@ApiResponse(code = 500, message = "Internal Server Error")})
@GET
@Path("/search/{path:.*}")
@Produces(MediaType.APPLICATION_JSON)
public List<ItemReference> search(@ApiParam(value = "Workspace ID", required = true)
@PathParam("ws-id") String workspace,
@ApiParam(value = "Path to resource, i.e. where to search?", required = true)
@PathParam("path") String path,
@ApiParam(value = "Resource name")
@QueryParam("name") String name,
@ApiParam(value = "Media type")
@QueryParam("mediatype") String mediatype,
@ApiParam(value = "Search keywords")
@QueryParam("text") String text,
@ApiParam(value = "Maximum items to display. If this parameter is dropped, there are no limits")
@QueryParam("maxItems") @DefaultValue("-1") int maxItems,
@ApiParam(value = "Skip count")
@QueryParam("skipCount") int skipCount)
throws NotFoundException, ForbiddenException, ConflictException, ServerException {
// to search from workspace root path should end with "/" i.e /{ws}/search/?<query>
final FolderEntry folder = path.isEmpty() ? projectManager.getProjectsRoot(workspace) : asFolder(workspace, path);
if (searcherProvider != null) {
if (skipCount < 0) {
throw new ConflictException(String.format("Invalid 'skipCount' parameter: %d.", skipCount));
}
final QueryExpression expr = new QueryExpression()
.setPath(path.startsWith("/") ? path : ('/' + path))
.setName(name)
.setMediaType(mediatype)
.setText(text);
final String[] result = searcherProvider.getSearcher(folder.getVirtualFile().getMountPoint(), true).search(expr);
if (skipCount > 0) {
if (skipCount > result.length) {
throw new ConflictException(
String.format("'skipCount' parameter: %d is greater then total number of items in result: %d.",
skipCount, result.length));
}
}
final int length = maxItems > 0 ? Math.min(result.length, maxItems) : result.length;
final List<ItemReference> items = new ArrayList<>(length);
final FolderEntry root = projectManager.getProjectsRoot(workspace);
final UriBuilder uriBuilder = getServiceContext().getServiceUriBuilder();
for (int i = skipCount; i < length; i++) {
VirtualFileEntry child = null;
try {
child = root.getChild(result[i]);
} catch (ForbiddenException ignored) {
// Ignore item that user can't access
}
if (child != null && child.isFile()) {
items.add(DtoConverter.toItemReferenceDto((FileEntry)child, uriBuilder.clone()));
}
}
return items;
}
return Collections.emptyList();
}
@ApiOperation(value = "Get user permissions in a project",
notes = "Get permissions for a user in a specified project, such as read, write, build, " +
"run etc. ID of a user is set in a query parameter of a request URL.",
response = AccessControlEntry.class,
responseContainer = "List",
position = 24)
@ApiResponses(value = {
@ApiResponse(code = 200, message = "OK"),
@ApiResponse(code = 403, message = "User not authorized to call this operation"),
@ApiResponse(code = 404, message = "Not found"),
@ApiResponse(code = 500, message = "Internal Server Error")})
@GET
@Path("/permissions/{path:.*}")
@Produces(MediaType.APPLICATION_JSON)
@RolesAllowed("workspace/admin")
public List<AccessControlEntry> getPermissions(@ApiParam(value = "Workspace ID", required = true)
@PathParam("ws-id") String wsId,
@ApiParam(value = "Path to a project", required = true)
@PathParam("path") String path,
@ApiParam(value = "User ID", required = true)
@QueryParam("userid") Set<String> users)
throws NotFoundException, ForbiddenException, ServerException {
final Project project = projectManager.getProject(wsId, path);
if (project == null) {
throw new NotFoundException(String.format("Project '%s' doesn't exist in workspace '%s'.", path, wsId));
}
final List<AccessControlEntry> acl = project.getPermissions();
if (!(users == null || users.isEmpty())) {
for (Iterator<AccessControlEntry> itr = acl.iterator(); itr.hasNext(); ) {
final AccessControlEntry ace = itr.next();
final Principal principal = ace.getPrincipal();
if (principal.getType() != Principal.Type.USER || !users.contains(principal.getName())) {
itr.remove();
}
}
}
return acl;
}
@ApiOperation(value = "Set project visibility",
notes = "Set project visibility. Projects can be private or public",
position = 25)
@ApiResponses(value = {
@ApiResponse(code = 204, message = "OK"),
@ApiResponse(code = 403, message = "User not authorized to call this operation"),
@ApiResponse(code = 404, message = "Not found"),
@ApiResponse(code = 500, message = "Internal Server Error")})
@POST
@Path("/switch_visibility/{path:.*}")
public void switchVisibility(@ApiParam(value = "Workspace ID", required = true)
@PathParam("ws-id") String wsId,
@ApiParam(value = "Path to a project", required = true)
@PathParam("path") String path,
@ApiParam(value = "Visibility type", required = true, allowableValues = "public,private")
@QueryParam("visibility") String visibility)
throws NotFoundException, ForbiddenException, ServerException {
if (visibility == null || visibility.isEmpty()) {
throw new ServerException(String.format("Invalid visibility '%s'", visibility));
}
final Project project = projectManager.getProject(wsId, path);
if (project == null) {
throw new NotFoundException(String.format("Project '%s' doesn't exist in workspace '%s'.", path, wsId));
}
project.setVisibility(visibility);
}
@ApiOperation(value = "Set permissions for a user in a project",
notes = "Set permissions for a user in a specified project, such as read, write, build, " +
"run etc. ID of a user is set in a query parameter of a request URL.",
response = AccessControlEntry.class,
responseContainer = "List",
position = 26)
@ApiResponses(value = {
@ApiResponse(code = 204, message = "OK"),
@ApiResponse(code = 403, message = "User not authorized to call this operation"),
@ApiResponse(code = 404, message = "Not found"),
@ApiResponse(code = 500, message = "Internal Server Error")})
@POST
@Path("/permissions/{path:.*}")
@Consumes(MediaType.APPLICATION_JSON)
@RolesAllowed("workspace/admin")
public List<AccessControlEntry> setPermissions(@ApiParam(value = "Workspace ID", required = true)
@PathParam("ws-id") String wsId,
@ApiParam(value = "Path to a project", required = true)
@PathParam("path") String path,
@ApiParam(value = "Permissions", required = true)
List<AccessControlEntry> acl) throws ForbiddenException, ServerException {
final Project project = projectManager.getProject(wsId, path);
if (project == null) {
throw new ServerException(String.format("Project '%s' doesn't exist in workspace '%s'. ", path, wsId));
}
project.setPermissions(acl);
return project.getPermissions();
}
@ApiOperation(value = "Get available project-scoped runner environments",
notes = "Get available project-scoped runner environments.",
response = RunnerEnvironmentTree.class,
position = 27)
@ApiResponses(value = {
@ApiResponse(code = 200, message = "OK"),
@ApiResponse(code = 403, message = "User not authorized to call this operation"),
@ApiResponse(code = 404, message = "Not found"),
@ApiResponse(code = 500, message = "Internal Server Error")})
@GET
@Path("/runner_environments/{path:.*}")
@Produces(MediaType.APPLICATION_JSON)
public RunnerEnvironmentTree getRunnerEnvironments(@ApiParam(value = "Workspace ID", required = true)
@PathParam("ws-id") String workspace,
@ApiParam(value = "Path to a project", required = true)
@PathParam("path") String path)
throws NotFoundException, ForbiddenException, ServerException {
final Project project = projectManager.getProject(workspace, path);
final DtoFactory dtoFactory = DtoFactory.getInstance();
final RunnerEnvironmentTree root = dtoFactory.createDto(RunnerEnvironmentTree.class).withDisplayName("project");
if (project != null) {
final List<RunnerEnvironmentLeaf> environments = new LinkedList<>();
final VirtualFileEntry environmentsFolder = project.getBaseFolder().getChild(Constants.CODENVY_RUNNER_ENVIRONMENTS_DIR);
if (environmentsFolder != null && environmentsFolder.isFolder()) {
for (FolderEntry childFolder : ((FolderEntry)environmentsFolder).getChildFolders()) {
final String id = new EnvironmentId(EnvironmentId.Scope.project, childFolder.getName()).toString();
environments.add(dtoFactory.createDto(RunnerEnvironmentLeaf.class)
.withEnvironment(dtoFactory.createDto(RunnerEnvironment.class).withId(id))
.withDisplayName(childFolder.getName()));
}
}
return root.withLeaves(environments);
} else {
return root.withLeaves(Collections.<RunnerEnvironmentLeaf>emptyList());
}
}
private FileEntry asFile(String workspace, String path) throws ForbiddenException, NotFoundException, ServerException {
final VirtualFileEntry entry = getVirtualFileEntry(workspace, path);
if (!entry.isFile()) {
throw new ForbiddenException(String.format("Item '%s' isn't a file. ", path));
}
return (FileEntry)entry;
}
private FolderEntry asFolder(String workspace, String path) throws ForbiddenException, NotFoundException, ServerException {
final VirtualFileEntry entry = getVirtualFileEntry(workspace, path);
if (!entry.isFolder()) {
throw new ForbiddenException(String.format("Item '%s' isn't a file. ", path));
}
return (FolderEntry)entry;
}
private VirtualFileEntry getVirtualFileEntry(String workspace, String path)
throws NotFoundException, ForbiddenException, ServerException {
final FolderEntry root = projectManager.getProjectsRoot(workspace);
final VirtualFileEntry entry = root.getChild(path);
if (entry == null) {
throw new NotFoundException(String.format("Path '%s' doesn't exist.", path));
}
return entry;
}
private void logProjectCreatedEvent(@Nonnull String projectName, @Nonnull String projectType) {
LOG.info("EVENT#project-created# PROJECT#{}# TYPE#{}# WS#{}# USER#{}# PAAS#default#",
projectName,
projectType,
EnvironmentContext.getCurrent().getWorkspaceId(),
EnvironmentContext.getCurrent().getUser().getId());
}
private String projectPath(String path) {
return path.substring(0, path.indexOf("/"));
}
}