GithubResolver.java
package com.olepoeschl.upme;
import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.jspecify.annotations.NullMarked;
import org.semver4j.Semver;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
@NullMarked
public class GithubResolver implements UpdateResolver {
private static final ObjectMapper mapper = new ObjectMapper();
private final String releasesUrl, updateFileAssetPattern;
private final Map<String, String> headers = new HashMap<String, String>();
public GithubResolver(String repoOwner, String repoName, String updateFileAssetPattern) {
releasesUrl = "https://api.github.com/repos/%s/%s/releases".formatted(repoOwner, repoName);
this.updateFileAssetPattern = updateFileAssetPattern;
}
public String getUrl() {
return releasesUrl;
}
public Map<String, String> getHeaders() {
return headers;
}
public void addHeader(String key, String value) {
headers.put(key, value);
}
@Override
public Version[] checkAvailableUpdates(String currentVersionString) throws IOException {
var currentSemver = Semver.parse(currentVersionString);
if(currentSemver == null)
throw new IllegalArgumentException("not a valid semantic versioning string: " + currentVersionString);
final List<Version> versions = new ArrayList<>();
try {
var reqBuilder = HttpRequest.newBuilder()
.uri(URI.create(releasesUrl))
.GET();
for(var header : headers.entrySet())
reqBuilder.header(header.getKey(), header.getValue());
HttpRequest request = reqBuilder.build();
try (HttpClient client = HttpClient.newHttpClient()) {
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
if(response.statusCode() < 200 || response.statusCode() > 299)
throw new IOException("could not fetch available updates form Github: status code "
+ response.statusCode() + ": " + response.body());
var rootNode = mapper.readTree(response.body());
if(rootNode instanceof ArrayNode) {
rootNode.forEach(releaseNode -> {
var versionString = releaseNode.get("tag_name").asText();
if(currentSemver.isGreaterThanOrEqualTo(versionString))
return;
// check if the release has an asset that matches the updateFileAssetPattern
String updateAssetDownloadUrl = null; // null means there is no matching asset
String checksum = null;
var assetsNode = releaseNode.get("assets");
if(assetsNode instanceof ArrayNode) {
for(int i = 0; i < assetsNode.size(); i++) {
var asset = assetsNode.get(i);
if(asset.get("name").asText().equals(updateFileAssetPattern)) {
updateAssetDownloadUrl = asset.get("browser_download_url").asText();
checksum = asset.get("digest").asText().substring(7); // always begins with "sha256:"
break;
}
}
}
// if a matching asset was found, add this version to the list
if(updateAssetDownloadUrl != null) {
var description = releaseNode.get("body").asText();
versions.add(new Version(versionString, updateAssetDownloadUrl, description, checksum));
}
});
}
return versions.toArray(Version[]::new);
}
} catch (Exception e) {
throw new IOException("could not fetch available updates from Github: " + e.getMessage(), e);
}
}
}