/*******************************************************************************
* 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 io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import io.swagger.annotations.ApiResponse;
import io.swagger.annotations.ApiResponses;
import org.apache.commons.fileupload.FileItem;
import org.apache.tika.Tika;
import org.eclipse.che.WorkspaceIdProvider;
import org.eclipse.che.api.core.BadRequestException;
import org.eclipse.che.api.core.ConflictException;
import org.eclipse.che.api.core.ForbiddenException;
import org.eclipse.che.api.core.NotFoundException;
import org.eclipse.che.api.core.ServerException;
import org.eclipse.che.api.core.UnauthorizedException;
import org.eclipse.che.api.core.model.project.type.Value;
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.project.server.importer.ProjectImportOutputWSLineConsumer;
import org.eclipse.che.api.project.server.notification.ProjectItemModifiedEvent;
import org.eclipse.che.api.project.server.type.ProjectTypeResolution;
import org.eclipse.che.api.project.shared.dto.CopyOptions;
import org.eclipse.che.api.project.shared.dto.ItemReference;
import org.eclipse.che.api.project.shared.dto.MoveOptions;
import org.eclipse.che.api.project.shared.dto.SourceEstimation;
import org.eclipse.che.api.project.shared.dto.TreeElement;
import org.eclipse.che.api.vfs.VirtualFile;
import org.eclipse.che.api.vfs.search.QueryExpression;
import org.eclipse.che.api.vfs.search.SearchResult;
import org.eclipse.che.api.vfs.search.SearchResultEntry;
import org.eclipse.che.api.vfs.search.Searcher;
import org.eclipse.che.api.workspace.shared.dto.NewProjectConfigDto;
import org.eclipse.che.api.workspace.shared.dto.ProjectConfigDto;
import org.eclipse.che.api.workspace.shared.dto.SourceStorageDto;
import org.eclipse.che.commons.env.EnvironmentContext;
import org.eclipse.che.commons.lang.ws.rs.ExtMediaType;
import org.eclipse.che.dto.server.DtoFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import javax.inject.Singleton;
import javax.validation.constraints.NotNull;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.DefaultValue;
import javax.ws.rs.GET;
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.Context;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriInfo;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import static org.eclipse.che.api.project.server.DtoConverter.asDto;
import static org.eclipse.che.api.project.shared.Constants.LINK_REL_CREATE_BATCH_PROJECTS;
import static org.eclipse.che.api.project.shared.Constants.LINK_REL_CREATE_PROJECT;
import static org.eclipse.che.api.project.shared.Constants.LINK_REL_GET_PROJECTS;
import static org.eclipse.che.dto.server.DtoFactory.newDto;
/**
* Project API.
*
* @author andrew00x
* @author Eugene Voevodin
* @author Artem Zatsarynnyi
* @author Valeriy Svydenko
* @author Dmitry Shnurenko
*/
@Api(value = "/project", description = "Project REST API")
@Path("/project")
@Singleton
public class ProjectService extends Service {
private static final Logger LOG = LoggerFactory.getLogger(ProjectService.class);
private static final Tika TIKA = new Tika();
private final ProjectManager projectManager;
private final EventService eventService;
private final ProjectServiceLinksInjector projectServiceLinksInjector;
private final String workspace;
@Inject
public ProjectService(ProjectManager projectManager,
EventService eventService,
ProjectServiceLinksInjector projectServiceLinksInjector) {
this.projectManager = projectManager;
this.eventService = eventService;
this.projectServiceLinksInjector = projectServiceLinksInjector;
this.workspace = WorkspaceIdProvider.getWorkspaceId();
}
@GET
@Produces(MediaType.APPLICATION_JSON)
@ApiOperation(value = "Gets list of projects in root folder",
response = ProjectConfigDto.class,
responseContainer = "List")
@ApiResponses({@ApiResponse(code = 200, message = "OK"),
@ApiResponse(code = 500, message = "Server error")})
@GenerateLink(rel = LINK_REL_GET_PROJECTS)
public List<ProjectConfigDto> getProjects() throws IOException,
ServerException,
ConflictException,
ForbiddenException {
return projectManager.getProjects()
.stream()
.map(p -> injectProjectLinks(asDto(p)))
.collect(Collectors.toList());
}
@GET
@Path("/{path:.*}")
@Produces(MediaType.APPLICATION_JSON)
@ApiOperation(value = "Gets project by ID of workspace and project's path",
response = ProjectConfigDto.class)
@ApiResponses({@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")})
public ProjectConfigDto getProject(@ApiParam(value = "Path to requested project", required = true)
@PathParam("path") String path) throws NotFoundException,
ForbiddenException,
ServerException,
ConflictException {
return injectProjectLinks(asDto(projectManager.getProject(path)));
}
@POST
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
@ApiOperation(value = "Creates new project",
response = ProjectConfigDto.class)
@ApiResponses({@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")})
@GenerateLink(rel = LINK_REL_CREATE_PROJECT)
/**
* NOTE: parentPath is added to make a module
*/
public ProjectConfigDto createProject(@ApiParam(value = "Add to this project as module", required = false)
@Context UriInfo uriInfo,
@Description("descriptor of project") ProjectConfigDto projectConfig) throws ConflictException,
ForbiddenException,
ServerException,
NotFoundException {
Map<String, String> options = new HashMap<>();
MultivaluedMap<String, String> map = uriInfo.getQueryParameters();
for(String key: map.keySet()) {
options.put(key, map.get(key).get(0));
}
String pathToProject = projectConfig.getPath();
String pathToParent = pathToProject.substring(0, pathToProject.lastIndexOf("/"));
if (!pathToParent.equals("/")) {
VirtualFileEntry parentFileEntry = projectManager.getProjectsRoot().getChild(pathToParent);
if (parentFileEntry == null) {
throw new NotFoundException("The parent folder with path " + pathToParent + " does not exist.");
}
}
final RegisteredProject project = projectManager.createProject(projectConfig, options);
final ProjectConfigDto configDto = asDto(project);
eventService.publish(new ProjectCreatedEvent(workspace, project.getPath()));
// TODO this throws NPE
//logProjectCreatedEvent(configDto.getName(), configDto.getProjectType());
return injectProjectLinks(configDto);
}
@POST
@Path("/batch")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
@ApiOperation(value = "Creates batch of projects according to their configurations",
notes = "A project will be created by importing when project configuration contains source object. " +
"For creating a project by generator options should be specified.",
response = ProjectConfigDto.class)
@ApiResponses({@ApiResponse(code = 200, message = "OK"),
@ApiResponse(code = 400, message = "Path for new project should be defined"),
@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")})
@GenerateLink(rel = LINK_REL_CREATE_BATCH_PROJECTS)
public List<ProjectConfigDto> createBatchProjects(
@Description("list of descriptors for projects") List<NewProjectConfigDto> projectConfigList,
@ApiParam(value = "Force rewrite existing project", allowableValues = "true,false")
@QueryParam("force") boolean rewrite)
throws ConflictException, ForbiddenException, ServerException, NotFoundException, IOException, UnauthorizedException,
BadRequestException {
List<ProjectConfigDto> result = new ArrayList<>(projectConfigList.size());
final ProjectOutputLineConsumerFactory outputOutputConsumerFactory = new ProjectOutputLineConsumerFactory(workspace, 300);
for (RegisteredProject registeredProject : projectManager.createBatchProjects(projectConfigList, rewrite, outputOutputConsumerFactory)) {
ProjectConfigDto projectConfig = injectProjectLinks(asDto(registeredProject));
result.add(projectConfig);
eventService.publish(new ProjectCreatedEvent(workspace, registeredProject.getPath()));
}
return result;
}
@PUT
@Path("/{path:.*}")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
@ApiOperation(value = "Updates existing project",
response = ProjectConfigDto.class)
@ApiResponses({@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")})
public ProjectConfigDto updateProject(@ApiParam(value = "Path to updated project", required = true)
@PathParam("path") String path,
ProjectConfigDto projectConfigDto) throws NotFoundException,
ConflictException,
ForbiddenException,
ServerException,
IOException {
if (path != null) {
projectConfigDto.setPath(path);
}
return asDto(projectManager.updateProject(projectConfigDto));
}
@DELETE
@Path("/{path:.*}")
@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")
@ApiResponses({@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")})
public void delete(@ApiParam("Path to a resource to be deleted")
@PathParam("path") String path) throws NotFoundException, ForbiddenException, ConflictException, ServerException {
projectManager.delete(path);
}
@GET
@Path("/estimate/{path:.*}")
@Produces(MediaType.APPLICATION_JSON)
@ApiOperation(value = "Estimates if the folder supposed to be project of certain type",
response = Map.class)
@ApiResponses({@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")})
public SourceEstimation estimateProject(@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 ProjectTypeResolution resolution = projectManager.estimateProject(path, projectType);
final HashMap<String, List<String>> attributes = new HashMap<>();
for (Map.Entry<String, Value> attr : resolution.getProvidedAttributes().entrySet()) {
attributes.put(attr.getKey(), attr.getValue().getList());
}
return DtoFactory.newDto(SourceEstimation.class)
.withType(projectType)
.withMatched(resolution.matched())
.withResolution(resolution.getResolution())
.withAttributes(attributes);
}
@GET
@Path("/resolve/{path:.*}")
@Produces(MediaType.APPLICATION_JSON)
public List<SourceEstimation> resolveSources(@ApiParam(value = "Path to requested project", required = true)
@PathParam("path") String path) throws NotFoundException,
ForbiddenException,
ServerException,
ConflictException {
List<SourceEstimation> estimations = new ArrayList<>();
for (ProjectTypeResolution resolution : projectManager.resolveSources(path, false)) {
if (resolution.matched()) {
final HashMap<String, List<String>> attributes = new HashMap<>();
for (Map.Entry<String, Value> attr : resolution.getProvidedAttributes().entrySet()) {
attributes.put(attr.getKey(), attr.getValue().getList());
}
estimations.add(DtoFactory.newDto(SourceEstimation.class)
.withType(resolution.getType())
.withMatched(resolution.matched())
.withAttributes(attributes));
}
}
return estimations;
}
@POST
@Path("/import/{path:.*}")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
@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")
@ApiResponses({@ApiResponse(code = 204, 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")})
public void importProject(@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,
SourceStorageDto sourceStorage) throws ConflictException,
ForbiddenException,
UnauthorizedException,
IOException,
ServerException,
NotFoundException,
BadRequestException {
projectManager.importProject(path, sourceStorage, force,
() -> new ProjectImportOutputWSLineConsumer(path, workspace, 300));
}
@POST
@Path("/file/{parent:.*}")
@Consumes({MediaType.MEDIA_TYPE_WILDCARD})
@Produces({MediaType.APPLICATION_JSON})
@ApiOperation(value = "Create file",
notes = "Create a new file in a project. If file type isn't specified the server will resolve its type.")
@ApiResponses({@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")})
public Response createFile(@ApiParam(value = "Path to a target directory", required = true)
@PathParam("parent") String parentPath,
@ApiParam(value = "New file name", required = true)
@QueryParam("name") String fileName,
InputStream content) throws NotFoundException, ConflictException, ForbiddenException, ServerException {
final FolderEntry parent = projectManager.asFolder(parentPath);
if (parent == null) {
throw new NotFoundException("Parent not found for " + parentPath);
}
final FileEntry newFile = parent.createFile(fileName, content);
eventService.publish(new ProjectItemModifiedEvent(ProjectItemModifiedEvent.EventType.CREATED,
workspace,
newFile.getProject(),
newFile.getPath().toString(),
false));
final URI location = getServiceContext().getServiceUriBuilder().clone()
.path(getClass(), "getFile")
.build(new String[]{newFile.getPath().toString().substring(1)}, false);
return Response.created(location)
.entity(injectFileLinks(asDto(newFile)))
.build();
}
@POST
@Path("/folder/{path:.*}")
@Produces({MediaType.APPLICATION_JSON})
@ApiOperation(value = "Create a folder",
notes = "Create a folder is a specified project")
@ApiResponses({@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")})
public Response createFolder(@ApiParam(value = "Path to a new folder destination", required = true)
@PathParam("path") String path) throws ConflictException,
ForbiddenException,
ServerException,
NotFoundException {
final FolderEntry newFolder = projectManager.getProjectsRoot().createFolder(path);
final URI location = getServiceContext().getServiceUriBuilder().clone()
.path(getClass(), "getChildren")
.build(new String[]{newFolder.getPath().toString().substring(1)}, false);
eventService.publish(new ProjectItemModifiedEvent(ProjectItemModifiedEvent.EventType.CREATED,
workspace,
newFolder.getProject(),
newFolder.getPath().toString(),
true));
return Response.created(location)
.entity(injectFolderLinks(asDto(newFolder)))
.build();
}
@POST
@Path("/uploadfile/{parent:.*}")
@Consumes({MediaType.MULTIPART_FORM_DATA})
@Produces({MediaType.TEXT_HTML})
@ApiOperation(value = "Upload a file",
notes = "Upload a new file")
@ApiResponses({@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")})
public Response uploadFile(@ApiParam(value = "Destination path", required = true)
@PathParam("parent") String parentPath,
Iterator<FileItem> formData) throws NotFoundException,
ConflictException,
ForbiddenException,
ServerException {
final FolderEntry parent = projectManager.asFolder(parentPath);
if (parent == null) {
throw new NotFoundException("Parent not found for " + parentPath);
}
return uploadFile(parent.getVirtualFile(), formData);
}
@POST
@Path("/upload/zipfolder/{path:.*}")
@Consumes({MediaType.MULTIPART_FORM_DATA})
@Produces(MediaType.APPLICATION_JSON)
@ApiOperation(value = "Upload zip folder",
notes = "Upload folder from local zip",
response = Response.class)
@ApiResponses({@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")})
public Response uploadFolderFromZip(@ApiParam(value = "Path in the project", required = true)
@PathParam("path") String path,
Iterator<FileItem> formData) throws ServerException,
ConflictException,
ForbiddenException,
NotFoundException {
final FolderEntry parent = projectManager.asFolder(path);
if (parent == null) {
throw new NotFoundException("Parent not found for " + path);
}
return uploadZip(parent.getVirtualFile(), formData);
}
@ApiOperation(value = "Get file content",
notes = "Get file content by its name")
@ApiResponses({@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 = "Path to a file", required = true)
@PathParam("path") String path) throws IOException, NotFoundException, ForbiddenException, ServerException {
final FileEntry file = projectManager.asFile(path);
if (file == null) {
throw new NotFoundException("File not found for " + path);
}
return Response.ok().entity(file.getInputStream()).type(TIKA.detect(file.getName())).build();
}
@PUT
@Path("/file/{path:.*}")
@Consumes({MediaType.MEDIA_TYPE_WILDCARD})
@ApiOperation(value = "Update file",
notes = "Update an existing file with new content")
@ApiResponses({@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")})
public Response updateFile(@ApiParam(value = "Full path to a file", required = true)
@PathParam("path") String path,
InputStream content) throws NotFoundException, ForbiddenException, ServerException {
final FileEntry file = projectManager.asFile(path);
if (file == null) {
throw new NotFoundException("File not found for " + path);
}
file.updateContent(content);
eventService.publish(new ProjectItemModifiedEvent(ProjectItemModifiedEvent.EventType.UPDATED,
workspace,
file.getProject(),
file.getPath().toString(),
false));
return Response.ok().build();
}
@POST
@Path("/copy/{path:.*}")
@Consumes(MediaType.APPLICATION_JSON)
@ApiOperation(value = "Copy resource",
notes = "Copy resource to a new location which is specified in a query parameter")
@ApiResponses({@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")})
public Response copy(@ApiParam("Path to a resource") @PathParam("path") String path,
@ApiParam(value = "Path to a new location", required = true) @QueryParam("to") String newParent,
CopyOptions copyOptions) throws NotFoundException,
ForbiddenException,
ConflictException,
ServerException {
final VirtualFileEntry entry = projectManager.asVirtualFileEntry(path);
// used to indicate over write of destination
boolean isOverWrite = false;
// used to hold new name set in request body
String newName = entry.getName();
if (copyOptions != null) {
if (copyOptions.getOverWrite() != null) {
isOverWrite = copyOptions.getOverWrite();
}
if (copyOptions.getName() != null) {
newName = copyOptions.getName();
}
}
final VirtualFileEntry copy = projectManager.copyTo(path, newParent, newName, isOverWrite);
final URI location = getServiceContext().getServiceUriBuilder()
.path(getClass(), copy.isFile() ? "getFile" : "getChildren")
.build(new String[]{copy.getPath().toString().substring(1)}, false);
if (copy.isFolder()) {
try {
final RegisteredProject project = projectManager.getProject(copy.getPath().toString());
final String name = project.getName();
final String projectType = project.getProjectType().getId();
logProjectCreatedEvent(name, projectType);
} catch (NotFoundException ignore) {
}
}
return Response.created(location).build();
}
@POST
@Path("/move/{path:.*}")
@Consumes(MediaType.APPLICATION_JSON)
@ApiOperation(value = "Move resource",
notes = "Move resource to a new location which is specified in a query parameter")
@ApiResponses({@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")})
public Response move(@ApiParam("Path to a resource to be moved") @PathParam("path") String path,
@ApiParam("Path to a new location") @QueryParam("to") String newParent,
MoveOptions moveOptions) throws NotFoundException, ForbiddenException, ConflictException, ServerException {
final VirtualFileEntry entry = projectManager.asVirtualFileEntry(path);
// used to indicate over write of destination
boolean isOverWrite = false;
// used to hold new name set in request body
String newName = entry.getName();
if (moveOptions != null) {
if (moveOptions.getOverWrite() != null) {
isOverWrite = moveOptions.getOverWrite();
}
if (moveOptions.getName() != null) {
newName = moveOptions.getName();
}
}
final VirtualFileEntry move = projectManager.moveTo(path, newParent, newName, isOverWrite);
final URI location = getServiceContext().getServiceUriBuilder()
.path(getClass(), move.isFile() ? "getFile" : "getChildren")
.build(new String[]{move.getPath().toString().substring(1)}, false);
return Response.created(location).build();
}
@POST
@Path("/upload/zipproject/{path:.*}")
@Consumes({MediaType.MULTIPART_FORM_DATA})
@Produces(MediaType.APPLICATION_JSON)
@ApiOperation(value = "Upload zip project",
notes = "Upload project from local zip",
response = ProjectConfigDto.class)
@ApiResponses({@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")})
public List<SourceEstimation> uploadProjectFromZip(@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,
BadRequestException {
// 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
final FolderEntry baseProjectFolder = (FolderEntry)getVirtualFile(path, force);
int stripNumber = 0;
String projectName = "";
String projectDescription = "";
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 is expected. ");
}
} else {
switch (item.getFieldName()) {
case ("name"):
projectName = item.getString().trim();
break;
case ("description"):
projectDescription = item.getString().trim();
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);
}
return resolveSources(path);
}
@POST
@Path("/import/{path:.*}")
@Consumes(ExtMediaType.APPLICATION_ZIP)
@ApiOperation(value = "Import zip",
notes = "Import resources as zip")
@ApiResponses({@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")})
public Response importZip(@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 = projectManager.asFolder(path);
if (parent == null) {
throw new NotFoundException("Parent not found for " + path);
}
importZip(parent.getVirtualFile(), zip, true, skipFirstLevel);
try {
final RegisteredProject project = projectManager.getProject(path);
eventService.publish(new ProjectCreatedEvent(workspace, project.getPath()));
final String projectType = project.getProjectType().getId();
logProjectCreatedEvent(path, projectType);
} catch (NotFoundException ignore) {
}
return Response.created(getServiceContext().getServiceUriBuilder()
.path(getClass(), "getChildren")
.build(new String[]{parent.getPath().toString().substring(1)}, false)).build();
}
@GET
@Path("/export/{path:.*}")
@Produces(ExtMediaType.APPLICATION_ZIP)
@ApiOperation(value = "Download ZIP",
notes = "Export resource as zip. It can be an entire project or folder")
@ApiResponses({@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")})
public InputStream exportZip(@ApiParam(value = "Path to resource to be exported")
@PathParam("path") String path) throws NotFoundException, ForbiddenException, ServerException {
final FolderEntry folder = projectManager.asFolder(path);
if (folder == null) {
throw new NotFoundException("Folder not found " + path);
}
return folder.getVirtualFile().zip();
}
@GET
@Path("/export/file/{path:.*}")
@Produces(MediaType.APPLICATION_OCTET_STREAM)
public Response exportFile(@ApiParam(value = "Path to resource to be imported")
@PathParam("path") String path) throws NotFoundException, ForbiddenException, ServerException {
final FileEntry file = projectManager.asFile(path);
if (file == null) {
throw new NotFoundException("File not found " + path);
}
final VirtualFile virtualFile = file.getVirtualFile();
return Response.ok(virtualFile.getContent(), TIKA.detect(virtualFile.getName()))
.lastModified(new Date(virtualFile.getLastModificationDate()))
.header(HttpHeaders.CONTENT_LENGTH, Long.toString(virtualFile.getLength()))
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + virtualFile.getName() + '"')
.build();
}
@GET
@Path("/children/{parent:.*}")
@Produces(MediaType.APPLICATION_JSON)
@ApiOperation(value = "Get project children items",
notes = "Request all children items for a project, such as files and folders",
response = ItemReference.class,
responseContainer = "List")
@ApiResponses({@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")})
public List<ItemReference> getChildren(@ApiParam(value = "Path to a project", required = true)
@PathParam("parent") String path) throws NotFoundException,
ForbiddenException,
ServerException {
final FolderEntry folder = projectManager.asFolder(path);
if (folder == null) {
throw new NotFoundException("Parent not found for " + path);
}
final List<VirtualFileEntry> children = folder.getChildren();
final ArrayList<ItemReference> result = new ArrayList<>(children.size());
for (VirtualFileEntry child : children) {
if (child.isFile()) {
result.add(injectFileLinks(asDto((FileEntry)child)));
} else {
result.add(injectFolderLinks(asDto((FolderEntry)child)));
}
}
return result;
}
@GET
@Path("/tree/{parent:.*}")
@Produces(MediaType.APPLICATION_JSON)
@ApiOperation(value = "Get project tree",
notes = "Get project tree. Depth is specified in a query parameter",
response = TreeElement.class)
@ApiResponses({@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")})
public TreeElement getTree(@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,
@ApiParam(value = "include children files (in addition to children folders). This parameter can be dropped" +
". If not specified ?includeFiles=false is used by default")
@DefaultValue("false") @QueryParam("includeFiles") boolean includeFiles) throws NotFoundException,
ForbiddenException,
ServerException {
final FolderEntry folder = projectManager.asFolder(path);
if (folder == null) {
throw new NotFoundException("Folder " + path + " was not found");
}
return newDto(TreeElement.class).withNode(injectFolderLinks(asDto(folder)))
.withChildren(getTree(folder, depth, includeFiles));
}
@GET
@Path("/item/{path:.*}")
@Produces(MediaType.APPLICATION_JSON)
@ApiOperation(value = "Get file or folder",
response = ItemReference.class)
@ApiResponses({@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")})
public ItemReference getItem(@ApiParam(value = "Path to resource. Can be project or its folders", required = true)
@PathParam("path") String path) throws NotFoundException,
ForbiddenException,
ServerException {
final VirtualFileEntry entry = projectManager.getProjectsRoot().getChild(path);
if (entry == null) {
throw new NotFoundException("Project " + path + " was not found");
}
if (entry.isFile()) {
return injectFileLinks(asDto((FileEntry)entry));
} else {
return injectFolderLinks(asDto((FolderEntry)entry));
}
}
@GET
@Path("/search/{path:.*}")
@Produces(MediaType.APPLICATION_JSON)
@ApiOperation(value = "Search for resources",
notes = "Search for resources applying a number of search filters as query parameters",
response = ItemReference.class,
responseContainer = "List")
@ApiResponses({@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")})
public List<ItemReference> search(@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 = "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 {
final Searcher searcher;
try {
searcher = projectManager.getSearcher();
} catch (NotFoundException e) {
LOG.warn(e.getLocalizedMessage());
return Collections.emptyList();
}
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)
.setText(text)
.setMaxItems(maxItems)
.setSkipCount(skipCount);
final SearchResult result = searcher.search(expr);
final List<SearchResultEntry> searchResultEntries = result.getResults();
final List<ItemReference> items = new ArrayList<>(searchResultEntries.size());
final FolderEntry root = projectManager.getProjectsRoot();
for (SearchResultEntry searchResultEntry : searchResultEntries) {
final VirtualFileEntry child = root.getChild(searchResultEntry.getFilePath());
if (child != null && child.isFile()) {
items.add(injectFileLinks(asDto((FileEntry)child)));
}
}
return items;
}
private void logProjectCreatedEvent(@NotNull String projectName, @NotNull String projectType) {
LOG.info("EVENT#project-created# PROJECT#{}# TYPE#{}# WS#{}# USER#{}# PAAS#default#",
projectName,
projectType,
workspace,
EnvironmentContext.getCurrent().getSubject().getUserId());
}
private VirtualFileEntry getVirtualFile(String path, boolean force) throws ServerException,
ForbiddenException,
ConflictException,
NotFoundException {
final VirtualFileEntry virtualFile = projectManager.getProjectsRoot().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().createFolder(path);
} else if (!force) {
// Project already exists.
throw new ConflictException(String.format("Project with the name '%s' already exists.", path));
}
}
return virtualFile;
}
private List<TreeElement> getTree(FolderEntry folder,
int depth,
boolean includeFiles) throws ServerException, NotFoundException {
if (depth == 0) {
return null;
}
final List<? extends VirtualFileEntry> children;
if (includeFiles) {
children = folder.getChildFoldersFiles();
} else {
children = folder.getChildFolders();
}
final List<TreeElement> nodes = new ArrayList<>(children.size());
for (VirtualFileEntry child : children) {
if (child.isFolder()) {
nodes.add(newDto(TreeElement.class)
.withNode(injectFolderLinks(asDto((FolderEntry)child)))
.withChildren(getTree((FolderEntry)child, depth - 1, includeFiles)));
} else {
nodes.add(newDto(TreeElement.class).withNode(injectFileLinks(asDto((FileEntry)child))));
}
}
return nodes;
}
/* --------------------------------------------------------------------------- */
/* TODO check "upload" methods below, they were copied from old VFS as is */
/* --------------------------------------------------------------------------- */
private static Response uploadFile(VirtualFile parent, Iterator<FileItem> formData) throws ForbiddenException,
ConflictException,
ServerException {
try {
FileItem contentItem = null;
String name = null;
boolean overwrite = false;
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 if ("name".equals(item.getFieldName())) {
name = item.getString().trim();
} else if ("overwrite".equals(item.getFieldName())) {
overwrite = Boolean.parseBoolean(item.getString().trim());
}
}
if (contentItem == null) {
throw new ServerException("Cannot find file for upload. ");
}
if (name == null || name.isEmpty()) {
name = contentItem.getName();
}
try {
try {
parent.createFile(name, contentItem.getInputStream());
} catch (ConflictException e) {
if (!overwrite) {
throw new ConflictException("Unable upload file. Item with the same name exists. ");
}
parent.getChild(org.eclipse.che.api.vfs.Path.of(name)).updateContent(contentItem.getInputStream(), null);
}
} catch (IOException ioe) {
throw new ServerException(ioe.getMessage(), ioe);
}
return Response.ok("", MediaType.TEXT_HTML).build();
} catch (ForbiddenException | ConflictException | ServerException e) {
HtmlErrorFormatter.sendErrorAsHTML(e);
// never thrown
throw e;
}
}
private static Response uploadZip(VirtualFile parent, Iterator<FileItem> formData) throws ForbiddenException,
ConflictException,
ServerException {
try {
FileItem contentItem = null;
boolean overwrite = false;
boolean skipFirstLevel = false;
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 if ("overwrite".equals(item.getFieldName())) {
overwrite = Boolean.parseBoolean(item.getString().trim());
} else if ("skipFirstLevel".equals(item.getFieldName())) {
skipFirstLevel = Boolean.parseBoolean(item.getString().trim());
}
}
if (contentItem == null) {
throw new ServerException("Cannot find file for upload. ");
}
try {
importZip(parent, contentItem.getInputStream(), overwrite, skipFirstLevel);
} catch (IOException ioe) {
throw new ServerException(ioe.getMessage(), ioe);
}
return Response.ok("", MediaType.TEXT_HTML).build();
} catch (ForbiddenException | ConflictException | ServerException e) {
HtmlErrorFormatter.sendErrorAsHTML(e);
// never thrown
throw e;
}
}
private static void importZip(VirtualFile parent, InputStream in, boolean overwrite, boolean skipFirstLevel) throws ForbiddenException,
ConflictException,
ServerException {
int stripNum = skipFirstLevel ? 1 : 0;
parent.unzip(in, overwrite, stripNum);
}
private ItemReference injectFileLinks(ItemReference itemReference) {
return projectServiceLinksInjector.injectFileLinks(itemReference, getServiceContext());
}
private ItemReference injectFolderLinks(ItemReference itemReference) {
return projectServiceLinksInjector.injectFolderLinks(itemReference, getServiceContext());
}
private ProjectConfigDto injectProjectLinks(ProjectConfigDto projectConfig) {
return projectServiceLinksInjector.injectProjectLinks(projectConfig, getServiceContext());
}
}