WebResolver.java

package com.olepoeschl.upme;

import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.jspecify.annotations.NullMarked;
import org.semver4j.Semver;

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.HashSet;

/**
 * An {@link UpdateResolver} implementation that uses HTTP / HTTPS GET to check for available updates by reading a json
 * array of {@link Version} objects from a specified URL.
 * <p>
 *     This is the go-to resolver for applications with a self-hosted update server.
 * </p>
 *
 * @see Version
 * @see UpdateResolver
 * @version 1.0.0
 * @since 1.0.0
 */
@NullMarked
public record WebResolver(String url) implements UpdateResolver {

    private static final ObjectMapper mapper = new ObjectMapper();

    /**
     * Checks for available updates by sending an HTTP GET request to the specified URL, which is expected to return a
     * JSON array of {@link Version} objects. See the {@link Version} class for the expected structure of the JSON
     * response.
     *
     * @param currentVersionString the current version of the application as a semantic versioning string
     * @return an array of {@link Version} objects representing available updates
     * @throws IOException if an error occurs while sending the request or processing the response
     *
     * @see Version
     * @since 1.0.0
     * @implNote This method uses Java's built-in HTTP client.
     */
    @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);

        try {
            HttpRequest request = HttpRequest.newBuilder()
                .uri(URI.create(url))
                .GET()
                .build();

            try (HttpClient client = HttpClient.newHttpClient()) {
                HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
                var availableVersions = mapper.readValue(response.body(), new TypeReference<HashSet<Version>>() {
                });

                availableVersions.removeIf(v -> new Semver(v.versionString()).isLowerThanOrEqualTo(currentSemver));
                return availableVersions.toArray(new Version[0]);
            }
        } catch (Exception e) {
            throw new IOException("could not check for available updates: " + e.getMessage(), e);
        }
    }

}