installationTokens();
+
+ protected abstract GitHubClientConfig clientConfig();
+
+ protected abstract OkHttpClient client();
+
+ private static boolean isJwtRequest(final String path) {
+ return path.startsWith("/app/installation") || path.endsWith("installation");
+ }
+
+ /*
+ Generates the Authentication header, given the API endpoint and the credentials provided.
+
+ GitHub Requests can be authenticated in 3 different ways.
+ (1) Regular, static access token;
+ (2) JWT Token, generated from a private key. Used in Github Apps;
+ (3) Installation Token, generated from the JWT token. Also used in Github Apps.
+ */
+ public String getAuthorizationHeader(final String path) {
+ var config = clientConfig();
+ if (isJwtRequest(path) && config.privateKey().isEmpty()) {
+ throw new IllegalStateException("This endpoint needs a client with a private key for an App");
+ }
+ if (config.accessToken().isPresent()) {
+ return String.format("token %s", config.accessToken().get());
+ } else if (config.privateKey().isPresent()) {
+ final String jwtToken;
+ try {
+ jwtToken =
+ JwtTokenIssuer.fromPrivateKey(config.privateKey().get()).getToken(config.appId().get());
+ } catch (Exception e) {
+ throw new RuntimeException("There was an error generating JWT token", e);
+ }
+ if (isJwtRequest(path)) {
+ return String.format("Bearer %s", jwtToken);
+ }
+ if (config.installationId().isEmpty()) {
+ throw new RuntimeException("This endpoint needs a client with an installation ID");
+ }
+ try {
+ return String.format(
+ "token %s", getInstallationToken(jwtToken, config.installationId().get()));
+ } catch (Exception e) {
+ throw new RuntimeException("Could not generate access token for github app", e);
+ }
+ }
+ throw new RuntimeException("Not possible to authenticate. ");
+ }
+
+ private boolean isExpired(final AccessToken token) {
+ // Adds a few minutes to avoid making calls with an expired token due to clock differences
+ return token.expiresAt().isBefore(ZonedDateTime.now().plusMinutes(EXPIRY_MARGIN_IN_MINUTES));
+ }
+
+ private String getInstallationToken(final String jwtToken, final int installationId)
+ throws Exception {
+
+ AccessToken installationToken = installationTokens().get(installationId);
+
+ if (installationToken == null || isExpired(installationToken)) {
+ log.info(
+ "Github token for installation {} is either expired or null. Trying to get a new one.",
+ installationId);
+ installationToken = generateInstallationToken(jwtToken, installationId);
+ installationTokens().put(installationId, installationToken);
+ }
+ return installationToken.token();
+ }
+
+ /**
+ * Create a URL for a given path to this GitHub server.
+ *
+ * @param path relative URI
+ * @return URL to path on this server
+ */
+ public Optional urlFor(final String path) {
+ return clientConfig()
+ .baseUrl()
+ .map(
+ baseUrl -> baseUrl.toString().replaceAll("/+$", "") + "/" + path.replaceAll("^/+", ""));
+ }
+
+ private AccessToken generateInstallationToken(final String jwtToken, final int installationId)
+ throws Exception {
+ log.info("Got JWT Token. Now getting Github access_token for installation {}", installationId);
+ final String accessTokenUrl =
+ urlFor(GET_ACCESS_TOKEN_URL)
+ .orElseThrow(() -> new IllegalStateException("No baseUrl defined"));
+ final String url = String.format(accessTokenUrl, installationId);
+ final Request request =
+ new Request.Builder()
+ .addHeader("Accept", "application/vnd.github.machine-man-preview+json")
+ .addHeader("Authorization", "Bearer " + jwtToken)
+ .url(url)
+ .method("POST", RequestBody.create(parse(MediaType.APPLICATION_JSON), ""))
+ .build();
+
+ final Response response = client().newCall(request).execute();
+
+ if (!response.isSuccessful()) {
+ throw new Exception(
+ String.format(
+ "Got non-2xx status %s when getting an access token from GitHub: %s",
+ response.code(), response.message()));
+ }
+
+ if (response.body() == null) {
+ throw new Exception(
+ String.format(
+ "Got empty response body when getting an access token from GitHub, HTTP status was: %s",
+ response.message()));
+ }
+ final String text = response.body().string();
+ response.body().close();
+ return Json.create().fromJson(text, AccessToken.class);
+ }
+
+ protected abstract RequestNotOkException mapException(Response res, Request request)
+ throws IOException;
+
+ protected abstract CompletableFuture processPossibleRedirects(
+ Response response, AtomicBoolean redirected);
+
+ protected CompletableFuture call(final Request request) {
+ final Call call = client().newCall(request);
+
+ final CompletableFuture future = new CompletableFuture<>();
+
+ // avoid multiple redirects
+ final AtomicBoolean redirected = new AtomicBoolean(false);
+
+ call.enqueue(
+ new Callback() {
+ @Override
+ public void onFailure(@NotNull final Call call, final IOException e) {
+ future.completeExceptionally(e);
+ }
+
+ @Override
+ public void onResponse(@NotNull final Call call, final Response response) {
+ processPossibleRedirects(response, redirected)
+ .handle(
+ (res, ex) -> {
+ if (Objects.nonNull(ex)) {
+ future.completeExceptionally(ex);
+ } else if (!res.isSuccessful()) {
+ try {
+ future.completeExceptionally(mapException(res, request));
+ } catch (final Throwable e) {
+ future.completeExceptionally(e);
+ } finally {
+ if (res.body() != null) {
+ res.body().close();
+ }
+ }
+ } else {
+ future.complete(res);
+ }
+ return res;
+ });
+ }
+ });
+ tracer.span(request.url().toString(), request.method(), future);
+ return future;
+ }
+}
diff --git a/src/main/java/com/spotify/github/http/GitHubClientConfig.java b/src/main/java/com/spotify/github/http/GitHubClientConfig.java
new file mode 100644
index 00000000..3f78aa24
--- /dev/null
+++ b/src/main/java/com/spotify/github/http/GitHubClientConfig.java
@@ -0,0 +1,48 @@
+/*-
+ * -\-\-
+ * github-api
+ * --
+ * Copyright (C) 2016 - 2020 Spotify AB
+ * --
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * -/-/-
+ */
+package com.spotify.github.http;
+
+import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
+import com.fasterxml.jackson.databind.annotation.JsonSerialize;
+import com.spotify.github.GithubStyle;
+import java.net.URI;
+import java.util.Optional;
+import okhttp3.OkHttpClient;
+import org.immutables.value.Value;
+
+@Value.Immutable
+@GithubStyle
+@JsonSerialize(as = ImmutableGitHubClientConfig.class)
+@JsonDeserialize(as = ImmutableGitHubClientConfig.class)
+public interface GitHubClientConfig {
+ OkHttpClient client();
+
+ Optional baseUrl();
+
+ Optional graphqlApiUrl();
+
+ Optional accessToken();
+
+ Optional privateKey();
+
+ Optional appId();
+
+ Optional installationId();
+}
diff --git a/src/main/java/com/spotify/github/v3/clients/GitHubClient.java b/src/main/java/com/spotify/github/v3/clients/GitHubClient.java
index 8f41df5e..4dd64b9a 100644
--- a/src/main/java/com/spotify/github/v3/clients/GitHubClient.java
+++ b/src/main/java/com/spotify/github/v3/clients/GitHubClient.java
@@ -25,6 +25,9 @@
import com.fasterxml.jackson.core.type.TypeReference;
import com.spotify.github.Tracer;
+import com.spotify.github.http.AbstractGitHubApiClient;
+import com.spotify.github.http.GitHubClientConfig;
+import com.spotify.github.http.ImmutableGitHubClientConfig;
import com.spotify.github.jackson.Json;
import com.spotify.github.v3.Team;
import com.spotify.github.v3.User;
@@ -41,70 +44,60 @@
import com.spotify.github.v3.repos.CommitItem;
import com.spotify.github.v3.repos.FolderContent;
import com.spotify.github.v3.repos.Repository;
-import com.spotify.github.v3.repos.Status;
import com.spotify.github.v3.repos.RepositoryInvitation;
-
+import com.spotify.github.v3.repos.Status;
import java.io.*;
import java.lang.invoke.MethodHandles;
import java.net.URI;
-import java.time.ZonedDateTime;
+import java.util.HashMap;
import java.util.List;
import java.util.Map;
-import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
-import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Consumer;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MediaType;
-
import okhttp3.*;
import org.apache.commons.io.FileUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
- * Github client is a main communication entry point. Provides lower level communication
- * functionality as well as acts as a factory for the higher level API clients.
+ * GitHub client is a main communication entry point for the REST APIs. Provides lower level
+ * communication functionality as well as acts as a factory for the higher level API clients.
*/
-public class GitHubClient {
+public class GitHubClient extends AbstractGitHubApiClient {
private static final int EXPIRY_MARGIN_IN_MINUTES = 5;
-
- private Tracer tracer = NoopTracer.INSTANCE;
-
- static final Consumer IGNORE_RESPONSE_CONSUMER = (response) -> {
- if (response.body() != null) {
- response.body().close();
- }
- };
- static final TypeReference> LIST_COMMENT_TYPE_REFERENCE =
- new TypeReference<>() {};
- static final TypeReference> LIST_REPOSITORY =
- new TypeReference<>() {};
+ private final GitHubClientConfig clientConfig;
+
+ static final Consumer IGNORE_RESPONSE_CONSUMER =
+ (response) -> {
+ if (response.body() != null) {
+ response.body().close();
+ }
+ };
+ static final TypeReference> LIST_COMMENT_TYPE_REFERENCE = new TypeReference<>() {};
+ static final TypeReference> LIST_REPOSITORY = new TypeReference<>() {};
static final TypeReference> LIST_COMMIT_TYPE_REFERENCE =
new TypeReference<>() {};
static final TypeReference> LIST_REVIEW_TYPE_REFERENCE = new TypeReference<>() {};
static final TypeReference LIST_REVIEW_REQUEST_TYPE_REFERENCE =
new TypeReference<>() {};
- static final TypeReference> LIST_STATUS_TYPE_REFERENCE =
- new TypeReference<>() {};
+ static final TypeReference> LIST_STATUS_TYPE_REFERENCE = new TypeReference<>() {};
static final TypeReference> LIST_FOLDERCONTENT_TYPE_REFERENCE =
new TypeReference<>() {};
static final TypeReference> LIST_PR_TYPE_REFERENCE =
new TypeReference<>() {};
- static final TypeReference> LIST_BRANCHES =
+ static final TypeReference> LIST_BRANCHES = new TypeReference<>() {};
+ static final TypeReference> LIST_REFERENCES = new TypeReference<>() {};
+ static final TypeReference> LIST_REPOSITORY_INVITATION =
new TypeReference<>() {};
- static final TypeReference> LIST_REFERENCES =
- new TypeReference<>() {};
- static final TypeReference> LIST_REPOSITORY_INVITATION = new TypeReference<>() {};
- static final TypeReference> LIST_TEAMS =
- new TypeReference<>() {};
+ static final TypeReference> LIST_TEAMS = new TypeReference<>() {};
- static final TypeReference> LIST_TEAM_MEMBERS =
- new TypeReference<>() {};
+ static final TypeReference> LIST_TEAM_MEMBERS = new TypeReference<>() {};
static final TypeReference> LIST_PENDING_TEAM_INVITATIONS =
new TypeReference<>() {};
@@ -119,7 +112,6 @@ public class GitHubClient {
private final URI baseUrl;
- private final Optional graphqlUrl;
private final Json json = Json.create();
private final OkHttpClient client;
private final String token;
@@ -133,63 +125,93 @@ public class GitHubClient {
private GitHubClient(
final OkHttpClient client,
final URI baseUrl,
- final URI graphqlUrl,
final String accessToken,
final byte[] privateKey,
final Integer appId,
final Integer installationId) {
- this.baseUrl = baseUrl;
- this.graphqlUrl = Optional.ofNullable(graphqlUrl);
- this.token = accessToken;
- this.client = client;
- this.privateKey = privateKey;
- this.appId = appId;
- this.installationId = installationId;
- this.installationTokens = new ConcurrentHashMap<>();
+
+ this.clientConfig =
+ ImmutableGitHubClientConfig.builder()
+ .client(client)
+ .baseUrl(Optional.ofNullable(baseUrl))
+ .accessToken(Optional.ofNullable(accessToken))
+ .privateKey(Optional.ofNullable(privateKey))
+ .appId(Optional.ofNullable(appId))
+ .installationId(Optional.ofNullable(installationId))
+ .build();
+ this.baseUrl = clientConfig.baseUrl().orElse(null);
+ this.token = clientConfig.accessToken().orElse(null);
+ this.client = clientConfig.client();
+ this.privateKey = clientConfig.privateKey().orElse(null);
+ this.appId = clientConfig.appId().orElse(null);
+ this.installationId = clientConfig.installationId().orElse(null);
+ this.installationTokens = new HashMap<>();
+ }
+
+ private GitHubClient(final GitHubClientConfig config) {
+ this.baseUrl = config.baseUrl().orElse(null);
+ this.token = config.accessToken().orElse(null);
+ this.client = config.client();
+ this.privateKey = config.privateKey().orElse(null);
+ this.appId = config.appId().orElse(null);
+ this.installationId = config.installationId().orElse(null);
+ this.installationTokens = new HashMap<>();
+ this.clientConfig = config;
+ }
+
+ /**
+ * Create a github api client with a given base URL and authorization token.
+ *
+ * @param config GitHubClientConfig object
+ * @return github api client
+ */
+ public static GitHubClient create(final GitHubClientConfig config) {
+ return new GitHubClient(config);
}
/**
* Create a github api client with a given base URL and authorization token.
*
+ * @deprecated use {@link #create(GitHubClientConfig)} instead
* @param baseUrl base URL
* @param token authorization token
* @return github api client
*/
public static GitHubClient create(final URI baseUrl, final String token) {
- return new GitHubClient(new OkHttpClient(), baseUrl, null, token, null, null, null);
- }
-
- public static GitHubClient create(final URI baseUrl, final URI graphqlUri, final String token) {
- return new GitHubClient(new OkHttpClient(), baseUrl, graphqlUri, token, null, null, null);
+ return new GitHubClient(new OkHttpClient(), baseUrl, token, null, null, null);
}
/**
* Create a github api client with a given base URL and a path to a key.
*
+ * @deprecated use {@link #create(GitHubClientConfig)} instead
* @param baseUrl base URL
* @param privateKey the private key PEM file
* @param appId the github app ID
* @return github api client
*/
public static GitHubClient create(final URI baseUrl, final File privateKey, final Integer appId) {
- return createOrThrow(new OkHttpClient(), baseUrl, null, privateKey, appId, null);
+ return createOrThrow(new OkHttpClient(), baseUrl, privateKey, appId, null);
}
/**
* Create a github api client with a given base URL and a path to a key.
*
+ * @deprecated use {@link #create(GitHubClientConfig)} instead
* @param baseUrl base URL
* @param privateKey the private key as byte array
* @param appId the github app ID
* @return github api client
*/
- public static GitHubClient create(final URI baseUrl, final byte[] privateKey, final Integer appId) {
- return new GitHubClient(new OkHttpClient(), baseUrl, null, null, privateKey, appId, null);
+ public static GitHubClient create(
+ final URI baseUrl, final byte[] privateKey, final Integer appId) {
+ return new GitHubClient(new OkHttpClient(), baseUrl, null, privateKey, appId, null);
}
/**
* Create a github api client with a given base URL and a path to a key.
*
+ * @deprecated use {@link #create(GitHubClientConfig)} instead
* @param baseUrl base URL
* @param privateKey the private key PEM file
* @param appId the github app ID
@@ -198,12 +220,13 @@ public static GitHubClient create(final URI baseUrl, final byte[] privateKey, fi
*/
public static GitHubClient create(
final URI baseUrl, final File privateKey, final Integer appId, final Integer installationId) {
- return createOrThrow(new OkHttpClient(), baseUrl, null, privateKey, appId, installationId);
+ return createOrThrow(new OkHttpClient(), baseUrl, privateKey, appId, installationId);
}
/**
* Create a github api client with a given base URL and a path to a key.
*
+ * @deprecated use {@link #create(GitHubClientConfig)} instead
* @param baseUrl base URL
* @param privateKey the private key as byte array
* @param appId the github app ID
@@ -211,13 +234,17 @@ public static GitHubClient create(
* @return github api client
*/
public static GitHubClient create(
- final URI baseUrl, final byte[] privateKey, final Integer appId, final Integer installationId) {
- return new GitHubClient(new OkHttpClient(), baseUrl, null, null, privateKey, appId, installationId);
+ final URI baseUrl,
+ final byte[] privateKey,
+ final Integer appId,
+ final Integer installationId) {
+ return new GitHubClient(new OkHttpClient(), baseUrl, null, privateKey, appId, installationId);
}
/**
* Create a github api client with a given base URL and a path to a key.
*
+ * @deprecated use {@link #create(GitHubClientConfig)} instead
* @param httpClient an instance of OkHttpClient
* @param baseUrl base URL
* @param privateKey the private key PEM file
@@ -229,30 +256,13 @@ public static GitHubClient create(
final URI baseUrl,
final File privateKey,
final Integer appId) {
- return createOrThrow(httpClient, baseUrl, null, privateKey, appId, null);
- }
-
- /**
- * Create a github api client with a given base URL and a path to a key.
- *
- * @param httpClient an instance of OkHttpClient
- * @param baseUrl base URL
- * @param privateKey the private key PEM file
- * @param appId the github app ID
- * @return github api client
- */
- public static GitHubClient create(
- final OkHttpClient httpClient,
- final URI baseUrl,
- final URI graphqlUrl,
- final File privateKey,
- final Integer appId) {
- return createOrThrow(httpClient, baseUrl, graphqlUrl, privateKey, appId, null);
+ return createOrThrow(httpClient, baseUrl, privateKey, appId, null);
}
/**
* Create a github api client with a given base URL and a path to a key.
*
+ * @deprecated use {@link #create(GitHubClientConfig)} instead
* @param httpClient an instance of OkHttpClient
* @param baseUrl base URL
* @param privateKey the private key as byte array
@@ -260,15 +270,13 @@ public static GitHubClient create(
* @return github api client
*/
public static GitHubClient create(
- final OkHttpClient httpClient,
- final URI baseUrl,
- final byte[] privateKey,
- final Integer appId) {
- return new GitHubClient(httpClient, baseUrl, null, null, privateKey, appId, null);
+ final OkHttpClient httpClient,
+ final URI baseUrl,
+ final byte[] privateKey,
+ final Integer appId) {
+ return new GitHubClient(httpClient, baseUrl, null, privateKey, appId, null);
}
-
-
/**
* Create a github api client with a given base URL and a path to a key.
*
@@ -284,7 +292,7 @@ public static GitHubClient create(
final File privateKey,
final Integer appId,
final Integer installationId) {
- return createOrThrow(httpClient, baseUrl, null, privateKey, appId, installationId);
+ return createOrThrow(httpClient, baseUrl, privateKey, appId, installationId);
}
/**
@@ -297,17 +305,18 @@ public static GitHubClient create(
* @return github api client
*/
public static GitHubClient create(
- final OkHttpClient httpClient,
- final URI baseUrl,
- final byte[] privateKey,
- final Integer appId,
- final Integer installationId) {
- return new GitHubClient(httpClient, baseUrl, null, null, privateKey, appId, installationId);
+ final OkHttpClient httpClient,
+ final URI baseUrl,
+ final byte[] privateKey,
+ final Integer appId,
+ final Integer installationId) {
+ return new GitHubClient(httpClient, baseUrl, null, privateKey, appId, installationId);
}
/**
* Create a github api client with a given base URL and authorization token.
*
+ * @deprecated use {@link #create(GitHubClientConfig)} instead
* @param httpClient an instance of OkHttpClient
* @param baseUrl base URL
* @param token authorization token
@@ -315,12 +324,7 @@ public static GitHubClient create(
*/
public static GitHubClient create(
final OkHttpClient httpClient, final URI baseUrl, final String token) {
- return new GitHubClient(httpClient, baseUrl, null, token, null, null, null);
- }
-
- public static GitHubClient create(
- final OkHttpClient httpClient, final URI baseUrl, final URI graphqlUrl, final String token) {
- return new GitHubClient(httpClient, baseUrl, graphqlUrl, token, null, null, null);
+ return new GitHubClient(httpClient, baseUrl, token, null, null, null);
}
/**
@@ -339,7 +343,6 @@ public static GitHubClient scopeForInstallationId(
client.client,
client.baseUrl,
null,
- null,
client.getPrivateKey().get(),
client.appId,
installationId);
@@ -357,14 +360,7 @@ public GitHubClient withScopeForInstallationId(final int installationId) {
if (Optional.ofNullable(privateKey).isEmpty()) {
throw new RuntimeException("Installation ID scoped client needs a private key");
}
- return new GitHubClient(
- client,
- baseUrl,
- graphqlUrl.orElse(null),
- null,
- privateKey,
- appId,
- installationId);
+ return new GitHubClient(client, baseUrl, null, privateKey, appId, installationId);
}
public GitHubClient withTracer(final Tracer tracer) {
@@ -431,11 +427,6 @@ public OrganisationClient createOrganisationClient(final String org) {
return OrganisationClient.create(this, org);
}
- /**
- * Create user API client
- *
- * @return user API client
- */
public UserClient createUserClient(final String owner) {
return UserClient.create(this, owner);
}
@@ -452,7 +443,7 @@ Json json() {
*/
CompletableFuture request(final String path) {
final Request request = requestBuilder(path).build();
- log.debug("Making request to {}", request.url().toString());
+ log.debug("Making request to {}", request.url());
return call(request);
}
@@ -467,7 +458,7 @@ CompletableFuture request(final String path, final Map
final Request.Builder builder = requestBuilder(path);
extraHeaders.forEach(builder::addHeader);
final Request request = builder.build();
- log.debug("Making request to {}", request.url().toString());
+ log.debug("Making request to {}", request.url());
return call(request);
}
@@ -479,7 +470,7 @@ CompletableFuture request(final String path, final Map
*/
CompletableFuture request(final String path, final Class clazz) {
final Request request = requestBuilder(path).build();
- log.debug("Making request to {}", request.url().toString());
+ log.debug("Making request to {}", request.url());
return call(request)
.thenApply(body -> json().fromJsonUncheckedNotNull(responseBodyUnchecked(body), clazz));
}
@@ -496,7 +487,7 @@ CompletableFuture request(
final Request.Builder builder = requestBuilder(path);
extraHeaders.forEach(builder::addHeader);
final Request request = builder.build();
- log.debug("Making request to {}", request.url().toString());
+ log.debug("Making request to {}", request.url());
return call(request)
.thenApply(body -> json().fromJsonUncheckedNotNull(responseBodyUnchecked(body), clazz));
}
@@ -515,7 +506,7 @@ CompletableFuture request(
final Request.Builder builder = requestBuilder(path);
extraHeaders.forEach(builder::addHeader);
final Request request = builder.build();
- log.debug("Making request to {}", request.url().toString());
+ log.debug("Making request to {}", request.url());
return call(request)
.thenApply(
response ->
@@ -530,7 +521,7 @@ CompletableFuture request(
*/
CompletableFuture request(final String path, final TypeReference typeReference) {
final Request request = requestBuilder(path).build();
- log.debug("Making request to {}", request.url().toString());
+ log.debug("Making request to {}", request.url());
return call(request)
.thenApply(
response ->
@@ -549,7 +540,7 @@ CompletableFuture post(final String path, final String data) {
requestBuilder(path)
.method("POST", RequestBody.create(parse(MediaType.APPLICATION_JSON), data))
.build();
- log.debug("Making POST request to {}", request.url().toString());
+ log.debug("Making POST request to {}", request.url());
return call(request);
}
@@ -568,7 +559,7 @@ CompletableFuture post(
.method("POST", RequestBody.create(parse(MediaType.APPLICATION_JSON), data));
extraHeaders.forEach(builder::addHeader);
final Request request = builder.build();
- log.debug("Making POST request to {}", request.url().toString());
+ log.debug("Making POST request to {}", request.url());
return call(request);
}
@@ -605,23 +596,6 @@ CompletableFuture post(final String path, final String data, final Class<
response -> json().fromJsonUncheckedNotNull(responseBodyUnchecked(response), clazz));
}
- /**
- * Make a POST request to the graphql endpoint of Github
- *
- * @param data request body as stringified JSON
- * @return response
- *
- * @see "https://docs.github.com/en/enterprise-server@3.9/graphql/guides/forming-calls-with-graphql#communicating-with-graphql"
- */
- public CompletableFuture postGraphql(final String data) {
- final Request request =
- graphqlRequestBuilder()
- .method("POST", RequestBody.create(parse(MediaType.APPLICATION_JSON), data))
- .build();
- log.info("Making POST request to {}", request.url());
- return call(request);
- }
-
/**
* Make an http PUT request for the given path with provided JSON body.
*
@@ -634,7 +608,7 @@ CompletableFuture put(final String path, final String data) {
requestBuilder(path)
.method("PUT", RequestBody.create(parse(MediaType.APPLICATION_JSON), data))
.build();
- log.debug("Making POST request to {}", request.url().toString());
+ log.debug("Making POST request to {}", request.url());
return call(request);
}
@@ -664,7 +638,7 @@ CompletableFuture patch(final String path, final String data) {
requestBuilder(path)
.method("PATCH", RequestBody.create(parse(MediaType.APPLICATION_JSON), data))
.build();
- log.debug("Making PATCH request to {}", request.url().toString());
+ log.debug("Making PATCH request to {}", request.url());
return call(request);
}
@@ -700,7 +674,7 @@ CompletableFuture patch(
.method("PATCH", RequestBody.create(parse(MediaType.APPLICATION_JSON), data));
extraHeaders.forEach(builder::addHeader);
final Request request = builder.build();
- log.debug("Making PATCH request to {}", request.url().toString());
+ log.debug("Making PATCH request to {}", request.url());
return call(request)
.thenApply(
response -> json().fromJsonUncheckedNotNull(responseBodyUnchecked(response), clazz));
@@ -714,7 +688,7 @@ CompletableFuture patch(
*/
CompletableFuture delete(final String path) {
final Request request = requestBuilder(path).delete().build();
- log.debug("Making DELETE request to {}", request.url().toString());
+ log.debug("Making DELETE request to {}", request.url());
return call(request);
}
@@ -730,24 +704,15 @@ CompletableFuture delete(final String path, final String data) {
requestBuilder(path)
.method("DELETE", RequestBody.create(parse(MediaType.APPLICATION_JSON), data))
.build();
- log.debug("Making DELETE request to {}", request.url().toString());
+ log.debug("Making DELETE request to {}", request.url());
return call(request);
}
- /**
- * Create a URL for a given path to this Github server.
- *
- * @param path relative URI
- * @return URL to path on this server
- */
- String urlFor(final String path) {
- return baseUrl.toString().replaceAll("/+$", "") + "/" + path.replaceAll("^/+", "");
- }
-
private Request.Builder requestBuilder(final String path) {
+ String url = urlFor(path).orElseThrow(() -> new IllegalStateException("No baseUrl defined"));
final Request.Builder builder =
new Request.Builder()
- .url(urlFor(path))
+ .url(url)
.addHeader(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON)
.addHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON);
builder.addHeader(HttpHeaders.AUTHORIZATION, getAuthorizationHeader(path));
@@ -755,172 +720,40 @@ private Request.Builder requestBuilder(final String path) {
return builder;
}
- private Request.Builder graphqlRequestBuilder() {
- URI url = graphqlUrl.orElseThrow(() -> new IllegalStateException("No graphql url set"));
- final Request.Builder builder =
- new Request.Builder()
- .url(url.toString())
- .addHeader(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON)
- .addHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON);
- builder.addHeader(HttpHeaders.AUTHORIZATION, getAuthorizationHeader("/graphql"));
- return builder;
+ @Override
+ protected Map installationTokens() {
+ return this.installationTokens;
}
- public boolean isGraphqlEnabled() {
- return graphqlUrl.isPresent();
+ @Override
+ protected GitHubClientConfig clientConfig() {
+ return this.clientConfig;
}
-
- /*
- Generates the Authentication header, given the API endpoint and the credentials provided.
-
- Github Requests can be authenticated in 3 different ways.
- (1) Regular, static access token;
- (2) JWT Token, generated from a private key. Used in Github Apps;
- (3) Installation Token, generated from the JWT token. Also used in Github Apps.
- */
- private String getAuthorizationHeader(final String path) {
- if (isJwtRequest(path) && getPrivateKey().isEmpty()) {
- throw new IllegalStateException("This endpoint needs a client with a private key for an App");
- }
- if (getAccessToken().isPresent()) {
- return String.format("token %s", token);
- } else if (getPrivateKey().isPresent()) {
- final String jwtToken;
- try {
- jwtToken = JwtTokenIssuer.fromPrivateKey(privateKey).getToken(appId);
- } catch (Exception e) {
- throw new RuntimeException("There was an error generating JWT token", e);
- }
- if (isJwtRequest(path)) {
- return String.format("Bearer %s", jwtToken);
- }
- if (installationId == null) {
- throw new RuntimeException("This endpoint needs a client with an installation ID");
- }
- try {
- return String.format("token %s", getInstallationToken(jwtToken, installationId));
- } catch (Exception e) {
- throw new RuntimeException("Could not generate access token for github app", e);
- }
- }
- throw new RuntimeException("Not possible to authenticate. ");
- }
-
- private boolean isJwtRequest(final String path) {
- return path.startsWith("/app/installation") || path.endsWith("installation");
- }
-
- private String getInstallationToken(final String jwtToken, final int installationId)
- throws Exception {
-
- AccessToken installationToken = installationTokens.get(installationId);
-
- if (installationToken == null || isExpired(installationToken)) {
- log.info(
- "Github token for installation {} is either expired or null. Trying to get a new one.",
- installationId);
- installationToken = generateInstallationToken(jwtToken, installationId);
- installationTokens.put(installationId, installationToken);
- }
- return installationToken.token();
+ @Override
+ protected OkHttpClient client() {
+ return client;
}
- private boolean isExpired(final AccessToken token) {
- // Adds a few minutes to avoid making calls with an expired token due to clock differences
- return token.expiresAt().isBefore(ZonedDateTime.now().plusMinutes(EXPIRY_MARGIN_IN_MINUTES));
- }
-
- private AccessToken generateInstallationToken(final String jwtToken, final int installationId)
- throws Exception {
- log.info("Got JWT Token. Now getting Github access_token for installation {}", installationId);
- final String url = String.format(urlFor(GET_ACCESS_TOKEN_URL), installationId);
- final Request request =
- new Request.Builder()
- .addHeader("Accept", "application/vnd.github.machine-man-preview+json")
- .addHeader("Authorization", "Bearer " + jwtToken)
- .url(url)
- .method("POST", RequestBody.create(parse(MediaType.APPLICATION_JSON), ""))
- .build();
-
- final Response response = client.newCall(request).execute();
-
- if (!response.isSuccessful()) {
- throw new Exception(
- String.format(
- "Got non-2xx status %s when getting an access token from GitHub: %s",
- response.code(), response.message()));
- }
-
- if (response.body() == null) {
- throw new Exception(
- String.format(
- "Got empty response body when getting an access token from GitHub, HTTP status was: %s",
- response.message()));
- }
- final String text = response.body().string();
- response.body().close();
- return Json.create().fromJson(text, AccessToken.class);
- }
-
- private CompletableFuture call(final Request request) {
- final Call call = client.newCall(request);
-
- final CompletableFuture future = new CompletableFuture<>();
-
- // avoid multiple redirects
- final AtomicBoolean redirected = new AtomicBoolean(false);
-
- call.enqueue(
- new Callback() {
- @Override
- public void onFailure(final Call call, final IOException e) {
- future.completeExceptionally(e);
- }
-
- @Override
- public void onResponse(final Call call, final Response response) {
- processPossibleRedirects(response, redirected)
- .handle(
- (res, ex) -> {
- if (Objects.nonNull(ex)) {
- future.completeExceptionally(ex);
- } else if (!res.isSuccessful()) {
- try {
- future.completeExceptionally(mapException(res, request));
- } catch (final Throwable e) {
- future.completeExceptionally(e);
- } finally {
- if (res.body() != null) {
- res.body().close();
- }
- }
- } else {
- future.complete(res);
- }
- return res;
- });
- }
- });
- tracer.span(request.url().toString(), request.method(), future);
- return future;
- }
-
- private RequestNotOkException mapException(final Response res, final Request request)
+ @Override
+ protected RequestNotOkException mapException(final Response res, final Request request)
throws IOException {
String bodyString = res.body() != null ? res.body().string() : "";
Map> headersMap = res.headers().toMultimap();
if (res.code() == FORBIDDEN) {
if (bodyString.contains("Repository was archived so is read-only")) {
- return new ReadOnlyRepositoryException(request.method(), request.url().encodedPath(), res.code(), bodyString, headersMap);
+ return new ReadOnlyRepositoryException(
+ request.method(), request.url().encodedPath(), res.code(), bodyString, headersMap);
}
}
- return new RequestNotOkException(request.method(), request.url().encodedPath(), res.code(), bodyString, headersMap);
+ return new RequestNotOkException(
+ request.method(), request.url().encodedPath(), res.code(), bodyString, headersMap);
}
- CompletableFuture processPossibleRedirects(
+ @Override
+ protected CompletableFuture processPossibleRedirects(
final Response response, final AtomicBoolean redirected) {
if (response.code() >= PERMANENT_REDIRECT
&& response.code() <= TEMPORARY_REDIRECT
@@ -940,12 +773,23 @@ CompletableFuture processPossibleRedirects(
return completedFuture(response);
}
- /**
- * Wrapper to Constructors that expose File object for the privateKey argument
- * */
- private static GitHubClient createOrThrow(final OkHttpClient httpClient, final URI baseUrl, final URI graphqlUrl, final File privateKey, final Integer appId, final Integer installationId) {
+ /** Wrapper to Constructors that expose File object for the privateKey argument */
+ private static GitHubClient createOrThrow(
+ final OkHttpClient httpClient,
+ final URI baseUrl,
+ final File privateKey,
+ final Integer appId,
+ final Integer installationId) {
+
try {
- return new GitHubClient(httpClient, baseUrl, graphqlUrl, null, FileUtils.readFileToByteArray(privateKey), appId, installationId);
+ return new GitHubClient(
+ ImmutableGitHubClientConfig.builder()
+ .baseUrl(Optional.ofNullable(baseUrl))
+ .privateKey(FileUtils.readFileToByteArray(privateKey))
+ .appId(Optional.ofNullable(appId))
+ .installationId(Optional.ofNullable(installationId))
+ .client(httpClient)
+ .build());
} catch (IOException e) {
throw new RuntimeException("There was an error generating JWT token", e);
}
diff --git a/src/main/java/com/spotify/github/v3/clients/GithubPage.java b/src/main/java/com/spotify/github/v3/clients/GithubPage.java
index 6bbd4ad0..b82a9d30 100644
--- a/src/main/java/com/spotify/github/v3/clients/GithubPage.java
+++ b/src/main/java/com/spotify/github/v3/clients/GithubPage.java
@@ -7,9 +7,9 @@
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
- *
+ *
* http://www.apache.org/licenses/LICENSE-2.0
- *
+ *
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -37,7 +37,6 @@
import java.util.NoSuchElementException;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
-import java.util.stream.Stream;
import okhttp3.ResponseBody;
/**
@@ -117,12 +116,14 @@ public CompletableFuture pagination() {
/** {@inheritDoc} */
@Override
public CompletableFuture> nextPage() {
+ String url =
+ github.urlFor("").orElseThrow(() -> new IllegalStateException("No baseUrl defined"));
return linkMapAsync()
.thenApply(
linkMap -> {
final String nextPath =
Optional.ofNullable(linkMap.get("next"))
- .map(nextLink -> nextLink.url().toString().replaceAll(github.urlFor(""), ""))
+ .map(nextLink -> nextLink.url().toString().replaceAll(url, ""))
.orElseThrow(() -> new NoSuchElementException("Page iteration exhausted"));
return new GithubPage<>(github, nextPath, typeReference);
});
@@ -155,18 +156,19 @@ public Iterator iterator() {
}
private CompletableFuture