Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 11 additions & 8 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -85,12 +85,12 @@ tasks.clean {

tasks {
withType<Test> {
useJUnitPlatform()
testLogging {
events("standardOut", "standardError", "skipped", "failed")
useJUnitPlatform()
testLogging {
events("standardOut", "standardError", "skipped", "failed")
}
jvmArgs("-XX:+EnableDynamicAgentLoading", "-Duser.timezone=UTC")
}
jvmArgs("-XX:+EnableDynamicAgentLoading")
}
}

spotless {
Expand All @@ -103,6 +103,7 @@ spotless {

val dropWizardVersion = "4.0.7"
val feignVersion = "13.6"
val gsonVersion = "2.12.1"
val jaxbVersion = "4.0.5"
val junitVersion = "5.13.4"
val mockitoVersion = "5.19.0"
Expand All @@ -111,14 +112,13 @@ val tripleaVersion = "2.7.15281"
dependencies {
implementation("at.favre.lib:bcrypt:0.10.2")
implementation("be.tomcools:dropwizard-websocket-jsr356-bundle:4.0.0")
implementation("com.google.code.gson:gson:$gsonVersion")
implementation("com.sun.mail:jakarta.mail:2.0.2")
implementation("com.sun.xml.bind:jaxb-core:$jaxbVersion")
implementation("com.sun.xml.bind:jaxb-impl:$jaxbVersion")
implementation("io.dropwizard:dropwizard-auth:$dropWizardVersion")
implementation("io.dropwizard:dropwizard-core:$dropWizardVersion")
implementation("io.dropwizard:dropwizard-jdbi3:$dropWizardVersion")
implementation("io.github.openfeign:feign-core:$feignVersion")
implementation("io.github.openfeign:feign-gson:$feignVersion")
implementation("javax.activation:activation:1.1.1")
implementation("javax.servlet:servlet-api:2.5")
implementation("javax.xml.bind:jaxb-api:2.3.1")
Expand All @@ -128,12 +128,15 @@ dependencies {
implementation("org.jdbi:jdbi3-sqlobject:3.49.5")
implementation("org.snakeyaml:snakeyaml-engine:2.10")
implementation("triplea:domain-data:$tripleaVersion")
implementation("triplea:feign-common:$tripleaVersion")
implementation("triplea:java-extras:$tripleaVersion")
implementation("triplea:lobby-client:$tripleaVersion")
implementation("triplea:websocket-client:$tripleaVersion")
runtimeOnly("org.postgresql:postgresql:42.7.7")

testImplementation("io.github.openfeign:feign-core:$feignVersion")
testImplementation("io.github.openfeign:feign-gson:$feignVersion")
testImplementation("triplea:feign-common:$tripleaVersion")

testImplementation("com.github.database-rider:rider-junit5:1.44.0")
testImplementation("com.github.npathai:hamcrest-optional:2.0.0")
testImplementation("com.sun.mail:jakarta.mail:2.0.2")
Expand Down
170 changes: 170 additions & 0 deletions src/main/java/org/triplea/http/client/github/GithubApiClient.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
package org.triplea.http.client.github;

import static org.slf4j.LoggerFactory.getLogger;

import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import java.io.IOException;
import java.lang.reflect.Type;
import java.net.URI;
import java.net.URLEncoder;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.time.Instant;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import javax.annotation.Nonnull;
import org.slf4j.Logger;

/** Can be used to interact with github's webservice API. */
class GithubApiClient implements GithubClient {

private static final Logger log = getLogger(GithubApiClient.class);

private static final String LIST_REPOS_PATH = "/orgs/%s/repos?per_page=100&page=%d";
private static final String BRANCHES_PATH = "/repos/%s/%s/branches/%s";
private static final String ISSUES_PATH = "/repos/%s/%s/issues";
private static final String LATEST_RELEASE_PATH = "/repos/%s/%s/releases/latest";

private static final Gson GSON = new Gson();

private final HttpClient httpClient;
private final URI baseUri;
private final String authToken;
private final String org;

/**
* @param baseUri The base URI for the Github webservice API.
* @param authToken Auth token sent to Github for webservice calls. Can be empty, but if specified
* must be valid (no auth token still works, but rate limits will be more restrictive).
* @param org Name of the Github org to be queried.
*/
GithubApiClient(@Nonnull URI baseUri, String authToken, @Nonnull String org) {
this.httpClient = HttpClient.newHttpClient();
this.baseUri = baseUri;
this.authToken = authToken;
this.org = org;
}

/**
* Returns a listing of the repositories within a github organization. This call handles paging,
* it returns a complete list and may perform multiple calls to Github.
*
* <p>Example equivalent cUrl call:
*
* <p>curl https://api.github.com/orgs/triplea-maps/repos
*/
@Override
public Collection<MapRepoListing> listRepositories() {
final Collection<MapRepoListing> allRepos = new HashSet<>();
int pageNumber = 1;
Collection<MapRepoListing> repos = listRepositories(pageNumber);
while (!repos.isEmpty()) {
pageNumber++;
allRepos.addAll(repos);
repos = listRepositories(pageNumber);
}
return allRepos;
}

private Collection<MapRepoListing> listRepositories(int pageNumber) {
String path = String.format(LIST_REPOS_PATH, encodePath(org), pageNumber);
Type listType = new TypeToken<List<MapRepoListing>>() {}.getType();
return GSON.fromJson(sendGet(path), listType);
}

@Override
public Instant getLatestCommitDate(String repoName, String branchName) {
String path =
String.format(BRANCHES_PATH, encodePath(org), encodePath(repoName), encodePath(branchName));
return GSON.fromJson(sendGet(path), BranchInfoResponse.class).getLastCommitDate();
}

@Override
public BranchInfoResponse fetchBranchInfo(String repo, String branch) {
String path =
String.format(BRANCHES_PATH, encodePath(org), encodePath(repo), encodePath(branch));
return GSON.fromJson(sendGet(path), BranchInfoResponse.class);
}

@Override
public Optional<String> fetchLatestVersion(String repo) {
String path = String.format(LATEST_RELEASE_PATH, encodePath(org), encodePath(repo));
try {
return Optional.of(GSON.fromJson(sendGet(path), LatestReleaseResponse.class).getTagName());
} catch (RuntimeException e) {
log.warn("No data received from server for latest engine version", e);
return Optional.empty();
}
}

@Override
public CreateIssueResponse newIssue(String repo, CreateIssueRequest createIssueRequest) {
String path = String.format(ISSUES_PATH, encodePath(org), encodePath(repo));
return GSON.fromJson(sendPost(path, createIssueRequest), CreateIssueResponse.class);
}

private static String encodePath(String segment) {
return URLEncoder.encode(segment, StandardCharsets.UTF_8).replace("+", "%20");
}

private String sendGet(String path) {
HttpRequest.Builder builder = HttpRequest.newBuilder().uri(baseUri.resolve(path)).GET();
if (authToken != null && !authToken.isEmpty()) {
builder.header("Authorization", "token " + authToken);
}
try {
HttpResponse<String> response =
httpClient.send(builder.build(), HttpResponse.BodyHandlers.ofString());
if (response.statusCode() < 200 || response.statusCode() >= 300) {
throw new RuntimeException(
"GitHub API request to "
+ path
+ " failed with status "
+ response.statusCode()
+ ": "
+ response.body());
}
return response.body();
} catch (IOException e) {
throw new RuntimeException("GitHub API request failed: " + path, e);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException("GitHub API request interrupted: " + path, e);
}
}

private String sendPost(String path, Object body) {
HttpRequest.Builder builder =
HttpRequest.newBuilder()
.uri(baseUri.resolve(path))
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(GSON.toJson(body)));
if (authToken != null && !authToken.isEmpty()) {
builder.header("Authorization", "token " + authToken);
}
try {
HttpResponse<String> response =
httpClient.send(builder.build(), HttpResponse.BodyHandlers.ofString());
if (response.statusCode() < 200 || response.statusCode() >= 300) {
throw new RuntimeException(
"GitHub API request to "
+ path
+ " failed with status "
+ response.statusCode()
+ ": "
+ response.body());
}
return response.body();
} catch (IOException e) {
throw new RuntimeException("GitHub API request failed: " + path, e);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException("GitHub API request interrupted: " + path, e);
}
}
}
136 changes: 10 additions & 126 deletions src/main/java/org/triplea/http/client/github/GithubClient.java
Original file line number Diff line number Diff line change
@@ -1,144 +1,28 @@
package org.triplea.http.client.github;

import static org.slf4j.LoggerFactory.getLogger;

import com.google.common.base.Strings;
import feign.FeignException;
import feign.Param;
import feign.QueryMap;
import feign.RequestLine;
import java.net.URI;
import java.time.Instant;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import javax.annotation.Nonnull;
import lombok.Builder;
import org.slf4j.Logger;
import org.triplea.http.client.HttpClient;

/** Can be used to interact with github's webservice API. */
public class GithubClient {

private static final Logger log = getLogger(GithubClient.class);

/**
* Inner implementation for the HTTP client, Feign interface, we can instantiate this with Feign.
*/
@SuppressWarnings("InterfaceNeverImplemented")
interface GithubApiFeignClient {
@RequestLine("POST /repos/{org}/{repo}/issues")
CreateIssueResponse newIssue(
@Param("org") String org,
@Param("repo") String repo,
CreateIssueRequest createIssueRequest);

@RequestLine("GET /orgs/{org}/repos")
List<MapRepoListing> listRepos(
@QueryMap Map<String, String> queryParams, @Param("org") String org);

@RequestLine("GET /repos/{org}/{repo}/branches/{branch}")
BranchInfoResponse getBranchInfo(
@Param("org") String org, @Param("repo") String repo, @Param("branch") String branch);
public interface GithubClient {

@RequestLine("GET /repos/{org}/{repo}/releases/latest")
LatestReleaseResponse getLatestRelease(@Param("org") String org, @Param("repo") String repo);
static GithubClient build(String authToken, @Nonnull String org) {
return new GithubApiClient(URI.create("https://api.github.com"), authToken, org);
}

private static final String GITHUB_HOST = "https://api.github.com";

private final GithubApiFeignClient githubApiFeignClient;
private final String org;

/**
* @param authToken Auth token that will be sent to Github for webservice calls. Can be empty, but
* if specified must be valid (no auth token still works, but rate limits will be more
* restrictive).
* @param org Name of the github org to be queried.
*/
@Builder
public GithubClient(String authToken, @Nonnull String org) {
this(URI.create(GITHUB_HOST), authToken, org);
static GithubClient build(@Nonnull URI baseUri, String authToken, @Nonnull String org) {
return new GithubApiClient(baseUri, authToken, org);
}

public GithubClient(@Nonnull URI uri, String authToken, @Nonnull String org) {
githubApiFeignClient =
HttpClient.newClient(
GithubApiFeignClient.class,
uri,
Strings.isNullOrEmpty(authToken)
? Map.of()
: Map.of("Authorization", "token " + authToken));
this.org = org;
}
Collection<MapRepoListing> listRepositories();

/**
* Returns a listing of the repositories within a github organization. This call handles paging,
* it returns a complete list and may perform multiple calls to Github.
*
* <p>Example equivalent cUrl call:
*
* <p>curl https://api.github.com/orgs/triplea-maps/repos
*/
public Collection<MapRepoListing> listRepositories() {
final Collection<MapRepoListing> allRepos = new HashSet<>();
int pageNumber = 1;
Collection<MapRepoListing> repos = listRepositories(pageNumber);
while (!repos.isEmpty()) {
pageNumber++;
allRepos.addAll(repos);
repos = listRepositories(pageNumber);
}
return allRepos;
}
Instant getLatestCommitDate(String repoName, String branchName);

private Collection<MapRepoListing> listRepositories(int pageNumber) {
final Map<String, String> queryParams = new HashMap<>();
queryParams.put("per_page", "100");
queryParams.put("page", String.valueOf(pageNumber));
BranchInfoResponse fetchBranchInfo(String repo, String branch);

return githubApiFeignClient.listRepos(queryParams, org);
}

public Instant getLatestCommitDate(String repoName, String branchName) {
return githubApiFeignClient.getBranchInfo(org, repoName, branchName).getLastCommitDate();
}

/**
* Invokes github web-API to create a github issue with the provided parameter data.
*
* @param createIssueRequest Upload data for creating the body and title of the github issue.
* @return Response from server containing link to the newly created issue.
* @throws feign.FeignException thrown on error or if non-2xx response is received
*/
public CreateIssueResponse newIssue(String repo, CreateIssueRequest createIssueRequest) {
return githubApiFeignClient.newIssue(org, repo, createIssueRequest);
}

/**
* Fetches details of a specific branch on a specific repo. Useful for retrieving info about the
* last commit to the repo. Note, the repo listing contains a 'last_push' date, but this method
* should be used instead as the last_push date on a repo can be for any branch (even PRs).
*
* <p>Example equivalent cUrl:
* https://api.github.com/repos/triplea-maps/star_wars_galactic_war/branches/master
*
* @param branch Which branch to be queried.
* @return Payload response object representing the response from Github's web API.
*/
public BranchInfoResponse fetchBranchInfo(String repo, String branch) {
return githubApiFeignClient.getBranchInfo(org, repo, branch);
}
Optional<String> fetchLatestVersion(String repo);

public Optional<String> fetchLatestVersion(String repo) {
try {
return Optional.of(githubApiFeignClient.getLatestRelease(org, repo).getTagName());
} catch (final FeignException e) {
log.warn("No data received from server for latest engine version", e);
return Optional.empty();
}
}
CreateIssueResponse newIssue(String repo, CreateIssueRequest createIssueRequest);
}
4 changes: 2 additions & 2 deletions src/main/java/org/triplea/server/SupportServerConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -50,10 +50,10 @@ public class SupportServerConfig extends Configuration {
private boolean logSqlStatements;

public GithubClient githubClientMaps() {
return GithubClient.builder().authToken(githubApiToken).org(githubMapsOrgName).build();
return GithubClient.build(githubApiToken, githubMapsOrgName);
}

public GithubClient githubClientErrorReporting() {
return GithubClient.builder().authToken(githubApiToken).org(tripleaOrgName).build();
return GithubClient.build(githubApiToken, tripleaOrgName);
}
}
Loading
Loading