GgceVersionCheck.java

/*
 * Copyright 2026 Global Crop Diversity Trust
 * Licensed under the Apache License, Version 2.0
 * See LICENSE file in project root folder or http://www.apache.org/licenses/LICENSE-2.0
 */

package org.gringlobal.worker;

import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.commons.lang3.Strings;
import org.springframework.scheduling.annotation.Scheduled;
import org.gringlobal.service.TransientMessageService;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import com.fasterxml.jackson.databind.ObjectMapper;

import lombok.extern.slf4j.Slf4j;

/**
 * Check for latest GGCE release version
 */
@Component
@Slf4j
public class GgceVersionCheck {

	@Value("${frontend.url}")
	private String frontendUrl;

	@Value("${build.version}")
	private String buildVersion;

	@Value("${disable.version.check:false}")
	private boolean disableVersionCheck;

	private final TransientMessageService transientMessageService;
	private final ObjectMapper objectMapper;

	public GgceVersionCheck(TransientMessageService transientMessageService, ObjectMapper objectMapper) {
		this.transientMessageService = transientMessageService;
		this.objectMapper = objectMapper;
	}

	/**
	 * Periodically checks for a newer GGCE version.
	 * If a newer version is found, an administrator alert is created.
	 */
	@Scheduled(initialDelayString = "PT10S", fixedDelayString = "P7D")
	public void check() {
		if (disableVersionCheck) {
			return;
		}
		var latest = getLatestVersion();
		if (latest != null) {
			String latestVersion = latest.get("version");
			log.info("Checking GGCE version: current={} latest={}", buildVersion, latestVersion);
			if (isNewerVersion(latestVersion, buildVersion)) {
				String releaseNotes = latest.get("releaseNotes");
				transientMessageService.addAdminAlert(
					"ggceVersionAlert",
					String.format("GGCE %s is available! You are using %s. See https://ggce.genesys-pgr.org%s for more information",
						latestVersion, buildVersion, releaseNotes)
				);
			}
		}
	}

	public static boolean isNewerVersion(String latest, String current) {
		if (Strings.CI.equals(latest, current)) {
			return false;
		}
		String[] latestParts = latest.split("\\.");
		String[] currentParts = current.split("\\.");

		int length = Math.max(latestParts.length, currentParts.length);
		for (int i = 0; i < length; i++) {
			int l = i < latestParts.length ? getNumericPrefix(latestParts[i]) : 0;
			int c = i < currentParts.length ? getNumericPrefix(currentParts[i]) : 0;
			if (l > c) return true;
			if (l < c) return false;
		}
		return false;
	}

	private static int getNumericPrefix(String part) {
		if (part == null) return 0;
		Matcher matcher = Pattern.compile("^(\\d+)").matcher(part);
		if (matcher.find()) {
			return Integer.parseInt(matcher.group(1));
		}
		return 0;
	}

	/**
	 * Fetches the latest release information from the GGCE releases JSON endpoint.
	 *
	 * @return a map containing "version" and "releaseNotes", or null if fetching failed.
	 */
	public Map<String, String> getLatestVersion() {
		try {
			String userAgent = String.format("ggce-server/%s (%s; %s; %s)", buildVersion, System.getProperty("os.name"), System.getProperty("os.version"), System.getProperty("os.arch"));
			var client = HttpClient.newBuilder()
				.connectTimeout(Duration.ofSeconds(10))
				.build();
			var request = HttpRequest.newBuilder()
				.uri(URI.create("https://ggce.genesys-pgr.org/releases.json"))
				.timeout(Duration.ofSeconds(10))
				.header("User-Agent", userAgent)
				.header("Referer", frontendUrl)
				.GET()
				.build();

			var response = client.send(request, HttpResponse.BodyHandlers.ofString());
			log.debug("GGCE releases.json: {}", response.body());
			if (response.statusCode() == 200) {
				var releases = objectMapper.readTree(response.body());
				if (releases.isArray() && releases.size() > 0) {
					var latestRelease = releases.get(0);
					return Map.of(
						"version", latestRelease.get("version").asText(),
						"date", latestRelease.get("date").asText(),
						"releaseNotes", latestRelease.get("releaseNotes").asText()
					);
				}
			} else {
				log.warn("Could not fetch GGCE releases, received status code: {}", response.statusCode());
			}
		} catch (Exception e) {
			log.error("Error fetching GGCE version: {}", e.getMessage());
		}
		return null;
	}
}