1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 package org.apache.maven.plugins.toolchain.jdk;
20
21 import javax.inject.Named;
22 import javax.inject.Singleton;
23
24 import java.io.IOException;
25 import java.io.Reader;
26 import java.io.Writer;
27 import java.nio.file.Files;
28 import java.nio.file.Path;
29 import java.nio.file.Paths;
30 import java.util.ArrayList;
31 import java.util.Arrays;
32 import java.util.Collections;
33 import java.util.Comparator;
34 import java.util.HashMap;
35 import java.util.LinkedHashMap;
36 import java.util.List;
37 import java.util.Locale;
38 import java.util.Map;
39 import java.util.Objects;
40 import java.util.Optional;
41 import java.util.Set;
42 import java.util.concurrent.ConcurrentHashMap;
43 import java.util.function.Function;
44 import java.util.stream.Collectors;
45 import java.util.stream.Stream;
46
47 import org.apache.maven.toolchain.model.PersistedToolchains;
48 import org.apache.maven.toolchain.model.ToolchainModel;
49 import org.apache.maven.toolchain.model.io.xpp3.MavenToolchainsXpp3Reader;
50 import org.apache.maven.toolchain.model.io.xpp3.MavenToolchainsXpp3Writer;
51 import org.codehaus.plexus.util.xml.Xpp3Dom;
52 import org.codehaus.plexus.util.xml.pull.XmlPullParserException;
53 import org.slf4j.Logger;
54 import org.slf4j.LoggerFactory;
55
56 import static java.util.Comparator.comparing;
57 import static org.apache.maven.plugins.toolchain.jdk.SelectJdkToolchainMojo.TOOLCHAIN_TYPE_JDK;
58
59
60
61
62
63
64
65 @Named
66 @Singleton
67 public class ToolchainDiscoverer {
68
69 public static final String JAVA = "java.";
70 public static final String VERSION = "version";
71 public static final String RUNTIME_NAME = "runtime.name";
72 public static final String RUNTIME_VERSION = "runtime.version";
73 public static final String VENDOR = "vendor";
74 public static final String VENDOR_VERSION = "vendor.version";
75 public static final String[] PROPERTIES = {VERSION, RUNTIME_NAME, RUNTIME_VERSION, VENDOR, VENDOR_VERSION};
76
77 public static final String CURRENT = "current";
78 public static final String ENV = "env";
79 public static final String LTS = "lts";
80
81 public static final List<String> SORTED_PROVIDES = Collections.unmodifiableList(
82 Arrays.asList(VERSION, RUNTIME_NAME, RUNTIME_VERSION, VENDOR, VENDOR_VERSION, CURRENT, LTS, ENV));
83
84 public static final String DISCOVERED_TOOLCHAINS_CACHE_XML = ".m2/discovered-jdk-toolchains-cache.xml";
85
86 public static final String JDK_HOME = "jdkHome";
87 public static final String JAVA_HOME = "java.home";
88
89 private static final String COMMA = ",";
90 public static final String USER_HOME = "user.home";
91
92 private final Logger log = LoggerFactory.getLogger(getClass());
93
94 private volatile Map<Path, ToolchainModel> cache;
95 private volatile boolean cacheModified;
96 private volatile Set<Path> foundJdks;
97
98
99
100
101 public Optional<ToolchainModel> getCurrentJdkToolchain() {
102 Path currentJdkHome = getCanonicalPath(Paths.get(System.getProperty(JAVA_HOME)));
103 if (!hasJavaC(currentJdkHome)) {
104
105 return Optional.empty();
106 }
107 ToolchainModel model = new ToolchainModel();
108 model.setType(TOOLCHAIN_TYPE_JDK);
109 Stream.of(PROPERTIES).forEach(k -> {
110 String v = System.getProperty(JAVA + k);
111 if (v != null) {
112 model.addProvide(k, v);
113 }
114 });
115 model.addProvide(CURRENT, "true");
116 Xpp3Dom config = new Xpp3Dom("configuration");
117 Xpp3Dom jdkHome = new Xpp3Dom(JDK_HOME);
118 jdkHome.setValue(currentJdkHome.toString());
119 config.addChild(jdkHome);
120 model.setConfiguration(config);
121 return Optional.of(model);
122 }
123
124 public PersistedToolchains discoverToolchains() {
125 return discoverToolchains(LTS + COMMA + VERSION + COMMA + VENDOR);
126 }
127
128
129
130
131
132 public PersistedToolchains discoverToolchains(String comparator) {
133 try {
134 Set<Path> jdks = findJdks();
135 log.info("Found " + jdks.size() + " possible jdks: " + jdks);
136 readCache();
137 Map<Path, Map<String, String>> flags = new HashMap<>();
138 Path currentJdkHome = getCanonicalPath(Paths.get(System.getProperty(JAVA_HOME)));
139 flags.computeIfAbsent(currentJdkHome, p -> new HashMap<>()).put(CURRENT, "true");
140
141 System.getenv().entrySet().stream()
142 .filter(e -> e.getKey().startsWith("JAVA") && e.getKey().endsWith("_HOME"))
143 .forEach(e -> {
144 Path path = getCanonicalPath(Paths.get(e.getValue()));
145 Map<String, String> f = flags.computeIfAbsent(path, p -> new HashMap<>());
146 String val = f.getOrDefault(ENV, "");
147 f.put(ENV, (val.isEmpty() ? "" : val + ",") + e.getKey());
148 });
149
150 List<ToolchainModel> tcs = jdks.parallelStream()
151 .map(s -> {
152 ToolchainModel tc = getToolchainModel(s);
153 flags.getOrDefault(s, Collections.emptyMap())
154 .forEach((k, v) -> tc.getProvides().setProperty(k, v));
155 String version = tc.getProvides().getProperty(VERSION);
156 if (isLts(version)) {
157 tc.getProvides().setProperty(LTS, "true");
158 }
159 return tc;
160 })
161 .sorted(getToolchainModelComparator(comparator))
162 .collect(Collectors.toList());
163 writeCache();
164 PersistedToolchains ps = new PersistedToolchains();
165 ps.setToolchains(tcs);
166 return ps;
167 } catch (Exception e) {
168 if (log.isDebugEnabled()) {
169 log.warn("Error discovering toolchains: " + e, e);
170 } else {
171 log.warn("Error discovering toolchains (enable debug level for more information): " + e);
172 }
173 return new PersistedToolchains();
174 }
175 }
176
177 private static boolean isLts(String version) {
178 return Stream.of("1.8", "8", "11", "17", "21", "25")
179 .anyMatch(v -> version.equals(v) || version.startsWith(v + "."));
180 }
181
182 private synchronized void readCache() {
183 if (cache == null) {
184 try {
185 cache = new ConcurrentHashMap<>();
186 cacheModified = false;
187 Path cacheFile = getCacheFile();
188 if (Files.isRegularFile(cacheFile)) {
189 try (Reader r = Files.newBufferedReader(cacheFile)) {
190 PersistedToolchains pt = new MavenToolchainsXpp3Reader().read(r, false);
191 cache = pt.getToolchains().stream()
192
193 .filter(tc -> {
194
195 if (!hasJavaC(getJdkHome(tc))) {
196 cacheModified = true;
197 return false;
198 } else {
199 return true;
200 }
201 })
202 .collect(Collectors.toConcurrentMap(this::getJdkHome, Function.identity()));
203 }
204 }
205 } catch (IOException | XmlPullParserException e) {
206 log.debug("Error reading toolchains cache: " + e, e);
207 }
208 }
209 }
210
211 private synchronized void writeCache() {
212 if (cacheModified) {
213 try {
214 Path cacheFile = getCacheFile();
215 Files.createDirectories(cacheFile.getParent());
216 try (Writer w = Files.newBufferedWriter(cacheFile)) {
217 PersistedToolchains pt = new PersistedToolchains();
218 pt.setToolchains(cache.values().stream()
219 .map(tc -> {
220 ToolchainModel model = tc.clone();
221
222 model.getProvides().remove(CURRENT);
223 model.getProvides().remove(ENV);
224 return model;
225 })
226 .sorted(version().thenComparing(vendor()))
227 .collect(Collectors.toList()));
228 new MavenToolchainsXpp3Writer().write(w, pt);
229 }
230 } catch (IOException e) {
231 log.debug("Error writing toolchains cache: " + e, e);
232 }
233 cacheModified = false;
234 }
235 }
236
237 ToolchainModel getToolchainModel(Path jdk) {
238 ToolchainModel model = cache.get(jdk);
239 if (model == null) {
240 model = doGetToolchainModel(jdk);
241 cache.put(jdk, model);
242 cacheModified = true;
243 }
244 return model;
245 }
246
247 private static Path getCacheFile() {
248 return Paths.get(System.getProperty(USER_HOME)).resolve(DISCOVERED_TOOLCHAINS_CACHE_XML);
249 }
250
251 public Path getJdkHome(ToolchainModel toolchain) {
252 Xpp3Dom dom = (Xpp3Dom) toolchain.getConfiguration();
253 Xpp3Dom javahome = dom != null ? dom.getChild(JDK_HOME) : null;
254 String jdk = javahome != null ? javahome.getValue() : null;
255 return Paths.get(Objects.requireNonNull(jdk));
256 }
257
258 ToolchainModel doGetToolchainModel(Path jdk) {
259 Path java = jdk.resolve("bin").resolve("java");
260 if (!Files.exists(java)) {
261 java = jdk.resolve("bin").resolve("java.exe");
262 if (!Files.exists(java)) {
263 log.debug("JDK toolchain discovered at " + jdk
264 + " will be ignored: unable to find bin/java or bin\\java.exe");
265 return null;
266 }
267 }
268 if (!java.toFile().canExecute()) {
269 log.debug("JDK toolchain discovered at " + jdk
270 + " will be ignored: the bin/java or bin\\java.exe is not executable");
271 return null;
272 }
273 List<String> lines;
274 try {
275 Path temp = Files.createTempFile("jdk-opts-", ".out");
276 try {
277 new ProcessBuilder()
278 .command(java.toString(), "-XshowSettings:properties", "-version")
279 .redirectError(temp.toFile())
280 .start()
281 .waitFor();
282 lines = Files.readAllLines(temp);
283 } finally {
284 Files.delete(temp);
285 }
286 } catch (IOException | InterruptedException e) {
287 log.debug("JDK toolchain discovered at " + jdk + " will be ignored: error executing java: " + e);
288 return null;
289 }
290
291 Map<String, String> properties = new LinkedHashMap<>();
292 Stream.of(PROPERTIES).forEach(name -> {
293 lines.stream()
294 .filter(l -> l.contains(JAVA + name))
295 .map(l -> l.replaceFirst(".*=\\s*(.*)", "$1"))
296 .findFirst()
297 .ifPresent(value -> properties.put(name, value));
298 });
299 if (!properties.containsKey(VERSION)) {
300 log.debug("JDK toolchain discovered at " + jdk + " will be ignored: could not obtain " + JAVA + VERSION);
301 return null;
302 }
303
304 ToolchainModel model = new ToolchainModel();
305 model.setType(TOOLCHAIN_TYPE_JDK);
306 properties.forEach(model::addProvide);
307 Xpp3Dom configuration = new Xpp3Dom("configuration");
308 Xpp3Dom jdkHome = new Xpp3Dom(JDK_HOME);
309 jdkHome.setValue(jdk.toString());
310 configuration.addChild(jdkHome);
311 model.setConfiguration(configuration);
312 return model;
313 }
314
315 private static Path getCanonicalPath(Path path) {
316 try {
317 return path.toRealPath();
318 } catch (IOException e) {
319 return getCanonicalPath(path.getParent()).resolve(path.getFileName());
320 }
321 }
322
323 Comparator<ToolchainModel> getToolchainModelComparator(String comparator) {
324 Comparator<ToolchainModel> c = null;
325 for (String part : comparator.split(COMMA)) {
326 c = c == null ? getComparator(part) : c.thenComparing(getComparator(part));
327 }
328 return c;
329 }
330
331 private Comparator<ToolchainModel> getComparator(String part) {
332 switch (part.trim().toLowerCase(Locale.ROOT)) {
333 case LTS:
334 return lts();
335 case VENDOR:
336 return vendor();
337 case ENV:
338 return env();
339 case CURRENT:
340 return current();
341 case VERSION:
342 return version();
343 default:
344 throw new IllegalArgumentException("Unsupported comparator: " + part
345 + ". Supported comparators are: vendor, env, current, lts and version.");
346 }
347 }
348
349 Comparator<ToolchainModel> lts() {
350 return comparing((ToolchainModel tc) -> tc.getProvides().containsKey(LTS) ? -1 : +1);
351 }
352
353 Comparator<ToolchainModel> vendor() {
354 return comparing((ToolchainModel tc) -> tc.getProvides().getProperty(VENDOR));
355 }
356
357 Comparator<ToolchainModel> env() {
358 return comparing((ToolchainModel tc) -> tc.getProvides().containsKey(ENV) ? -1 : +1);
359 }
360
361 Comparator<ToolchainModel> current() {
362 return comparing((ToolchainModel tc) -> tc.getProvides().containsKey(CURRENT) ? -1 : +1);
363 }
364
365 Comparator<ToolchainModel> version() {
366 return comparing((ToolchainModel tc) -> tc.getProvides().getProperty(VERSION), (v1, v2) -> {
367 String[] a = v1.split("\\.");
368 String[] b = v2.split("\\.");
369 int length = Math.min(a.length, b.length);
370 for (int i = 0; i < length; i++) {
371 String oa = a[i];
372 String ob = b[i];
373 if (!Objects.equals(oa, ob)) {
374
375 if (oa == null || ob == null) {
376 return oa == null ? -1 : 1;
377 }
378 int v = oa.compareTo(ob);
379 if (v != 0) {
380 return v;
381 }
382 }
383 }
384 return a.length - b.length;
385 })
386 .reversed();
387 }
388
389 private Set<Path> findJdks() {
390 if (foundJdks == null) {
391 synchronized (this) {
392 if (foundJdks == null) {
393 foundJdks = doFindJdks();
394 }
395 }
396 }
397 return foundJdks;
398 }
399
400
401
402
403
404
405 private Set<Path> doFindJdks() {
406 List<Path> dirsToTest = new ArrayList<>();
407
408
409 dirsToTest.add(Paths.get(System.getProperty(JAVA_HOME)));
410
411
412 System.getenv().entrySet().stream()
413 .filter(e -> e.getKey().startsWith("JAVA") && e.getKey().endsWith("_HOME"))
414 .map(e -> Paths.get(e.getValue()))
415 .forEach(dirsToTest::add);
416
417 final Path userHome = Paths.get(System.getProperty(USER_HOME));
418 List<Path> installedDirs = new ArrayList<>();
419
420
421 installedDirs.add(userHome.resolve(".jdks"));
422 installedDirs.add(userHome.resolve(".m2").resolve("jdks"));
423 installedDirs.add(userHome.resolve(".sdkman").resolve("candidates").resolve("java"));
424 installedDirs.add(userHome.resolve(".gradle").resolve("jdks"));
425 installedDirs.add(userHome.resolve(".jenv").resolve("versions"));
426 installedDirs.add(userHome.resolve(".jbang").resolve("cache").resolve("jdks"));
427 installedDirs.add(userHome.resolve(".asdf").resolve("installs"));
428 installedDirs.add(userHome.resolve(".jabba").resolve("jdk"));
429
430
431 String osname = System.getProperty("os.name").toLowerCase(Locale.ROOT);
432 boolean macos = osname.startsWith("mac");
433 boolean win = osname.startsWith("win");
434 if (macos) {
435 installedDirs.add(Paths.get("/Library/Java/JavaVirtualMachines"));
436 installedDirs.add(userHome.resolve("Library/Java/JavaVirtualMachines"));
437 } else if (win) {
438 installedDirs.add(Paths.get("C:\\Program Files\\Java\\"));
439 Path scoop = userHome.resolve("scoop").resolve("apps");
440 if (Files.isDirectory(scoop)) {
441 try (Stream<Path> stream = Files.list(scoop)) {
442 stream.forEach(installedDirs::add);
443 } catch (IOException e) {
444
445 }
446 }
447 } else {
448 installedDirs.add(Paths.get("/usr/jdk"));
449 installedDirs.add(Paths.get("/usr/java"));
450 installedDirs.add(Paths.get("/opt/java"));
451 installedDirs.add(Paths.get("/usr/lib/jvm"));
452 }
453
454 for (Path dest : installedDirs) {
455 if (Files.isDirectory(dest)) {
456 try (Stream<Path> stream = Files.list(dest)) {
457 stream.forEach(dir -> {
458 dirsToTest.add(dir);
459 if (macos) {
460 dirsToTest.add(dir.resolve("Contents").resolve("Home"));
461 }
462 });
463 } catch (IOException e) {
464
465 }
466 }
467 }
468
469
470 return dirsToTest.stream()
471 .filter(ToolchainDiscoverer::hasJavaC)
472 .map(ToolchainDiscoverer::getCanonicalPath)
473 .collect(Collectors.toSet());
474 }
475
476 private static boolean hasJavaC(Path subdir) {
477 return Files.exists(subdir.resolve(Paths.get("bin", "javac")))
478 || Files.exists(subdir.resolve(Paths.get("bin", "javac.exe")));
479 }
480 }