All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.vmware.connectors.bitbucket.server.BitbucketServerController Maven / Gradle / Ivy

/*
 * Copyright © 2018 VMware, Inc. All Rights Reserved.
 * SPDX-License-Identifier: BSD-2-Clause
 */

package com.vmware.connectors.bitbucket.server;

import com.vmware.connectors.bitbucket.server.utils.BitbucketServerPullRequest;
import com.vmware.connectors.common.json.JsonDocument;
import com.vmware.connectors.common.payloads.request.CardRequest;
import com.vmware.connectors.common.payloads.response.*;
import com.vmware.connectors.common.utils.CardTextAccessor;
import com.vmware.connectors.common.utils.CommonUtils;
import com.vmware.connectors.common.utils.Reactive;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

import javax.servlet.http.HttpServletRequest;
import javax.validation.Valid;
import java.util.HashSet;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import static org.springframework.http.HttpHeaders.AUTHORIZATION;

@RestController
public class BitbucketServerController {

    private static final Logger logger = LoggerFactory.getLogger(BitbucketServerController.class);

    private static final String AUTH_HEADER = "X-Connector-Authorization";
    private static final String BASE_URL_HEADER = "X-Connector-Base-Url";
    private static final String ROUTING_PREFIX = "x-routing-prefix";

    private static final String PROJECT_PARAM_KEY = "project";
    private static final String USER_PARAM_KEY = "user";
    private static final String COMMENT_PARAM_KEY = "comment";

    private static final String OPEN = "OPEN";

    // To prevent CSRF check by Bitbucket Server.
    private static final String ATLASSIAN_TOKEN = "X-Atlassian-Token";

    private final WebClient rest;
    private final CardTextAccessor cardTextAccessor;

    @Autowired
    public BitbucketServerController(WebClient rest, CardTextAccessor cardTextAccessor) {
        this.rest = rest;
        this.cardTextAccessor = cardTextAccessor;
    }

    @GetMapping("/test-auth")
    public Mono verifyAuth(
            @RequestHeader(AUTH_HEADER) String authHeader,
            @RequestHeader(BASE_URL_HEADER) String baseUrl
    ) {
        return rest.head()
                .uri(baseUrl + "/rest/api/1.0/dashboard/pull-request-suggestions?limit=1")
                .header(AUTHORIZATION, authHeader)
                .retrieve()
                .bodyToMono(Void.class);
    }

    @PostMapping(
            value = "/cards/requests",
            produces = MediaType.APPLICATION_JSON_VALUE,
            consumes = MediaType.APPLICATION_JSON_VALUE
    )
    public Mono getCards(
            @RequestHeader(AUTH_HEADER) String authHeader,
            @RequestHeader(BASE_URL_HEADER) String baseUrl,
            @RequestHeader(ROUTING_PREFIX) String routingPrefix,
            Locale locale,
            @Valid @RequestBody CardRequest cardRequest,
            HttpServletRequest request
    ) {
        logger.debug("Cards requests for bitbucket server connector - baseUrlHeader: {}, routingPrefix: {}", baseUrl, routingPrefix);

        Set projectRepoTokens = cardRequest.getTokens("projects_pr_email_subject");
        Set userRepoTokens = cardRequest.getTokens("users_pr_email_subject");

        Set pullRequests = convertToProjectRepoPr(projectRepoTokens);
        pullRequests.addAll(convertToUserRepoPr(userRepoTokens));

        return Flux.fromIterable(pullRequests)
                .flatMap(pullRequest -> getCardForPr(authHeader, pullRequest, baseUrl, routingPrefix, locale, request))
                .collect(Cards::new, (cards, card) -> cards.getCards().add(card))
                .defaultIfEmpty(new Cards())
                .subscriberContext(Reactive.setupContext());
    }

    private Set convertToProjectRepoPr(
            Set cardTokens
    ) {
        return convertToPrs(
                cardTokens,
                matcher -> new BitbucketServerPullRequest(null, matcher.group(2), matcher.group(3), matcher.group(4))
        );
    }

    private Set convertToUserRepoPr(
            Set cardTokens
    ) {
        return convertToPrs(
                cardTokens,
                matcher -> new BitbucketServerPullRequest(matcher.group(2), null, matcher.group(3), matcher.group(4))
        );
    }

    private Set convertToPrs(
            Set cardTokens,
            Function mapper
    ) {
        Set pullRequests = new HashSet<>();

        if (cardTokens == null) {
            return pullRequests;
        }

        String regex = "(([a-zA-Z0-9]+)\\/([a-zA-Z0-9-]+) - Pull request #([0-9]+):[ ])";
        Pattern pattern = Pattern.compile(regex);

        for (String prEmailSubject : cardTokens) {
            Matcher matcher = pattern.matcher(prEmailSubject);
            while (matcher.find()) {
                pullRequests.add(mapper.apply(matcher));
            }
        }

        return pullRequests;
    }

    private Mono getCardForPr(
            String authHeader,
            BitbucketServerPullRequest pullRequest,
            String baseUrl,
            String routingPrefix,
            Locale locale,
            HttpServletRequest request
    ) {
        logger.debug("Requesting pull request info from bitbucket server base url: {} and pull request info: {}", baseUrl, pullRequest);

        Mono response = getPullRequestInfo(authHeader, pullRequest, baseUrl);

        return response
                .onErrorResume(Reactive::skipOnNotFound)
                .map(prResponse -> convertResponseIntoCard(prResponse, pullRequest, routingPrefix, locale, request));
    }

    private Mono getPullRequestInfo(
            String authHeader,
            BitbucketServerPullRequest pullRequest,
            String baseUrl
    ) {
        return rest.get()
                .uri(
                        baseUrl + "/rest/api/1.0/{prefix}/{key}/repos/{respositorySlug}/pull-requests/{pullRequestId}",
                        getPrefix(pullRequest),
                        getKey(pullRequest),
                        pullRequest.getRepositorySlug(),
                        pullRequest.getPullRequestId()
                )
                .header(AUTHORIZATION, authHeader)
                .retrieve()
                .bodyToMono(JsonDocument.class);
    }

    private String getPrefix(BitbucketServerPullRequest pullRequest) {
        return pullRequest.isProject() ? "projects" : "users";
    }

    private String getKey(BitbucketServerPullRequest pullRequest) {
        return pullRequest.isProject() ? pullRequest.getProjectKey() : pullRequest.getUserKey();
    }

    private Card convertResponseIntoCard(
            JsonDocument response,
            BitbucketServerPullRequest pullRequest,
            String routingPrefix,
            Locale locale,
            HttpServletRequest request
    ) {
        Card.Builder card = new Card.Builder()
                .setHeader(
                        cardTextAccessor.getHeader(locale,
                                pullRequest.getPullRequestId(),
                                response.read("$.author.user.displayName")
                        ),
                        cardTextAccessor.getMessage("subtitle", locale,
                                getKey(pullRequest),
                                pullRequest.getRepositorySlug()
                        )
                );

        // Set image url to card response.
        CommonUtils.buildConnectorImageUrl(card, request);

        if (OPEN.equalsIgnoreCase(response.read("$.state"))) {
            card.addAction(
                    new CardAction.Builder()
                            .setPrimary(true)
                            .setLabel(cardTextAccessor.getActionLabel("bitbucket.approve", locale))
                            .setCompletedLabel(cardTextAccessor.getActionCompletedLabel("bitbucket.approve", locale))
                            .setActionKey(CardActionKey.DIRECT)
                            .addRequestParam(USER_PARAM_KEY, pullRequest.getUserKey())
                            .addRequestParam(PROJECT_PARAM_KEY, pullRequest.getProjectKey())
                            .setUrl(buildActionUrl(routingPrefix, pullRequest, "approve"))
                            .setType(HttpMethod.POST)
                            .build()
            );
        }

        card.addAction(
                new CardAction.Builder()
                        .setLabel(cardTextAccessor.getActionLabel("bitbucket.comments", locale))
                        .setCompletedLabel(cardTextAccessor.getActionCompletedLabel("bitbucket.comments", locale))
                        .setActionKey(CardActionKey.USER_INPUT)
                        .addRequestParam(USER_PARAM_KEY, pullRequest.getUserKey())
                        .addRequestParam(PROJECT_PARAM_KEY, pullRequest.getProjectKey())
                        .setUrl(buildActionUrl(routingPrefix, pullRequest, "comments"))
                        .setAllowRepeated(true)
                        .addUserInputField(
                                new CardActionInputField.Builder()
                                        .setId(COMMENT_PARAM_KEY)
                                        .setFormat("textarea")
                                        .setLabel(cardTextAccessor.getMessage("bitbucket.comments", locale))
                                        .setMinLength(1)
                                        .build()
                        )
                        .setType(HttpMethod.POST)
                        .build()
        );

        return card.build();
    }

    private String buildActionUrl(
            String routingPrefix,
            BitbucketServerPullRequest pullRequest,
            String action
    ) {
        return String.format(
                "%sapi/v1/%s/%s/%s",
                routingPrefix,
                pullRequest.getRepositorySlug(),
                pullRequest.getPullRequestId(),
                action
        );
    }

    @PostMapping(
            path = "/api/v1/{repositorySlug}/{pullRequestId}/approve",
            consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE
    )
    public Mono approve(
            @RequestParam(name = USER_PARAM_KEY, required = false) String user,
            @RequestParam(name = PROJECT_PARAM_KEY, required = false) String project,
            @PathVariable("repositorySlug") String repositorySlug,
            @PathVariable("pullRequestId") String pullRequestId,
            @RequestHeader(AUTH_HEADER) String authHeader,
            @RequestHeader(BASE_URL_HEADER) String baseUrl
    ) {
        BitbucketServerPullRequest pullRequest = new BitbucketServerPullRequest(
                user,
                project,
                repositorySlug,
                pullRequestId
        );

        logger.debug("Approve ACTION for bitbucket server pull request: {}, baseURL: {}", pullRequest, baseUrl);

        return forceApprove(baseUrl, authHeader, pullRequest, "approve");
    }

    private Mono forceApprove(
            String baseUrl,
            String authHeader,
            BitbucketServerPullRequest pullRequest,
            String action
    ) {
        // Get current version of the pull request. Pull request "version" changes when we do any actions on it.
        // When the pull request is raised, the current value will be 0.
        // For example, when we approve the pull request, then the version will change from 0 to 1.
        // We have to add the latest version of the pull request URI to do any actions. Otherwise, the ACTION will be rejected.
        // If the build for the branch is going on, then the actions would be rejected.
        return getVersion(authHeader, baseUrl, pullRequest)
                .flatMap(version -> callApprove(baseUrl, authHeader, pullRequest, action, version));
    }

    private Mono getVersion(
            String authHeader,
            String baseUrl,
            BitbucketServerPullRequest pullRequest
    ) {
        return getPullRequestInfo(authHeader, pullRequest, baseUrl)
                .map(jsonDocument -> jsonDocument.read("$.version"));
    }

    private Mono callApprove(
            String baseUrl,
            String authHeader,
            BitbucketServerPullRequest pullRequest,
            String action,
            Integer version
    ) {
        return rest.post()
                .uri(
                        baseUrl + "/rest/api/1.0/{prefix}/{key}/repos/{repositoryPlug}/pull-requests/{pullRequestId}/{action}?version={version}",
                        getPrefix(pullRequest),
                        getKey(pullRequest),
                        pullRequest.getRepositorySlug(),
                        pullRequest.getPullRequestId(),
                        action,
                        version
                )
                .header(AUTHORIZATION, authHeader)
                .header(ATLASSIAN_TOKEN, "no-check")
                .retrieve()
                .bodyToMono(String.class);
    }

    @PostMapping(
            path = "/api/v1/{repositorySlug}/{pullRequestId}/comments",
            consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE
    )
    public Mono> comments(
            @RequestParam(name = USER_PARAM_KEY, required = false) String user,
            @RequestParam(name = PROJECT_PARAM_KEY, required = false) String project,
            @PathVariable("repositorySlug") String repositorySlug,
            @PathVariable("pullRequestId") String pullRequestId,
            @RequestParam(COMMENT_PARAM_KEY) String comment,
            @RequestHeader(AUTH_HEADER) String authHeader,
            @RequestHeader(BASE_URL_HEADER) String baseUrl
    ) {
        BitbucketServerPullRequest pullRequest = new BitbucketServerPullRequest(
                user,
                project,
                repositorySlug,
                pullRequestId
        );

        logger.debug("Comment ACTION for bitbucket server pull request: {}, baseURL: {}", pullRequest, baseUrl);

        Map payload = Map.of("text", comment);

        return rest.post()
                .uri(
                        baseUrl + "/rest/api/1.0/{prefix}/{key}/repos/{repositorySlug}/pull-requests/{pullRequestId}/comments",
                        getPrefix(pullRequest),
                        getKey(pullRequest),
                        pullRequest.getRepositorySlug(),
                        pullRequest.getPullRequestId()
                )
                .header(AUTHORIZATION, authHeader)
                .contentType(MediaType.APPLICATION_JSON)
                .syncBody(payload)
                .exchange()
                .flatMap(Reactive::checkStatus)
                .flatMap(response -> response.toEntity(String.class));
    }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy