com.streamsets.datacollector.restapi.PreviewResource.java Source code

Java tutorial

Introduction

Here is the source code for com.streamsets.datacollector.restapi.PreviewResource.java

Source

/*
 * Copyright 2017 StreamSets Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.streamsets.datacollector.restapi;

import com.google.common.collect.ImmutableMap;
import com.streamsets.datacollector.blobstore.BlobStoreTask;
import com.streamsets.datacollector.config.PipelineConfiguration;
import com.streamsets.datacollector.dynamicpreview.DynamicPreviewConstants;
import com.streamsets.datacollector.dynamicpreview.DynamicPreviewRequestJson;
import com.streamsets.datacollector.dynamicpreview.DynamicPreviewType;
import com.streamsets.datacollector.event.binding.MessagingDtoJsonMapper;
import com.streamsets.datacollector.event.binding.MessagingJsonToFromDto;
import com.streamsets.datacollector.event.dto.EventType;
import com.streamsets.datacollector.event.dto.PipelinePreviewEvent;
import com.streamsets.datacollector.event.handler.EventHandlerTask;
import com.streamsets.datacollector.event.handler.remote.RemoteDataCollectorResult;
import com.streamsets.datacollector.event.json.DynamicPreviewEventJson;
import com.streamsets.datacollector.event.json.EventJson;
import com.streamsets.datacollector.execution.AclManager;
import com.streamsets.datacollector.execution.Manager;
import com.streamsets.datacollector.execution.PreviewOutput;
import com.streamsets.datacollector.execution.PreviewStatus;
import com.streamsets.datacollector.execution.Previewer;
import com.streamsets.datacollector.execution.RawPreview;
import com.streamsets.datacollector.execution.preview.common.PreviewError;
import com.streamsets.datacollector.json.ObjectMapperFactory;
import com.streamsets.datacollector.main.RuntimeInfo;
import com.streamsets.datacollector.main.UserGroupManager;
import com.streamsets.datacollector.restapi.bean.BeanHelper;
import com.streamsets.datacollector.restapi.bean.DynamicPreviewRequestWithOverridesJson;
import com.streamsets.datacollector.restapi.bean.PreviewInfoJson;
import com.streamsets.datacollector.restapi.bean.PreviewOutputJson;
import com.streamsets.datacollector.restapi.bean.StageOutputJson;
import com.streamsets.datacollector.restapi.bean.UserJson;
import com.streamsets.datacollector.runner.PipelineRuntimeException;
import com.streamsets.datacollector.store.AclStoreTask;
import com.streamsets.datacollector.store.PipelineInfo;
import com.streamsets.datacollector.store.PipelineStoreTask;
import com.streamsets.datacollector.util.AuthzRole;
import com.streamsets.datacollector.util.Configuration;
import com.streamsets.datacollector.util.ContainerError;
import com.streamsets.datacollector.util.EdgeUtil;
import com.streamsets.datacollector.util.PipelineException;
import com.streamsets.lib.security.http.ForbiddenException;
import com.streamsets.lib.security.http.RemoteSSOService;
import com.streamsets.lib.security.http.RestClient;
import com.streamsets.lib.security.http.SSOPrincipal;
import com.streamsets.pipeline.api.Config;
import com.streamsets.pipeline.api.StageException;
import com.streamsets.pipeline.api.impl.Utils;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import io.swagger.annotations.Authorization;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.annotation.security.DenyAll;
import javax.annotation.security.RolesAllowed;
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.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.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.security.Principal;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

@Path("/v1")
@Api(value = "preview")
@DenyAll
@RequiresCredentialsDeployed
public class PreviewResource {
    private static final Logger LOG = LoggerFactory.getLogger(PreviewResource.class);
    private static final String MAX_BATCH_SIZE_KEY = "preview.maxBatchSize";
    private static final int MAX_BATCH_SIZE_DEFAULT = 10;
    private static final String MAX_BATCHES_KEY = "preview.maxBatches";
    private static final int MAX_BATCHES_DEFAULT = 10;

    //TODO: look into avoiding duplicating constants with DPM (com.streamsets.apps.common.Roles and ClassificationRoles)
    private static final List<String> DYNAMIC_PREVIEW_ALLOWED_ROLES_CLASSIFICATION = Arrays.asList(
            // classification administrator (SDP specific role)
            "classification:admin",
            // system administrator (DPM common role)
            "sys-admin",
            // organization administrator (DPM common role)
            "org-admin");

    private static final int DYNAMIC_PREVIEW_MAX_COUNT = 100;

    private final Manager manager;
    private final PipelineStoreTask store;
    private final Configuration configuration;
    private final String user;
    private final EventHandlerTask eventHandlerTask;
    private final PipelineStoreTask pipelineStoreTask;
    private final BlobStoreTask blobStoreTask;
    private final RuntimeInfo runtimeInfo;
    private final UserJson currentUser;

    @Inject
    public PreviewResource(Manager manager, Configuration configuration, Principal principal,
            PipelineStoreTask store, AclStoreTask aclStore, RuntimeInfo runtimeInfo,
            UserGroupManager userGroupManager, EventHandlerTask eventHandlerTask,
            PipelineStoreTask pipelineStoreTask, BlobStoreTask blobStoreTask) {
        this.configuration = configuration;
        this.user = principal.getName();
        this.store = store;
        this.eventHandlerTask = eventHandlerTask;
        this.pipelineStoreTask = pipelineStoreTask;
        this.blobStoreTask = blobStoreTask;

        if (runtimeInfo.isDPMEnabled()) {
            currentUser = new UserJson((SSOPrincipal) principal);
        } else {
            currentUser = userGroupManager.getUser(principal);
        }

        if (runtimeInfo.isAclEnabled()) {
            this.manager = new AclManager(manager, aclStore, currentUser);
        } else {
            this.manager = manager;
        }

        this.runtimeInfo = runtimeInfo;
    }

    @Path("/pipeline/{pipelineId}/preview")
    @POST
    @ApiOperation(value = "Run Pipeline preview", response = PreviewInfoJson.class, authorizations = @Authorization(value = "basic"))
    @Produces(MediaType.APPLICATION_JSON)
    @Consumes(MediaType.APPLICATION_JSON)
    @RolesAllowed({ AuthzRole.CREATOR, AuthzRole.ADMIN, AuthzRole.CREATOR_REMOTE, AuthzRole.ADMIN_REMOTE,
            AuthzRole.MANAGER, AuthzRole.MANAGER_REMOTE })
    public Response previewWithOverride(@PathParam("pipelineId") String pipelineId, @QueryParam("rev") String rev,
            @QueryParam("batchSize") @DefaultValue("" + Integer.MAX_VALUE) int batchSize,
            @QueryParam("batches") @DefaultValue("1") int batches,
            @QueryParam("skipTargets") @DefaultValue("true") boolean skipTargets,
            @QueryParam("skipLifecycleEvents") @DefaultValue("true") boolean skipLifecycleEvents,
            @QueryParam("endStage") String endStageInstanceName,
            @QueryParam("timeout") @DefaultValue("2000") long timeout,
            @QueryParam("edge") @DefaultValue("false") boolean edge,
            @QueryParam("testOrigin") @DefaultValue("false") boolean testOrigin,
            @ApiParam(name = "stageOutputsToOverrideJson", required = true) List<StageOutputJson> stageOutputsToOverrideJson)
            throws PipelineException {
        if (stageOutputsToOverrideJson == null) {
            stageOutputsToOverrideJson = Collections.emptyList();
        }

        if (edge) {
            PipelineConfiguration pipelineConfiguration = store.load(pipelineId, "0");
            Config edgeHttpUrlConfig = pipelineConfiguration.getConfiguration(EdgeUtil.EDGE_HTTP_URL);
            if (edgeHttpUrlConfig != null) {
                EdgeUtil.publishEdgePipeline(pipelineConfiguration, null);
                Map<String, Object> params = new HashMap<>();
                params.put("bathces", batches);
                params.put("batchSize", batchSize);
                params.put("skipTargets", skipTargets);
                params.put("endStage", endStageInstanceName);
                params.put("timeout", timeout);
                params.put("testOrigin", testOrigin);
                return EdgeUtil.proxyRequestPOST((String) edgeHttpUrlConfig.getValue(),
                        "/rest/v1/pipeline/" + pipelineId + "/preview", params, stageOutputsToOverrideJson);
            }
        }

        return startPreviewer(pipelineId, rev, batchSize, batches, skipTargets, skipLifecycleEvents,
                endStageInstanceName, timeout, testOrigin, stageOutputsToOverrideJson);
    }

    private Response startPreviewer(String pipelineId, String rev, int batchSize, int batches, boolean skipTargets,
            boolean skipLifecycleEvents, String endStageInstanceName, long timeout, boolean testOrigin,
            List<StageOutputJson> stageOutputsToOverrideJson) throws PipelineException {
        PipelineInfo pipelineInfo = store.getInfo(pipelineId);
        RestAPIUtils.injectPipelineInMDC(pipelineInfo.getTitle(), pipelineInfo.getPipelineId());
        int maxBatchSize = configuration.get(MAX_BATCH_SIZE_KEY, MAX_BATCH_SIZE_DEFAULT);
        batchSize = Math.min(maxBatchSize, batchSize);
        int maxBatches = configuration.get(MAX_BATCHES_KEY, MAX_BATCHES_DEFAULT);
        batches = Math.min(maxBatches, batches);

        Previewer previewer = manager.createPreviewer(this.user, pipelineId, rev, Collections.emptyList(),
                p -> null);
        try {
            previewer.start(batches, batchSize, skipTargets, skipLifecycleEvents, endStageInstanceName,
                    BeanHelper.unwrapStageOutput(stageOutputsToOverrideJson), timeout, testOrigin);
            PreviewInfoJson previewInfoJson = new PreviewInfoJson(previewer.getId(), previewer.getStatus());
            return Response.ok().type(MediaType.APPLICATION_JSON).entity(previewInfoJson).build();
        } catch (PipelineRuntimeException ex) {
            if (ex.getErrorCode() == ContainerError.CONTAINER_0165) {
                return Response.status(Response.Status.BAD_REQUEST).type(MediaType.APPLICATION_JSON)
                        .entity(BeanHelper.wrapIssues(ex.getIssues())).build();
            } else {
                throw ex;
            }
        }
    }

    @Path("/pipeline/dynamicPreview")
    @POST
    @ApiOperation(value = "Run dynamic pipeline preview", response = PreviewInfoJson.class, authorizations = @Authorization(value = "basic"))
    @Produces(MediaType.APPLICATION_JSON)
    @Consumes(MediaType.APPLICATION_JSON)
    @RolesAllowed({ AuthzRole.CREATOR, AuthzRole.ADMIN, AuthzRole.CREATOR_REMOTE, AuthzRole.ADMIN_REMOTE,
            AuthzRole.MANAGER, AuthzRole.MANAGER_REMOTE })
    public Response initiateDynamicPreview(
            @ApiParam(name = "dynamicPreviewRequest", required = true) DynamicPreviewRequestWithOverridesJson dynamicPreviewWithOverridesRequest)
            throws StageException, IOException {
        Utils.checkNotNull(dynamicPreviewWithOverridesRequest, "dynamicPreviewWithOverridesRequest");
        final DynamicPreviewRequestJson dynamicPreviewRequest = dynamicPreviewWithOverridesRequest
                .getDynamicPreviewRequestJson();

        Utils.checkNotNull(dynamicPreviewRequest, "dynamicPreviewRequestJson");

        checkDynamicPreviewPermissions(dynamicPreviewRequest);

        // TODO: fix this hack?
        final String orgId = StringUtils.substringAfterLast(currentUser.getName(), "@");
        final Map<String, Object> params = dynamicPreviewRequest.getParameters();
        params.put(DynamicPreviewConstants.REQUESTING_USER_ID_PARAMETER, currentUser.getName());
        params.put(DynamicPreviewConstants.REQUESTING_USER_ORG_ID_PARAMETER, orgId);

        final String controlHubBaseUrl = RemoteSSOService.getValidURL(
                configuration.get(RemoteSSOService.DPM_BASE_URL_CONFIG, RemoteSSOService.DPM_BASE_URL_DEFAULT));

        // serialize the stage overrides to a String, to be passed to Control Hub (see Javadocs in DynamicPreviewRequestJson
        // for a more complete explanation)
        dynamicPreviewRequest.setStageOutputsToOverrideJsonText(ObjectMapperFactory.get()
                .writeValueAsString(dynamicPreviewWithOverridesRequest.getStageOutputsToOverrideJson()));

        final DynamicPreviewEventJson dynamicPreviewEvent = getDynamicPreviewEvent(controlHubBaseUrl,
                "/dynamic_preview/rest/v2/dynamic_preview/createDynamicPreviewEvent", runtimeInfo.getAppAuthToken(),
                runtimeInfo.getId(), dynamicPreviewRequest);

        String generatedPipelineId = null;
        try {
            generatedPipelineId = handlePreviewEvents("before", dynamicPreviewEvent.getBeforeActionsEventTypeIds(),
                    dynamicPreviewEvent.getBeforeActions());
        } catch (IOException e) {
            throw new StageException(PreviewError.PREVIEW_0101, "before", e.getMessage(), e);
        }

        if (generatedPipelineId == null) {
            throw new StageException(PreviewError.PREVIEW_0103);
        }

        final PipelinePreviewEvent previewEvent = MessagingDtoJsonMapper.INSTANCE
                .asPipelinePreviewEventDto(dynamicPreviewEvent.getPreviewEvent());

        // set up after actions
        previewEvent.setAfterActionsFunction(p -> {
            LOG.debug("Running after actions for dynamic preview");
            try {
                handlePreviewEvents("after", dynamicPreviewEvent.getAfterActionsEventTypeIds(),
                        dynamicPreviewEvent.getAfterActions());
            } catch (IOException e) {
                LOG.error("IOException attempting to handle after actions for dynamic preview: {}", e.getMessage(),
                        e);
            }
            return null;
        });

        final RemoteDataCollectorResult previewEventResult = eventHandlerTask.handleLocalEvent(previewEvent,
                EventType.fromValue(dynamicPreviewEvent.getPreviewEventTypeId()));

        String previewerId = null;

        if (previewEventResult.getImmediateResult() == null) {
            throw new StageException(PreviewError.PREVIEW_0102);
        } else {
            previewerId = (String) previewEventResult.getImmediateResult();
        }
        final Previewer previewer = manager.getPreviewer(previewerId);
        final PreviewInfoJson previewInfoJson = new PreviewInfoJson(previewer.getId(), previewer.getStatus(),
                generatedPipelineId);
        return Response.ok().type(MediaType.APPLICATION_JSON).entity(previewInfoJson).build();
    }

    private void checkDynamicPreviewPermissions(DynamicPreviewRequestJson dynamicPreviewRequest) {
        switch (dynamicPreviewRequest.getType()) {
        case CLASSIFICATION_CATALOG:
            if (!CollectionUtils.containsAny(currentUser.getRoles(),
                    DYNAMIC_PREVIEW_ALLOWED_ROLES_CLASSIFICATION)) {
                throw new ForbiddenException(Collections.singletonMap("message",
                        String.format(
                                "User did not have any roles that would allow performing %s type dynamic preview",
                                DynamicPreviewType.CLASSIFICATION_CATALOG.name())));
            }
            break;
        case PROTECTION_POLICY:
            // permissions on specific policies will be checked on DPM side
            break;
        }
    }

    private DynamicPreviewEventJson getDynamicPreviewEvent(String schBaseUrl, String requestPath,
            String appAuthToken, String componentId, DynamicPreviewRequestJson requestJson) throws IOException {
        final RestClient dynamicPreviewAppClient = RestClient.builder(schBaseUrl).json(true)
                .appAuthToken(appAuthToken).componentId(componentId).csrf(true).build(requestPath);

        final RestClient.Response response = dynamicPreviewAppClient.post(requestJson);
        if (response.haveData() && response.successful()) {
            return response.getData(DynamicPreviewEventJson.class);
        } else {
            LOG.error("Error trying to get DynamicPreviewEventJson; status {}, error: {}", response.getStatus(),
                    response.getError());
            return null;
        }
    }

    private String handlePreviewEvents(String kind, List<Integer> eventTypeIds, List<EventJson> events)
            throws IOException {
        String generatedPipelineId = null;
        if (eventTypeIds == null && events == null) {
            LOG.debug("Null {} events; nothing to do", kind);
            return null;
        }
        if (eventTypeIds.size() != events.size()) {
            throw new IllegalArgumentException(String.format(
                    "Length of type IDs (%d) did not match length of events (%d); cannot process %s events in dynamic preview",
                    eventTypeIds.size(), events.size(), kind));
        }
        for (int i = 0; i < eventTypeIds.size(); i++) {
            final int eventTypeId = eventTypeIds.get(i);
            final EventType eventType = EventType.fromValue(eventTypeId);
            final RemoteDataCollectorResult result = eventHandlerTask
                    .handleLocalEvent(MessagingJsonToFromDto.INSTANCE.asDto(events.get(i), eventTypeId), eventType);
            if (eventType == EventType.SAVE_PIPELINE && result.getImmediateResult() != null) {
                generatedPipelineId = (String) result.getImmediateResult();
            }
        }
        return generatedPipelineId;
    }

    @Path("/pipeline/{pipelineId}/preview/{previewerId}/status")
    @GET
    @ApiOperation(value = "Return Preview status by previewer ID", response = PreviewInfoJson.class, authorizations = @Authorization(value = "basic"))
    @Produces(MediaType.APPLICATION_JSON)
    @RolesAllowed({ AuthzRole.CREATOR, AuthzRole.ADMIN, AuthzRole.CREATOR_REMOTE, AuthzRole.ADMIN_REMOTE,
            AuthzRole.MANAGER, AuthzRole.MANAGER_REMOTE })
    public Response getPreviewStatus(@PathParam("pipelineId") String pipelineId,
            @PathParam("previewerId") String previewerId, @QueryParam("edge") @DefaultValue("false") boolean edge)
            throws PipelineException {
        if (edge) {
            PipelineConfiguration pipelineConfiguration = store.load(pipelineId, "0");
            Config edgeHttpUrlConfig = pipelineConfiguration.getConfiguration(EdgeUtil.EDGE_HTTP_URL);
            if (edgeHttpUrlConfig != null) {
                return EdgeUtil.proxyRequestGET((String) edgeHttpUrlConfig.getValue(),
                        "/rest/v1/pipeline/" + pipelineId + "/preview/" + previewerId + "/status",
                        Collections.emptyMap());
            }
        }
        Previewer previewer = manager.getPreviewer(previewerId);
        if (previewer == null) {
            return Response.status(Response.Status.NOT_FOUND).entity("Cannot find previewer with id " + previewerId)
                    .build();
        }
        PipelineInfo pipelineInfo = store.getInfo(previewer.getName());
        RestAPIUtils.injectPipelineInMDC(pipelineInfo.getTitle(), pipelineInfo.getPipelineId());
        PreviewInfoJson previewInfoJson = new PreviewInfoJson(previewer.getId(), previewer.getStatus());
        return Response.ok().type(MediaType.APPLICATION_JSON).entity(previewInfoJson).build();
    }

    @Path("/pipeline/{pipelineId}/preview/{previewerId}")
    @GET
    @ApiOperation(value = "Return Preview Data by previewer ID", response = PreviewOutputJson.class, authorizations = @Authorization(value = "basic"))
    @Produces(MediaType.APPLICATION_JSON)
    @RolesAllowed({ AuthzRole.CREATOR, AuthzRole.ADMIN, AuthzRole.CREATOR_REMOTE, AuthzRole.ADMIN_REMOTE,
            AuthzRole.MANAGER, AuthzRole.MANAGER_REMOTE })
    public Response getPreviewData(@PathParam("pipelineId") String pipelineId,
            @PathParam("previewerId") String previewerId, @QueryParam("edge") @DefaultValue("false") boolean edge)
            throws PipelineException {
        if (edge) {
            PipelineConfiguration pipelineConfiguration = store.load(pipelineId, "0");
            Config edgeHttpUrlConfig = pipelineConfiguration.getConfiguration(EdgeUtil.EDGE_HTTP_URL);
            if (edgeHttpUrlConfig != null) {
                return EdgeUtil.proxyRequestGET((String) edgeHttpUrlConfig.getValue(),
                        "/rest/v1/pipeline/" + pipelineId + "/preview/" + previewerId, Collections.emptyMap());
            }
        }
        Previewer previewer = manager.getPreviewer(previewerId);
        if (previewer == null) {
            return Response.status(Response.Status.NOT_FOUND).entity("Cannot find previewer with id " + previewerId)
                    .build();
        }
        PipelineInfo pipelineInfo = store.getInfo(previewer.getName());
        RestAPIUtils.injectPipelineInMDC(pipelineInfo.getTitle(), pipelineInfo.getPipelineId());
        PreviewOutput previewOutput = previewer.getOutput();
        return Response.ok().type(MediaType.APPLICATION_JSON).entity(BeanHelper.wrapPreviewOutput(previewOutput))
                .build();
    }

    @Path("/pipeline/{pipelineId}/preview/{previewerId}")
    @DELETE
    @ApiOperation(value = "Stop Preview by previewer ID", response = PreviewInfoJson.class, authorizations = @Authorization(value = "basic"))
    @Produces(MediaType.APPLICATION_JSON)
    @RolesAllowed({ AuthzRole.CREATOR, AuthzRole.ADMIN, AuthzRole.CREATOR_REMOTE, AuthzRole.ADMIN_REMOTE,
            AuthzRole.MANAGER, AuthzRole.MANAGER_REMOTE })
    public Response stopPreview(@PathParam("pipelineId") String pipelineId,
            @PathParam("previewerId") String previewerId, @QueryParam("edge") @DefaultValue("false") boolean edge)
            throws PipelineException {
        if (edge) {
            PipelineConfiguration pipelineConfiguration = store.load(pipelineId, "0");
            Config edgeHttpUrlConfig = pipelineConfiguration.getConfiguration(EdgeUtil.EDGE_HTTP_URL);
            if (edgeHttpUrlConfig != null) {
                return EdgeUtil.proxyRequestDELETE((String) edgeHttpUrlConfig.getValue(),
                        "/rest/v1/pipeline/" + pipelineId + "/preview/" + previewerId, Collections.emptyMap());
            }
        }
        Previewer previewer = manager.getPreviewer(previewerId);
        if (previewer == null) {
            return Response.status(Response.Status.NOT_FOUND).entity("Cannot find previewer with id " + previewerId)
                    .build();
        }
        PipelineInfo pipelineInfo = store.getInfo(previewer.getName());
        RestAPIUtils.injectPipelineInMDC(pipelineInfo.getTitle(), pipelineInfo.getPipelineId());
        previewer.stop();
        PreviewInfoJson previewInfoJson = new PreviewInfoJson(previewer.getId(), previewer.getStatus());
        return Response.ok().type(MediaType.APPLICATION_JSON).entity(previewInfoJson).build();
    }

    @Path("/pipeline/{pipelineId}/rawSourcePreview")
    @GET
    @ApiOperation(value = "Get raw source preview data for pipeline name and revision", response = RawPreview.class, authorizations = @Authorization(value = "basic"))
    @Produces(MediaType.APPLICATION_JSON)
    @RolesAllowed({ AuthzRole.CREATOR, AuthzRole.ADMIN, AuthzRole.CREATOR_REMOTE, AuthzRole.ADMIN_REMOTE,
            AuthzRole.MANAGER, AuthzRole.MANAGER_REMOTE })
    public Response rawSourcePreview(@PathParam("pipelineId") String pipelineId, @QueryParam("rev") String rev,
            @Context UriInfo uriInfo) throws PipelineException {
        PipelineInfo pipelineInfo = store.getInfo(pipelineId);
        RestAPIUtils.injectPipelineInMDC(pipelineInfo.getTitle(), pipelineInfo.getPipelineId());
        MultivaluedMap<String, String> previewParams = uriInfo.getQueryParameters();
        Previewer previewer = manager.createPreviewer(this.user, pipelineId, rev, Collections.emptyList(),
                p -> null);
        RawPreview rawPreview = previewer.getRawSource(4 * 1024, previewParams);
        return Response.ok().type(MediaType.APPLICATION_JSON).entity(rawPreview).build();
    }

    @Path("/pipeline/{pipelineId}/validate")
    @GET
    @ApiOperation(value = "Validate pipeline configuration and return validation status and issues", response = PreviewInfoJson.class, authorizations = @Authorization(value = "basic"))
    @Produces(MediaType.APPLICATION_JSON)
    @RolesAllowed({ AuthzRole.CREATOR, AuthzRole.MANAGER, AuthzRole.ADMIN, AuthzRole.CREATOR_REMOTE,
            AuthzRole.MANAGER_REMOTE, AuthzRole.ADMIN_REMOTE })
    public Response validateConfigs(@PathParam("pipelineId") String pipelineId, @QueryParam("rev") String rev,
            @QueryParam("timeout") @DefaultValue("2000") long timeout,
            @QueryParam("edge") @DefaultValue("false") boolean edge) throws PipelineException {
        if (edge) {
            PipelineConfiguration pipelineConfiguration = store.load(pipelineId, "0");
            Config edgeHttpUrlConfig = pipelineConfiguration.getConfiguration(EdgeUtil.EDGE_HTTP_URL);
            if (edgeHttpUrlConfig != null) {
                EdgeUtil.publishEdgePipeline(pipelineConfiguration, null);
                return EdgeUtil.proxyRequestGET((String) edgeHttpUrlConfig.getValue(),
                        "/rest/v1/pipeline/" + pipelineId + "/validate", ImmutableMap.of("timeout", timeout));
            }
        }
        PipelineInfo pipelineInfo = store.getInfo(pipelineId);
        RestAPIUtils.injectPipelineInMDC(pipelineInfo.getTitle(), pipelineInfo.getPipelineId());
        try {
            Previewer previewer = manager.createPreviewer(this.user, pipelineId, rev, Collections.emptyList(),
                    p -> null);
            previewer.validateConfigs(timeout);
            PreviewStatus previewStatus = previewer.getStatus();
            if (previewStatus == null) {
                previewStatus = PreviewStatus.VALIDATING;
            }
            PreviewInfoJson previewInfoJson = new PreviewInfoJson(previewer.getId(), previewStatus);
            return Response.ok().type(MediaType.APPLICATION_JSON).entity(previewInfoJson).build();
        } catch (PipelineRuntimeException ex) {
            if (ex.getErrorCode() == ContainerError.CONTAINER_0165) {
                return Response.status(Response.Status.BAD_REQUEST).type(MediaType.APPLICATION_JSON)
                        .entity(BeanHelper.wrapIssues(ex.getIssues())).build();
            } else {
                throw ex;
            }
        }
    }

}