You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

BitbucketServerRestClient.java 8.5KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211
  1. /*
  2. * SonarQube
  3. * Copyright (C) 2009-2021 SonarSource SA
  4. * mailto:info AT sonarsource DOT com
  5. *
  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.
  10. *
  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.
  15. *
  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.
  19. */
  20. package org.sonar.alm.client.bitbucketserver;
  21. import com.google.gson.Gson;
  22. import com.google.gson.GsonBuilder;
  23. import com.google.gson.JsonParseException;
  24. import com.google.gson.JsonSyntaxException;
  25. import com.google.gson.annotations.SerializedName;
  26. import java.io.IOException;
  27. import java.util.Optional;
  28. import java.util.function.Function;
  29. import java.util.stream.Collectors;
  30. import java.util.stream.Stream;
  31. import javax.annotation.Nullable;
  32. import okhttp3.HttpUrl;
  33. import okhttp3.MediaType;
  34. import okhttp3.OkHttpClient;
  35. import okhttp3.Request;
  36. import okhttp3.RequestBody;
  37. import okhttp3.Response;
  38. import okhttp3.ResponseBody;
  39. import org.sonar.alm.client.TimeoutConfiguration;
  40. import org.sonar.api.server.ServerSide;
  41. import org.sonar.api.utils.log.Logger;
  42. import org.sonar.api.utils.log.Loggers;
  43. import org.sonarqube.ws.client.OkHttpClientBuilder;
  44. import static com.google.common.base.Strings.isNullOrEmpty;
  45. import static java.lang.String.format;
  46. import static java.net.HttpURLConnection.HTTP_NOT_FOUND;
  47. import static java.net.HttpURLConnection.HTTP_UNAUTHORIZED;
  48. import static java.util.Locale.ENGLISH;
  49. import static org.sonar.api.internal.apachecommons.lang.StringUtils.removeEnd;
  50. @ServerSide
  51. public class BitbucketServerRestClient {
  52. private static final Logger LOG = Loggers.get(BitbucketServerRestClient.class);
  53. private static final String GET = "GET";
  54. protected static final String UNABLE_TO_CONTACT_BITBUCKET_SERVER = "Unable to contact Bitbucket server";
  55. protected final OkHttpClient client;
  56. public BitbucketServerRestClient(TimeoutConfiguration timeoutConfiguration) {
  57. OkHttpClientBuilder okHttpClientBuilder = new OkHttpClientBuilder();
  58. client = okHttpClientBuilder
  59. .setConnectTimeoutMs(timeoutConfiguration.getConnectTimeout())
  60. .setReadTimeoutMs(timeoutConfiguration.getReadTimeout())
  61. .build();
  62. }
  63. public void validateUrl(String serverUrl) {
  64. HttpUrl url = buildUrl(serverUrl, "/rest/api/1.0/repos");
  65. doGet("", url, r -> buildGson().fromJson(r.body().charStream(), RepositoryList.class));
  66. }
  67. public void validateToken(String serverUrl, String token) {
  68. HttpUrl url = buildUrl(serverUrl, "/rest/api/1.0/users");
  69. doGet(token, url, r -> buildGson().fromJson(r.body().charStream(), UserList.class));
  70. }
  71. public void validateReadPermission(String serverUrl, String personalAccessToken) {
  72. HttpUrl url = buildUrl(serverUrl, "/rest/api/1.0/repos");
  73. doGet(personalAccessToken, url, r -> buildGson().fromJson(r.body().charStream(), RepositoryList.class));
  74. }
  75. public RepositoryList getRepos(String serverUrl, String token, @Nullable String project, @Nullable String repo) {
  76. String projectOrEmpty = Optional.ofNullable(project).orElse("");
  77. String repoOrEmpty = Optional.ofNullable(repo).orElse("");
  78. HttpUrl url = buildUrl(serverUrl, format("/rest/api/1.0/repos?projectname=%s&name=%s", projectOrEmpty, repoOrEmpty));
  79. return doGet(token, url, r -> buildGson().fromJson(r.body().charStream(), RepositoryList.class));
  80. }
  81. public Repository getRepo(String serverUrl, String token, String project, String repoSlug) {
  82. HttpUrl url = buildUrl(serverUrl, format("/rest/api/1.0/projects/%s/repos/%s", project, repoSlug));
  83. return doGet(token, url, r -> buildGson().fromJson(r.body().charStream(), Repository.class));
  84. }
  85. public RepositoryList getRecentRepo(String serverUrl, String token) {
  86. HttpUrl url = buildUrl(serverUrl, "/rest/api/1.0/profile/recent/repos");
  87. return doGet(token, url, r -> buildGson().fromJson(r.body().charStream(), RepositoryList.class));
  88. }
  89. public ProjectList getProjects(String serverUrl, String token) {
  90. HttpUrl url = buildUrl(serverUrl, "/rest/api/1.0/projects");
  91. return doGet(token, url, r -> buildGson().fromJson(r.body().charStream(), ProjectList.class));
  92. }
  93. public BranchesList getBranches(String serverUrl, String token, String projectSlug, String repositorySlug) {
  94. HttpUrl url = buildUrl(serverUrl, format("/rest/api/1.0/projects/%s/repos/%s/branches", projectSlug, repositorySlug));
  95. return doGet(token, url, r -> buildGson().fromJson(r.body().charStream(), BranchesList.class));
  96. }
  97. protected static HttpUrl buildUrl(@Nullable String serverUrl, String relativeUrl) {
  98. if (serverUrl == null || !(serverUrl.toLowerCase(ENGLISH).startsWith("http://") || serverUrl.toLowerCase(ENGLISH).startsWith("https://"))) {
  99. throw new IllegalArgumentException("url must start with http:// or https://");
  100. }
  101. return HttpUrl.parse(removeEnd(serverUrl, "/") + relativeUrl);
  102. }
  103. protected <G> G doGet(String token, HttpUrl url, Function<Response, G> handler) {
  104. Request request = prepareRequestWithBearerToken(token, GET, url, null);
  105. return doCall(request, handler);
  106. }
  107. protected static Request prepareRequestWithBearerToken(@Nullable String token, String method, HttpUrl url, @Nullable RequestBody body) {
  108. Request.Builder builder = new Request.Builder()
  109. .method(method, body)
  110. .url(url)
  111. .addHeader("x-atlassian-token", "no-check");
  112. if (!isNullOrEmpty(token)) {
  113. builder.addHeader("Authorization", "Bearer " + token);
  114. }
  115. return builder.build();
  116. }
  117. protected <G> G doCall(Request request, Function<Response, G> handler) {
  118. try (Response response = client.newCall(request).execute()) {
  119. handleError(response);
  120. return handler.apply(response);
  121. } catch (JsonSyntaxException e) {
  122. LOG.info(UNABLE_TO_CONTACT_BITBUCKET_SERVER + ": " + e.getMessage(), e);
  123. throw new IllegalArgumentException(UNABLE_TO_CONTACT_BITBUCKET_SERVER + ", got an unexpected response", e);
  124. } catch (IOException e) {
  125. LOG.info(UNABLE_TO_CONTACT_BITBUCKET_SERVER + ": " + e.getMessage(), e);
  126. throw new IllegalArgumentException(UNABLE_TO_CONTACT_BITBUCKET_SERVER, e);
  127. }
  128. }
  129. protected static void handleError(Response response) throws IOException {
  130. if (!response.isSuccessful()) {
  131. String errorMessage = getErrorMessage(response.body());
  132. LOG.debug(UNABLE_TO_CONTACT_BITBUCKET_SERVER + ": {} {}", response.code(), errorMessage);
  133. if (response.code() == HTTP_UNAUTHORIZED) {
  134. throw new BitbucketServerException(HTTP_UNAUTHORIZED, "Invalid personal access token");
  135. } else if (response.code() == HTTP_NOT_FOUND) {
  136. throw new BitbucketServerException(HTTP_NOT_FOUND, errorMessage);
  137. }
  138. throw new IllegalArgumentException(UNABLE_TO_CONTACT_BITBUCKET_SERVER);
  139. }
  140. }
  141. protected static boolean equals(@Nullable MediaType first, @Nullable MediaType second) {
  142. String s1 = first == null ? null : first.toString().toLowerCase(ENGLISH).replace(" ", "");
  143. String s2 = second == null ? null : second.toString().toLowerCase(ENGLISH).replace(" ", "");
  144. return s1 != null && s2 != null && s1.equals(s2);
  145. }
  146. protected static String getErrorMessage(ResponseBody body) throws IOException {
  147. String bodyString = body.string();
  148. if (equals(MediaType.parse("application/json;charset=utf-8"), body.contentType()) && !isNullOrEmpty(bodyString)) {
  149. try {
  150. return Stream.of(buildGson().fromJson(bodyString, Errors.class).errorData)
  151. .map(e -> e.exceptionName + " " + e.message)
  152. .collect(Collectors.joining("\n"));
  153. } catch (JsonParseException e) {
  154. return bodyString;
  155. }
  156. }
  157. return bodyString;
  158. }
  159. protected static Gson buildGson() {
  160. return new GsonBuilder()
  161. .create();
  162. }
  163. protected static class Errors {
  164. @SerializedName("errors")
  165. public Error[] errorData;
  166. public Errors() {
  167. // http://stackoverflow.com/a/18645370/229031
  168. }
  169. public static class Error {
  170. @SerializedName("message")
  171. public String message;
  172. @SerializedName("exceptionName")
  173. public String exceptionName;
  174. public Error() {
  175. // http://stackoverflow.com/a/18645370/229031
  176. }
  177. }
  178. }
  179. }