package com.trafficaudit.system.controller; import com.trafficaudit.common.Result; import org.springframework.core.io.ClassPathResource; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import java.io.InputStream; import java.net.URI; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.LinkedHashMap; import java.util.Map; import java.util.Properties; /** * Exposes the product version so the UI (and operators) can confirm which build * is actually running on a server. Kept public on purpose: the login page shows * the version before any token exists. */ @RestController @RequestMapping("/api") public class VersionController { private static final String VERSION_FILE = "app-version.properties"; @GetMapping("/version") public Result> version() { Map info = new LinkedHashMap<>(); info.put("version", readVersion()); info.put("buildTime", resolveBuildTime()); return Result.ok(info); } private String readVersion() { try (InputStream in = new ClassPathResource(VERSION_FILE).getInputStream()) { Properties props = new Properties(); props.load(in); String value = props.getProperty("version"); return value == null || value.trim().isEmpty() ? "unknown" : value.trim(); } catch (Exception e) { return "unknown"; } } /** * Build time of the running artifact. In a packaged run this is the jar file's * last-modified time; when running from exploded classes (mvn spring-boot:run) * there is no jar, so the value is null. */ private String resolveBuildTime() { try { URI location = VersionController.class.getProtectionDomain() .getCodeSource().getLocation().toURI(); Path path = resolveArtifactPath(location); if (path != null && Files.isRegularFile(path)) { return Files.getLastModifiedTime(path).toInstant().toString(); } } catch (Exception ignored) { // Fall through: build time is optional information. } return null; } /** * Spring Boot executable jars expose a nested classpath such as * "jar:file:/app.jar!/BOOT-INF/classes!/". Strip the nested part before * asking the filesystem for the jar timestamp. */ private Path resolveArtifactPath(URI location) { String value = location.toString(); int nested = value.indexOf("!/"); if (nested >= 0) { value = value.substring(0, nested); } if (value.startsWith("jar:")) { value = value.substring(4); } return Paths.get(URI.create(value)); } }