1import org.json.JSONArray;
2import org.json.JSONObject;
3
4import java.io.IOException;
5import java.io.PrintWriter;
6import java.net.URI;
7import java.net.http.*;
8import java.nio.file.Files;
9import java.nio.file.Path;
10import java.util.*;
11import java.util.regex.Pattern;
12import java.util.stream.Collectors;
13import java.util.stream.Stream;
14import java.util.stream.StreamSupport;
15
16public class JavaUpdater {
17
18 record GitHubResult(Optional<String> latestVersion, Optional<String> next) {
19 }
20
21 record JsonInfo(String repo, String version, String hash) {
22 public JsonInfo(JSONObject json) {
23 this(json.getString("repo"), json.getString("version"), json.getString("hash"));
24 }
25
26 public String toJsonString(String featureVersion) {
27 return """
28 \s "%s": {
29 \s "version": "%s",
30 \s "repo": "%s",
31 \s "hash": "%s"
32 \s }\
33 """.formatted(featureVersion, version, repo, hash);
34 }
35 }
36
37 // Parses the GitHub Link header
38 public static Optional<String> getNextLink(HttpHeaders headers) {
39 var linkHeader = headers.map().get("Link");
40 if (linkHeader == null || linkHeader.isEmpty()) return null;
41
42 var links = linkHeader.getFirst();
43 var linksRegex = Pattern.compile("<(.+)>;\\s*rel=\"next\"");
44 return Pattern.compile(",")
45 .splitAsStream(links)
46 .map(x -> linksRegex.matcher(x).results()
47 .map(g -> g.group(1))
48 .findFirst()
49 )
50 .filter(Optional::isPresent)
51 .map(Optional::orElseThrow)
52 .findFirst();
53 }
54
55 // HTTP request helper, sets GITHUB_TOKEN if present
56 private static HttpRequest NewGithubRequest(String url) {
57 var token = System.getenv().get("GITHUB_TOKEN");
58 var builder = HttpRequest.newBuilder()
59 .uri(URI.create(url));
60 if (token != null)
61 builder.setHeader("Authorization", "Bearer " + token);
62 return builder.build();
63 }
64
65 private static GitHubResult getLatestTag(String url) {
66 var request = NewGithubRequest(url);
67
68 var response =
69 HttpClient.newHttpClient().sendAsync(request, HttpResponse.BodyHandlers.ofString())
70 .join();
71
72 var json = new JSONArray(response.body());
73
74 Optional<String> version = StreamSupport.stream(json.spliterator(), false)
75 .map(JSONObject.class::cast)
76 .map(x -> x.getString("name").replaceFirst("jdk-", ""))
77 .filter(x -> x.contains("-ga"))
78 .max(Comparator.comparing(Runtime.Version::parse));
79
80 return new GitHubResult(version, getNextLink(response.headers()));
81 }
82
83 public String findNewerVersion() {
84 var url = Optional.of("https://api.github.com/repos/openjdk/" + getRepo() + "/tags?per_page=100");
85 String version = getCurrentVersion();
86 do {
87 GitHubResult response = getLatestTag(url.orElseThrow());
88 if (response.latestVersion.isPresent() && response.latestVersion.orElseThrow().equals(version)) {
89 return null;
90 }
91
92 String latestVersion = Stream.of(version, response.latestVersion.orElse(version))
93 .max(Comparator.comparing(Runtime.Version::parse)).orElseThrow();
94
95 if (latestVersion != version)
96 return latestVersion;
97
98 url = response.next;
99 } while (url.isPresent());
100 return null;
101 }
102
103
104 private static String prettyPrint(JSONObject json) {
105
106 Iterable<String> iterable = () -> json.keys();
107
108 return StreamSupport
109 .stream(iterable.spliterator(), false)
110 .sorted(Comparator.reverseOrder())
111 .map(majorVersion -> (new JsonInfo(json.getJSONObject(majorVersion))).toJsonString(majorVersion))
112 .collect(
113 Collectors.joining(",\n", "{\n", "\n}")
114 );
115 }
116
117 public void updateJsonInfo(String newVersion) {
118 try {
119 JSONObject json = getJsonInfo();
120 var info = json.getJSONObject(featureNumber);
121 info.put("version", newVersion);
122 info.put("hash", nixHash(newVersion));
123
124 try (PrintWriter out = new PrintWriter(infoJsonPath)) {
125 out.println(prettyPrint(json));
126 }
127
128 } catch (Exception e) {
129 throw new RuntimeException(e);
130 }
131 }
132
133 private String nixHash(String version) {
134 try {
135 var process = new ProcessBuilder("nix", "flake", "prefetch",
136 "--extra-experimental-features", "'nix-command flakes'",
137 "--json", "github:openjdk/" + getRepo() + "/jdk-" + version).start();
138
139 var json = new JSONObject(new String(process.getInputStream().readAllBytes()));
140 process.waitFor();
141 return json.getString("hash");
142 } catch (Exception e) {
143 throw new RuntimeException(e);
144 }
145 }
146
147 private final String featureNumber;
148 private final String infoJsonPath;
149 private final JSONObject jsonInfo;
150
151 public String getCurrentVersion() {
152 return this.jsonInfo.getJSONObject(this.featureNumber).getString("version");
153 }
154
155 public String getRepo() {
156 return this.jsonInfo.getJSONObject(this.featureNumber).getString("repo");
157 }
158
159 public JSONObject getJsonInfo() {
160 try {
161 String infoStr = Files.readString(Path.of(this.infoJsonPath));
162 return new JSONObject(infoStr);
163 } catch (IOException e) {
164 throw new RuntimeException(e);
165 }
166 }
167
168 public JavaUpdater(String featureNumber, String infoJsonPath) {
169 this.featureNumber = featureNumber;
170 this.infoJsonPath = infoJsonPath;
171 this.jsonInfo = getJsonInfo();
172 }
173
174 public static void main(String[] args) {
175 var updater = new JavaUpdater(args[0], args[1]);
176 String newerVersion = updater.findNewerVersion();
177 if (newerVersion != null) {
178 updater.updateJsonInfo(newerVersion);
179 }
180 }
181}