3 * Copyright (C) 2009-2021 SonarSource SA
4 * mailto:info AT sonarsource DOT com
6 * This program is free software; you can redistribute it and/or
7 * modify it under the terms of the GNU Lesser General Public
8 * License as published by the Free Software Foundation; either
9 * version 3 of the License, or (at your option) any later version.
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14 * Lesser General Public License for more details.
16 * You should have received a copy of the GNU Lesser General Public License
17 * along with this program; if not, write to the Free Software Foundation,
18 * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
20 package org.sonar.alm.client.bitbucketserver;
22 import com.google.gson.Gson;
23 import com.google.gson.GsonBuilder;
24 import com.google.gson.JsonParseException;
25 import com.google.gson.JsonSyntaxException;
26 import com.google.gson.annotations.SerializedName;
27 import java.io.IOException;
28 import java.net.HttpURLConnection;
29 import java.util.Optional;
30 import java.util.function.Function;
31 import java.util.stream.Collectors;
32 import java.util.stream.Stream;
33 import javax.annotation.Nullable;
34 import okhttp3.HttpUrl;
35 import okhttp3.MediaType;
36 import okhttp3.OkHttpClient;
37 import okhttp3.Request;
38 import okhttp3.RequestBody;
39 import okhttp3.Response;
40 import okhttp3.ResponseBody;
41 import org.sonar.alm.client.TimeoutConfiguration;
42 import org.sonar.api.server.ServerSide;
43 import org.sonar.api.utils.log.Logger;
44 import org.sonar.api.utils.log.Loggers;
45 import org.sonarqube.ws.client.OkHttpClientBuilder;
47 import static java.lang.String.format;
48 import static java.util.Locale.ENGLISH;
49 import static org.sonar.api.internal.apachecommons.lang.StringUtils.removeEnd;
52 public class BitbucketServerRestClient {
54 private static final Logger LOG = Loggers.get(BitbucketServerRestClient.class);
55 private static final String GET = "GET";
56 protected static final String UNABLE_TO_CONTACT_BITBUCKET_SERVER = "Unable to contact Bitbucket server";
58 protected final OkHttpClient client;
60 public BitbucketServerRestClient(TimeoutConfiguration timeoutConfiguration) {
61 OkHttpClientBuilder okHttpClientBuilder = new OkHttpClientBuilder();
62 client = okHttpClientBuilder
63 .setConnectTimeoutMs(timeoutConfiguration.getConnectTimeout())
64 .setReadTimeoutMs(timeoutConfiguration.getReadTimeout())
68 public void validateUrl(String serverUrl) {
69 HttpUrl url = buildUrl(serverUrl, "/rest/api/1.0/repos");
70 doGet("", url, r -> buildGson().fromJson(r.body().charStream(), RepositoryList.class));
73 public void validateToken(String serverUrl, String token) {
74 HttpUrl url = buildUrl(serverUrl, "/rest/api/1.0/users");
75 doGet(token, url, r -> buildGson().fromJson(r.body().charStream(), UserList.class));
78 public void validateReadPermission(String serverUrl, String personalAccessToken) {
79 HttpUrl url = buildUrl(serverUrl, "/rest/api/1.0/repos");
80 doGet(personalAccessToken, url, r -> buildGson().fromJson(r.body().charStream(), RepositoryList.class));
83 public RepositoryList getRepos(String serverUrl, String token, @Nullable String project, @Nullable String repo) {
84 String projectOrEmpty = Optional.ofNullable(project).orElse("");
85 String repoOrEmpty = Optional.ofNullable(repo).orElse("");
86 HttpUrl url = buildUrl(serverUrl, format("/rest/api/1.0/repos?projectname=%s&name=%s", projectOrEmpty, repoOrEmpty));
87 return doGet(token, url, r -> buildGson().fromJson(r.body().charStream(), RepositoryList.class));
90 public Repository getRepo(String serverUrl, String token, String project, String repoSlug) {
91 HttpUrl url = buildUrl(serverUrl, format("/rest/api/1.0/projects/%s/repos/%s", project, repoSlug));
92 return doGet(token, url, r -> buildGson().fromJson(r.body().charStream(), Repository.class));
95 public RepositoryList getRecentRepo(String serverUrl, String token) {
96 HttpUrl url = buildUrl(serverUrl, "/rest/api/1.0/profile/recent/repos");
97 return doGet(token, url, r -> buildGson().fromJson(r.body().charStream(), RepositoryList.class));
100 public ProjectList getProjects(String serverUrl, String token) {
101 HttpUrl url = buildUrl(serverUrl, "/rest/api/1.0/projects");
102 return doGet(token, url, r -> buildGson().fromJson(r.body().charStream(), ProjectList.class));
105 public BranchesList getBranches(String serverUrl, String token, String projectSlug, String repositorySlug){
106 HttpUrl url = buildUrl(serverUrl, format("/rest/api/1.0/projects/%s/repos/%s/branches", projectSlug, repositorySlug));
107 return doGet(token, url, r -> buildGson().fromJson(r.body().charStream(), BranchesList.class));
110 protected static HttpUrl buildUrl(@Nullable String serverUrl, String relativeUrl) {
111 if (serverUrl == null || !(serverUrl.toLowerCase(ENGLISH).startsWith("http://") || serverUrl.toLowerCase(ENGLISH).startsWith("https://"))) {
112 throw new IllegalArgumentException("url must start with http:// or https://");
114 return HttpUrl.parse(removeEnd(serverUrl, "/") + relativeUrl);
117 protected <G> G doGet(String token, HttpUrl url, Function<Response, G> handler) {
118 Request request = prepareRequestWithBearerToken(token, GET, url, null);
119 return doCall(request, handler);
122 protected static Request prepareRequestWithBearerToken(String token, String method, HttpUrl url, @Nullable RequestBody body) {
123 return new Request.Builder()
124 .method(method, body)
126 .addHeader("Authorization", "Bearer " + token)
127 .addHeader("x-atlassian-token", "no-check")
131 protected <G> G doCall(Request request, Function<Response, G> handler) {
132 try (Response response = client.newCall(request).execute()) {
133 handleError(response);
134 return handler.apply(response);
135 } catch (JsonSyntaxException e) {
136 throw new IllegalArgumentException(UNABLE_TO_CONTACT_BITBUCKET_SERVER + ", got an unexpected response", e);
137 } catch (IOException e) {
138 throw new IllegalArgumentException(UNABLE_TO_CONTACT_BITBUCKET_SERVER, e);
142 protected static void handleError(Response response) throws IOException {
143 if (!response.isSuccessful()) {
144 String errorMessage = getErrorMessage(response.body());
145 LOG.debug(UNABLE_TO_CONTACT_BITBUCKET_SERVER + ": {} {}", response.code(), errorMessage);
146 if (response.code() == HttpURLConnection.HTTP_UNAUTHORIZED) {
147 throw new IllegalArgumentException("Invalid personal access token");
149 throw new IllegalArgumentException(UNABLE_TO_CONTACT_BITBUCKET_SERVER);
153 protected static boolean equals(@Nullable MediaType first, @Nullable MediaType second) {
154 String s1 = first == null ? null : first.toString().toLowerCase(ENGLISH).replace(" ", "");
155 String s2 = second == null ? null : second.toString().toLowerCase(ENGLISH).replace(" ", "");
156 return s1 != null && s2 != null && s1.equals(s2);
159 protected static String getErrorMessage(ResponseBody body) throws IOException {
160 if (equals(MediaType.parse("application/json;charset=utf-8"), body.contentType())) {
162 return Stream.of(buildGson().fromJson(body.charStream(), Errors.class).errorData)
163 .map(e -> e.exceptionName + " " + e.message)
164 .collect(Collectors.joining("\n"));
165 } catch (JsonParseException e) {
166 return body.string();
169 return body.string();
172 protected static Gson buildGson() {
173 return new GsonBuilder()
177 protected static class Errors {
179 @SerializedName("errors")
180 public Error[] errorData;
183 // http://stackoverflow.com/a/18645370/229031
186 public static class Error {
187 @SerializedName("message")
188 public String message;
190 @SerializedName("exceptionName")
191 public String exceptionName;
194 // http://stackoverflow.com/a/18645370/229031