/******************************************************************************* * 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.factory.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 com.google.common.collect.ImmutableSet; import com.google.gson.JsonSyntaxException; import org.apache.commons.fileupload.FileItem; import org.eclipse.che.api.agent.server.filters.AddExecAgentInEnvironmentUtil; import org.eclipse.che.api.core.ApiException; 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.model.factory.Factory; import org.eclipse.che.api.core.model.project.ProjectConfig; import org.eclipse.che.api.core.model.user.User; import org.eclipse.che.api.core.rest.Service; import org.eclipse.che.api.factory.server.builder.FactoryBuilder; import org.eclipse.che.api.factory.shared.dto.AuthorDto; import org.eclipse.che.api.factory.shared.dto.FactoryDto; import org.eclipse.che.api.user.server.PreferenceManager; import org.eclipse.che.api.user.server.UserManager; import org.eclipse.che.api.workspace.server.WorkspaceManager; import org.eclipse.che.api.workspace.server.model.impl.ProjectConfigImpl; import org.eclipse.che.api.workspace.server.model.impl.WorkspaceImpl; import org.eclipse.che.commons.env.EnvironmentContext; import org.eclipse.che.commons.lang.NameGenerator; import org.eclipse.che.commons.lang.Pair; import org.eclipse.che.commons.lang.URLEncodedUtils; import org.eclipse.che.dto.server.DtoFactory; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.inject.Inject; 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.Response; import javax.ws.rs.core.UriInfo; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import java.util.function.Predicate; import static com.google.common.base.Strings.isNullOrEmpty; import static java.lang.Boolean.parseBoolean; import static java.util.stream.Collectors.toList; import static javax.ws.rs.core.HttpHeaders.CONTENT_DISPOSITION; import static javax.ws.rs.core.MediaType.APPLICATION_JSON; import static javax.ws.rs.core.MediaType.MULTIPART_FORM_DATA; import static javax.ws.rs.core.MediaType.TEXT_PLAIN; import static org.eclipse.che.api.factory.server.FactoryLinksHelper.createLinks; /** * Defines Factory REST API. * * @author Anton Korneta * @author Florent Benoit */ @Api(value = "/factory", description = "Factory manager") @Path("/factory") public class FactoryService extends Service { private static final Logger LOG = LoggerFactory.getLogger(FactoryService.class); /** * Error message if there is no plugged resolver. */ public static final String ERROR_NO_RESOLVER_AVAILABLE = "Cannot build factory with any of the provided parameters."; /** * Validate query parameter. If true, factory will be validated */ public static final String VALIDATE_QUERY_PARAMETER = "validate"; /** * Set of resolvers for factories. Injected through an holder. */ private final Set<FactoryParametersResolver> factoryParametersResolvers; private final FactoryManager factoryManager; private final UserManager userManager; private final PreferenceManager preferenceManager; private final FactoryEditValidator editValidator; private final FactoryCreateValidator createValidator; private final FactoryAcceptValidator acceptValidator; private final FactoryBuilder factoryBuilder; private final WorkspaceManager workspaceManager; @Inject public FactoryService(FactoryManager factoryManager, UserManager userManager, PreferenceManager preferenceManager, FactoryCreateValidator createValidator, FactoryAcceptValidator acceptValidator, FactoryEditValidator editValidator, FactoryBuilder factoryBuilder, WorkspaceManager workspaceManager, FactoryParametersResolverHolder factoryParametersResolverHolder) { this.factoryManager = factoryManager; this.userManager = userManager; this.createValidator = createValidator; this.preferenceManager = preferenceManager; this.acceptValidator = acceptValidator; this.editValidator = editValidator; this.factoryBuilder = factoryBuilder; this.workspaceManager = workspaceManager; this.factoryParametersResolvers = factoryParametersResolverHolder.getFactoryParametersResolvers(); } /** * @deprecated this is a legacy method for functionality that is no longer exists. * use {@link #saveFactory(FactoryDto)} */ @POST @Consumes(MULTIPART_FORM_DATA) @Produces(APPLICATION_JSON) @ApiOperation(value = "Create a new factory based on configuration and factory images", notes = "The field 'factory' is required") @ApiResponses({@ApiResponse(code = 200, message = "Factory successfully created"), @ApiResponse(code = 400, message = "Missed required parameters, parameters are not valid"), @ApiResponse(code = 403, message = "The user does not have rights to create factory"), @ApiResponse(code = 409, message = "When factory with given name and creator already exists"), @ApiResponse(code = 500, message = "Internal server error occurred")}) @Deprecated public FactoryDto saveFactory(Iterator<FileItem> formData) throws ForbiddenException, ConflictException, BadRequestException, ServerException { try { final Set<FactoryImage> images = new HashSet<>(); FactoryDto factory = null; while (formData.hasNext()) { final FileItem item = formData.next(); switch (item.getFieldName()) { case ("factory"): { try (InputStream factoryData = item.getInputStream()) { factory = factoryBuilder.build(factoryData); } catch (JsonSyntaxException ex) { throw new BadRequestException("Invalid JSON value of the field 'factory' provided"); } break; } case ("image"): { try (InputStream imageData = item.getInputStream()) { final FactoryImage image = createImage(imageData, item.getContentType(), NameGenerator.generate(null, 16)); if (image.hasContent()) { images.add(image); } } break; } default: //DO NOTHING } } requiredNotNull(factory, "factory configuration"); processDefaults(factory); AddExecAgentInEnvironmentUtil.addExecAgent(factory.getWorkspace()); createValidator.validateOnCreate(factory); return injectLinks(asDto(factoryManager.saveFactory(factory, images)), images); } catch (IOException ioEx) { throw new ServerException(ioEx.getLocalizedMessage(), ioEx); } } @POST @Consumes(APPLICATION_JSON) @Produces(APPLICATION_JSON) @ApiOperation(value = "Create a new factory based on configuration", notes = "Factory will be created without images") @ApiResponses({@ApiResponse(code = 200, message = "Factory successfully created"), @ApiResponse(code = 400, message = "Missed required parameters, parameters are not valid"), @ApiResponse(code = 403, message = "User does not have rights to create factory"), @ApiResponse(code = 409, message = "When factory with given name and creator already exists"), @ApiResponse(code = 500, message = "Internal server error occurred")}) public FactoryDto saveFactory(FactoryDto factory) throws BadRequestException, ServerException, ForbiddenException, ConflictException { requiredNotNull(factory, "Factory configuration"); factoryBuilder.checkValid(factory); processDefaults(factory); createValidator.validateOnCreate(factory); return injectLinks(asDto(factoryManager.saveFactory(factory)), null); } @GET @Path("/{id}") @Produces(APPLICATION_JSON) @ApiOperation(value = "Get factory by its identifier", notes = "If validate parameter is not specified, retrieved factory wont be validated") @ApiResponses({@ApiResponse(code = 200, message = "Response contains requested factory entry"), @ApiResponse(code = 400, message = "Missed required parameters, failed to validate factory"), @ApiResponse(code = 404, message = "Factory with specified identifier does not exist"), @ApiResponse(code = 500, message = "Internal server error occurred")}) public FactoryDto getFactory(@ApiParam(value = "Factory identifier") @PathParam("id") String factoryId, @ApiParam(value = "Whether or not to validate values like it is done when accepting the factory", allowableValues = "true, false", defaultValue = "false") @DefaultValue("false") @QueryParam("validate") Boolean validate) throws BadRequestException, NotFoundException, ServerException { final FactoryDto factoryDto = asDto(factoryManager.getById(factoryId)); if (validate) { acceptValidator.validateOnAccept(factoryDto); } return injectLinks(factoryDto, factoryManager.getFactoryImages(factoryId)); } @GET @Path("/find") @Produces(APPLICATION_JSON) @ApiOperation(value = "Get factory by attribute, " + "the attribute must match one of the Factory model fields with type 'String', " + "e.g. (factory.name, factory.creator.name)", notes = "If specify more than one value for a single query parameter then will be taken the first one") @ApiResponses({@ApiResponse(code = 200, message = "Response contains list requested factories"), @ApiResponse(code = 400, message = "When query does not contain at least one attribute to search for"), @ApiResponse(code = 500, message = "Internal server error")}) public List<FactoryDto> getFactoryByAttribute(@DefaultValue("0") @QueryParam("skipCount") Integer skipCount, @DefaultValue("30") @QueryParam("maxItems") Integer maxItems, @Context UriInfo uriInfo) throws BadRequestException, ServerException { final Set<String> skip = ImmutableSet.of("token", "skipCount", "maxItems"); final List<Pair<String, String>> query = URLEncodedUtils.parse(uriInfo.getRequestUri()) .entrySet() .stream() .filter(param -> !skip.contains(param.getKey()) && !param.getValue().isEmpty()) .map(entry -> Pair.of(entry.getKey(), entry.getValue() .iterator() .next())) .collect(toList()); checkArgument(!query.isEmpty(), "Query must contain at least one attribute"); final List<FactoryDto> factories = new ArrayList<>(); for (Factory factory : factoryManager.getByAttribute(maxItems, skipCount, query)) { factories.add(injectLinks(asDto(factory), null)); } return factories; } @PUT @Path("/{id}") @Consumes(APPLICATION_JSON) @Produces(APPLICATION_JSON) @ApiOperation(value = "Update factory information by configuration and specified identifier", notes = "Update factory based on the factory id which is passed in a path parameter. " + "For perform this operation user needs respective rights") @ApiResponses({@ApiResponse(code = 200, message = "Factory successfully updated"), @ApiResponse(code = 400, message = "Missed required parameters, parameters are not valid"), @ApiResponse(code = 403, message = "User does not have rights to update factory"), @ApiResponse(code = 404, message = "Factory to update not found"), @ApiResponse(code = 409, message = "Conflict error occurred during factory update" + "(e.g. Factory with such name and creator already exists)"), @ApiResponse(code = 500, message = "Internal server error")}) public FactoryDto updateFactory(@ApiParam(value = "Factory identifier") @PathParam("id") String factoryId, FactoryDto update) throws BadRequestException, NotFoundException, ServerException, ForbiddenException, ConflictException { requiredNotNull(update, "Factory configuration"); update.setId(factoryId); final Factory existing = factoryManager.getById(factoryId); // check if the current user has enough access to edit the factory editValidator.validate(existing); factoryBuilder.checkValid(update, true); // validate the new content createValidator.validateOnCreate(update); return injectLinks(asDto(factoryManager.updateFactory(update)), factoryManager.getFactoryImages(factoryId)); } @DELETE @Path("/{id}") @ApiOperation(value = "Removes factory by its identifier", notes = "Removes factory based on the factory id which is passed in a path parameter. " + "For perform this operation user needs respective rights") @ApiResponses({@ApiResponse(code = 200, message = "Factory successfully removed"), @ApiResponse(code = 403, message = "User not authorized to call this operation"), @ApiResponse(code = 404, message = "Factory not found"), @ApiResponse(code = 500, message = "Internal server error")}) public void removeFactory(@ApiParam(value = "Factory identifier") @PathParam("id") String id) throws ForbiddenException, ServerException { factoryManager.removeFactory(id); } /** * @deprecated this is a legacy method for functionality that is no longer exists. * There is no alternative for this method. */ @GET @Path("/{id}/image") @Produces("image/*") @ApiOperation(value = "Get factory image", notes = "If image identifier is not specified then first found image will be returned") @ApiResponses({@ApiResponse(code = 200, message = "Response contains requested factory image"), @ApiResponse(code = 400, message = "Missed required parameters, parameters are not valid"), @ApiResponse(code = 404, message = "Factory or factory image not found"), @ApiResponse(code = 500, message = "Internal server error")}) @Deprecated public Response getImage(@ApiParam(value = "Factory identifier") @PathParam("id") String factoryId, @ApiParam(value = "Image identifier") @QueryParam("imgId") String imageId) throws NotFoundException, BadRequestException, ServerException { final Set<FactoryImage> images; if (isNullOrEmpty(imageId)) { if ((images = factoryManager.getFactoryImages(factoryId)).isEmpty()) { LOG.warn("Default image for factory {} is not found.", factoryId); throw new NotFoundException("Default image for factory " + factoryId + " is not found."); } } else { if ((images = factoryManager.getFactoryImages(factoryId, imageId)).isEmpty()) { LOG.warn("Image with id {} is not found.", imageId); throw new NotFoundException("Image with id " + imageId + " is not found."); } } final FactoryImage image = images.iterator().next(); return Response.ok(image.getImageData(), image.getMediaType()).build(); } /** * @deprecated this is a legacy method for functionality that is no longer exists. * There is no alternative for this method. */ @GET @Path("/{id}/snippet") @Produces(TEXT_PLAIN) @ApiOperation(value = "Get factory snippet", notes = "If snippet type is not specified then default 'url' will be used") @ApiResponses({@ApiResponse(code = 200, message = "Response contains requested factory snippet"), @ApiResponse(code = 400, message = "Missed required parameters, parameters are not valid"), @ApiResponse(code = 404, message = "Factory or factory snippet not found"), @ApiResponse(code = 500, message = "Internal server error")}) @Deprecated public String getFactorySnippet(@ApiParam(value = "Factory identifier") @PathParam("id") String factoryId, @ApiParam(value = "Snippet type", required = true, allowableValues = "url, html, iframe, markdown", defaultValue = "url") @DefaultValue("url") @QueryParam("type") String type) throws NotFoundException, BadRequestException, ServerException { final String factorySnippet = factoryManager.getFactorySnippet(factoryId, type, uriInfo.getBaseUri()); checkArgument(factorySnippet != null, "Snippet type \"" + type + "\" is unsupported."); return factorySnippet; } @GET @Path("/workspace/{ws-id}") @Produces(APPLICATION_JSON) @ApiOperation(value = "Construct factory from workspace", notes = "This call returns a Factory.json that is used to create a factory") @ApiResponses({@ApiResponse(code = 200, message = "Response contains requested factory JSON"), @ApiResponse(code = 400, message = "Missed required parameters, parameters are not valid"), @ApiResponse(code = 404, message = "Workspace not found"), @ApiResponse(code = 500, message = "Internal server error")}) public Response getFactoryJson(@ApiParam(value = "Workspace identifier") @PathParam("ws-id") String wsId, @ApiParam(value = "Project path") @QueryParam("path") String path) throws BadRequestException, NotFoundException, ServerException { final WorkspaceImpl workspace = workspaceManager.getWorkspace(wsId); excludeProjectsWithoutLocation(workspace, path); final FactoryDto factoryDto = DtoFactory.newDto(FactoryDto.class) .withV("4.0") .withWorkspace(org.eclipse.che.api.workspace.server.DtoConverter .asDto(workspace.getConfig())); return Response.ok(factoryDto, APPLICATION_JSON) .header(CONTENT_DISPOSITION, "attachment; filename=factory.json") .build(); } @POST @Path("/resolver") @Consumes(APPLICATION_JSON) @Produces(APPLICATION_JSON) @ApiOperation(value = "Create factory by providing map of parameters", notes = "Get JSON with factory information") @ApiResponses({@ApiResponse(code = 200, message = "Factory successfully built from parameters"), @ApiResponse(code = 400, message = "Missed required parameters, failed to validate factory"), @ApiResponse(code = 500, message = "Internal server error")}) public FactoryDto resolveFactory(@ApiParam(value = "Parameters provided to create factories") Map<String, String> parameters, @ApiParam(value = "Whether or not to validate values like it is done when accepting a Factory", allowableValues = "true,false", defaultValue = "false") @DefaultValue("false") @QueryParam(VALIDATE_QUERY_PARAMETER) Boolean validate) throws ServerException, BadRequestException { // check parameter requiredNotNull(parameters, "Factory build parameters"); // search matching resolver and create factory from matching resolver for (FactoryParametersResolver resolver : factoryParametersResolvers) { if (resolver.accept(parameters)) { final FactoryDto factory = resolver.createFactory(parameters); if (validate) { acceptValidator.validateOnAccept(factory); } return injectLinks(factory, null); } } // no match throw new BadRequestException(ERROR_NO_RESOLVER_AVAILABLE); } /** * Injects factory links. If factory is named then accept named link will be injected, * if {@code images} is not null and not empty then image links will be injected */ private FactoryDto injectLinks(FactoryDto factory, Set<FactoryImage> images) { String username = null; if (factory.getCreator() != null && factory.getCreator().getUserId() != null) { try { username = userManager.getById(factory.getCreator().getUserId()).getName(); } catch (ApiException ignored) { // when impossible to get username then named factory link won't be injected } } return factory.withLinks(images != null && !images.isEmpty() ? createLinks(factory, images, getServiceContext(), username) : createLinks(factory, getServiceContext(), username)); } /** * Filters workspace projects and removes projects without source location. * If there is no at least one project with source location then {@link BadRequestException} will be thrown */ private static void excludeProjectsWithoutLocation(WorkspaceImpl usersWorkspace, String projectPath) throws BadRequestException { final boolean notEmptyPath = projectPath != null; //Condition for sifting valid project in user's workspace Predicate<ProjectConfig> predicate = projectConfig -> { // if project is a sub project (it's path contains another project) , then location can be null final boolean isSubProject = projectConfig.getPath().indexOf('/', 1) != -1; final boolean hasNotEmptySource = projectConfig.getSource() != null && projectConfig.getSource().getType() != null && projectConfig.getSource().getLocation() != null; return !(notEmptyPath && !projectPath.equals(projectConfig.getPath())) && (isSubProject || hasNotEmptySource); }; // Filtered out projects by path and source storage presence final List<ProjectConfigImpl> filtered = usersWorkspace.getConfig() .getProjects() .stream() .filter(predicate) .collect(toList()); checkArgument(!filtered.isEmpty(), "Unable to create factory from this workspace, " + "because it does not contains projects with source storage"); usersWorkspace.getConfig().setProjects(filtered); } /** * Checks the current user if it is not temporary then * adds to the factory creator information and time of creation */ private void processDefaults(FactoryDto factory) throws ForbiddenException { try { final String userId = EnvironmentContext.getCurrent().getSubject().getUserId(); final User user = userManager.getById(userId); if (user == null || parseBoolean(preferenceManager.find(userId).get("temporary"))) { throw new ForbiddenException("Current user is not allowed to use this method."); } factory.setCreator(DtoFactory.newDto(AuthorDto.class) .withUserId(userId) .withName(user.getName()) .withEmail(user.getEmail()) .withCreated(System.currentTimeMillis())); } catch (NotFoundException | ServerException ex) { throw new ForbiddenException("Current user is not allowed to use this method"); } } /** * Converts {@link Factory} to dto object */ private FactoryDto asDto(Factory factory) throws ServerException { try { return DtoConverter.asDto(factory, userManager.getById(factory.getCreator().getUserId())); } catch (ServerException | NotFoundException ex) { throw new ServerException("Failed to retrieve factory creator"); } } /** * Usage of a dedicated class to manage the optional resolvers */ protected static class FactoryParametersResolverHolder { /** * Optional inject for the resolvers. */ @com.google.inject.Inject(optional = true) private Set<FactoryParametersResolver> factoryParametersResolvers; /** * Provides the set of resolvers if there are some else return an empty set. * * @return a non null set */ public Set<FactoryParametersResolver> getFactoryParametersResolvers() { if (factoryParametersResolvers != null) { return factoryParametersResolvers; } else { return Collections.emptySet(); } } } /** * Creates factory image from input stream. * InputStream should be closed manually. * * @param is * input stream with image data * @param mediaType * media type of image * @param name * image name * @return factory image, if {@param is} has no content then empty factory image will be returned * @throws BadRequestException * when factory image exceeded maximum size * @throws ServerException * when any server errors occurs */ public static FactoryImage createImage(InputStream is, String mediaType, String name) throws BadRequestException, ServerException { try { final ByteArrayOutputStream out = new ByteArrayOutputStream(); final byte[] buffer = new byte[1024]; int read; while ((read = is.read(buffer, 0, buffer.length)) != -1) { out.write(buffer, 0, read); if (out.size() > 1024 * 1024) { throw new BadRequestException("Maximum upload size exceeded."); } } if (out.size() == 0) { return new FactoryImage(); } out.flush(); return new FactoryImage(out.toByteArray(), mediaType, name); } catch (IOException ioEx) { throw new ServerException(ioEx.getLocalizedMessage()); } } /** * Checks object reference is not {@code null} * * @param object * object reference to check * @param subject * used as subject of exception message "{subject} required" * @throws BadRequestException * when object reference is {@code null} */ private static void requiredNotNull(Object object, String subject) throws BadRequestException { if (object == null) { throw new BadRequestException(subject + " required"); } } /** * Checks that expression is true, throws {@link BadRequestException} otherwise. * * <p>Exception uses error message built from error message template and error message parameters. */ private static void checkArgument(boolean expression, String errorMessage) throws BadRequestException { if (!expression) { throw new BadRequestException(errorMessage); } } }