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