org.eclipse.che.api.factory.server.FactoryService.java Source code

Java tutorial

Introduction

Here is the source code for org.eclipse.che.api.factory.server.FactoryService.java

Source

/*******************************************************************************
 * 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();
    }

    @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") })
    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);
    }

    @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") })
    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();
    }

    @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") })
    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);
        }
    }
}