Skip to content

Commit 504d774

Browse files
chris-camposcopybara-github
authored andcommitted
Refactor GitHubApiTransport implementations to share code.
BUG=499007054 FIXED=499007054 GWSQ_IGNORES: chriscampos@google.com PiperOrigin-RevId: 893579182 Change-Id: I660a851d4c79f9f2db4906404cea6c00419497a7
1 parent 8f9d820 commit 504d774

File tree

2 files changed

+290
-228
lines changed

2 files changed

+290
-228
lines changed
Lines changed: 283 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,283 @@
1+
/*
2+
* Copyright (C) 2016 Google Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.google.copybara.git.github.api;
18+
19+
import com.google.api.client.http.GenericUrl;
20+
import com.google.api.client.http.HttpHeaders;
21+
import com.google.api.client.http.HttpRequest;
22+
import com.google.api.client.http.HttpRequestFactory;
23+
import com.google.api.client.http.HttpResponse;
24+
import com.google.api.client.http.HttpResponseException;
25+
import com.google.api.client.http.HttpTransport;
26+
import com.google.api.client.http.json.JsonHttpContent;
27+
import com.google.api.client.json.JsonFactory;
28+
import com.google.api.client.json.JsonObjectParser;
29+
import com.google.api.client.json.gson.GsonFactory;
30+
import com.google.common.annotations.VisibleForTesting;
31+
import com.google.common.base.Preconditions;
32+
import com.google.common.collect.ImmutableListMultimap;
33+
import com.google.common.collect.Iterables;
34+
import com.google.common.flogger.FluentLogger;
35+
import com.google.copybara.exception.RepoException;
36+
import com.google.copybara.exception.ValidationException;
37+
import com.google.copybara.git.GitCredential.UserPassword;
38+
import com.google.copybara.git.GitRepository;
39+
import com.google.copybara.json.GsonParserUtil;
40+
import com.google.copybara.util.console.Console;
41+
import java.io.IOException;
42+
import java.lang.reflect.Type;
43+
import java.net.URI;
44+
import java.time.Duration;
45+
import java.util.Collection;
46+
import java.util.List;
47+
import java.util.Map;
48+
import javax.annotation.Nullable;
49+
50+
/**
51+
* Base implementation of {@link GitHubApiTransport} that uses Google http client and gson for doing
52+
* the requests.
53+
*/
54+
public abstract class AbstractGitHubApiTransport implements GitHubApiTransport {
55+
56+
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
57+
58+
protected static final JsonFactory JSON_FACTORY = new GsonFactory();
59+
private static final String GITHUB_DOT_COM_API_URL = "https://api.github.com";
60+
private static final String GITHUB_DOT_COM_WEB_URL = "https://github.com";
61+
62+
protected final String apiUrl;
63+
protected final String webUrl;
64+
protected final GitRepository repo;
65+
protected final HttpTransport httpTransport;
66+
protected final String storePath;
67+
protected final Console console;
68+
protected final boolean bearerAuth;
69+
70+
protected AbstractGitHubApiTransport(
71+
GitRepository repo,
72+
HttpTransport httpTransport,
73+
String storePath,
74+
boolean bearerAuth,
75+
Console console,
76+
String webUrl) {
77+
this.repo = Preconditions.checkNotNull(repo);
78+
this.httpTransport = Preconditions.checkNotNull(httpTransport);
79+
this.storePath = storePath;
80+
this.console = Preconditions.checkNotNull(console);
81+
this.bearerAuth = bearerAuth;
82+
this.webUrl = buildWebUrl(Preconditions.checkNotNull(webUrl));
83+
this.apiUrl = determineApiUrl(this.webUrl);
84+
}
85+
86+
protected abstract HttpResponse executeRequest(HttpRequestFactory factory, HttpRequest request)
87+
throws IOException;
88+
89+
@SuppressWarnings({"unchecked", "TypeParameterUnusedInFormals"})
90+
@Override
91+
public <T> T get(
92+
String path,
93+
Type responseType,
94+
ImmutableListMultimap<String, String> headers,
95+
String requestType)
96+
throws RepoException, ValidationException {
97+
HttpRequestFactory requestFactory = getHttpRequestFactory(getCredentialsIfPresent(), headers);
98+
GenericUrl url = getFullEndpointUrl(path);
99+
try {
100+
console.verboseFmt("Executing %s", requestType);
101+
HttpRequest httpRequest = requestFactory.buildGetRequest(url);
102+
HttpResponse response = executeRequest(requestFactory, httpRequest);
103+
Object responseObj = GsonParserUtil.parseHttpResponse(response, responseType, false);
104+
if (responseObj instanceof PaginatedPayload<?> paginatedPayload) {
105+
return (T) paginatedPayload.annotatePayload(apiUrl, maybeGetLinkHeader(response));
106+
}
107+
return (T) responseObj;
108+
} catch (HttpResponseException e) {
109+
throw new GitHubApiException(
110+
e.getStatusCode(), parseErrorOrIgnore(e), "GET", path, null, e.getContent());
111+
} catch (IOException e) {
112+
throw new RepoException("Error running GitHub API operation " + path, e);
113+
}
114+
}
115+
116+
@SuppressWarnings("unchecked")
117+
@Override
118+
public <T> T post(String path, Object request, Type responseType, String requestType)
119+
throws RepoException, ValidationException {
120+
HttpRequestFactory requestFactory =
121+
getHttpRequestFactory(getCredentials(), ImmutableListMultimap.of());
122+
GenericUrl url = getFullEndpointUrl(path);
123+
try {
124+
console.verboseFmt("Executing %s", requestType);
125+
HttpRequest httpRequest =
126+
requestFactory.buildPostRequest(url, new JsonHttpContent(JSON_FACTORY, request));
127+
HttpResponse response = executeRequest(requestFactory, httpRequest);
128+
Object responseObj = GsonParserUtil.parseHttpResponse(response, responseType, false);
129+
if (responseObj instanceof PaginatedPayload<?> paginatedPayload) {
130+
return (T) paginatedPayload.annotatePayload(apiUrl, maybeGetLinkHeader(response));
131+
}
132+
return (T) responseObj;
133+
134+
} catch (HttpResponseException e) {
135+
try {
136+
throw new GitHubApiException(
137+
e.getStatusCode(),
138+
parseErrorOrIgnore(e),
139+
"POST",
140+
path,
141+
JSON_FACTORY.toPrettyString(request),
142+
e.getContent());
143+
} catch (IOException ioE) {
144+
logger.atSevere().withCause(ioE).log("Error serializing request for error");
145+
throw new GitHubApiException(
146+
e.getStatusCode(),
147+
parseErrorOrIgnore(e),
148+
"POST",
149+
path,
150+
"unknown request",
151+
e.getContent());
152+
}
153+
} catch (IOException e) {
154+
throw new RepoException("Error running GitHub API operation " + path, e);
155+
}
156+
}
157+
158+
protected GenericUrl getFullEndpointUrl(String path) {
159+
String maybePrefix = path.startsWith("/") ? "" : "/";
160+
return new GenericUrl(URI.create(apiUrl + maybePrefix + path));
161+
}
162+
163+
@Nullable
164+
protected static ClientError parseErrorOrIgnore(HttpResponseException e) {
165+
if (e.getContent() == null) {
166+
return null;
167+
}
168+
try {
169+
return JSON_FACTORY.createJsonParser(e.getContent()).parse(ClientError.class);
170+
} catch (IOException ignore) {
171+
logger.atWarning().withCause(ignore).log("Invalid error response");
172+
return new ClientError();
173+
}
174+
}
175+
176+
@SuppressWarnings("unchecked") // safe because the key is a known header.
177+
@Nullable
178+
protected static String maybeGetLinkHeader(HttpResponse response) {
179+
HttpHeaders headers = response.getHeaders();
180+
List<String> link = (List<String>) headers.get("Link");
181+
if (link == null) {
182+
return null;
183+
}
184+
return Iterables.getFirst(link, null);
185+
}
186+
187+
/** Credentials for API should be optional for any read operation (GET). */
188+
@Nullable
189+
protected UserPassword getCredentialsIfPresent() throws RepoException {
190+
try {
191+
return getCredentials();
192+
} catch (ValidationException e) {
193+
String msg =
194+
String.format(
195+
"GitHub credentials not found in %s. Assuming the repository is public.", storePath);
196+
logger.atInfo().log("%s", msg);
197+
console.info(msg);
198+
return null;
199+
}
200+
}
201+
202+
/**
203+
* Gets the credentials from git credential helper. First we try to get it for the apiUrl host,
204+
* just in case the user has an specific token for that url, otherwise we use the webUrl host one.
205+
*/
206+
protected UserPassword getCredentials() throws RepoException, ValidationException {
207+
try {
208+
return repo.credentialFill(apiUrl);
209+
} catch (ValidationException e) {
210+
try {
211+
return repo.credentialFill(webUrl);
212+
} catch (ValidationException e1) {
213+
// Ugly, but helpful...
214+
throw new ValidationException(
215+
String.format(
216+
"Cannot get credentials for host %s or %s from"
217+
+ " credentials helper. Make sure either your credential helper has the"
218+
+ " username and password/token or if you don't use one, that file '%s'"
219+
+ " contains one of the two lines: \n"
220+
+ "Either:\n"
221+
+ "https://USERNAME:TOKEN@%s\n"
222+
+ "or:\n"
223+
+ "https://USERNAME:TOKEN@%s\n"
224+
+ "\n"
225+
+ "Note that spaces or other special characters need to be escaped. For example"
226+
+ " ' ' should be %%20 and '@' should be %%40 (For example when using the email"
227+
+ " as username)",
228+
webUrl, apiUrl, storePath, removeHttpsPrefix(apiUrl), removeHttpsPrefix(webUrl)),
229+
e1);
230+
}
231+
}
232+
}
233+
234+
private static String removeHttpsPrefix(String url) {
235+
return url.replace("https://", "");
236+
}
237+
238+
protected HttpRequestFactory getHttpRequestFactory(
239+
@Nullable UserPassword userPassword, ImmutableListMultimap<String, String> headers) {
240+
return httpTransport.createRequestFactory(
241+
request -> {
242+
request.setConnectTimeout((int) Duration.ofMinutes(1).toMillis());
243+
request.setReadTimeout((int) Duration.ofMinutes(1).toMillis());
244+
HttpHeaders httpHeaders = new HttpHeaders();
245+
if (userPassword != null) {
246+
if (bearerAuth) {
247+
httpHeaders.setAuthorization("Bearer " + userPassword.getPassword_BeCareful());
248+
} else {
249+
httpHeaders.setBasicAuthentication(
250+
userPassword.getUsername(), userPassword.getPassword_BeCareful());
251+
}
252+
}
253+
for (Map.Entry<String, Collection<String>> header : headers.asMap().entrySet()) {
254+
httpHeaders.put(header.getKey(), header.getValue());
255+
}
256+
request.setHeaders(httpHeaders);
257+
request.setParser(new JsonObjectParser(JSON_FACTORY));
258+
});
259+
}
260+
261+
private static String buildWebUrl(String hostName) {
262+
return "https://" + hostName;
263+
}
264+
265+
private static String determineApiUrl(String hostName) {
266+
// Github.com has a unique API URL.
267+
// GitHub Enterprise instances have a specific format of API URL.
268+
if (hostName.equals(GITHUB_DOT_COM_WEB_URL)) {
269+
return GITHUB_DOT_COM_API_URL;
270+
}
271+
return hostName + "/api/v3";
272+
}
273+
274+
@VisibleForTesting
275+
public String getApiUrl() {
276+
return apiUrl;
277+
}
278+
279+
@VisibleForTesting
280+
public String getWebUrl() {
281+
return webUrl;
282+
}
283+
}

0 commit comments

Comments
 (0)